Skip to content
Forbmi
Menu
  • Главная
  • Новости
  • Общий
  • Фриланс
  • Клиенты
Menu

Combine: часть 1. Погружение в реактивное программирование

Posted on 24 января, 2023

Привет, Хабр! Меня зовут Сергей, я iOS-разработчик в компании SimbirSoft.

Уже наступил 2023 год, а обсуждения на тему выбора инструмента для обработки асинхронных событий не утихают. На сцене привычные колбэки, нотификейшн-центры с «бородатыми» Objective-C-селекторами, разные фреймворки для реактивной разработки, а не так давно Apple представила модный Swift Concurrency.

Combine все больше набирает популярность в продакшене. За счет нативного происхождения у него хороший уровень оптимизации, его легко «склеивать» как с существующими легаси-инструментами, так и с новыми — SwiftUI или async/await.

Пестрый «зоопарк» заставляет задуматься: что выбрать для нового проекта, а что для приложения с многолетней историей?

Поскольку Combine является отличным претендентом для разработки современных приложений с перспективой на будущее, о нем и поговорим подробнее.

Это первая часть статьи, где мы познакомимся с Combine, сравним его с RxSwift. Материал будет полезен для тех, кто до этого не сталкивался с реактивщиной, а также тем, кто успел поработать с аналогичными инструментами.

История реактивного программирования

Идея реактивного программирования появилась относительно недавно — в 2009 году компания Microsoft создала фреймворк Reactive Extensions для языка .NET (rx.NET). В 2012 году этот фреймворк выложили в опенсорс.

Со временем появилась возможность писать программы с использованием реактивного подхода на многих популярных языках: RxJS, RxKotlin, RxPHP и других.

Swift не стал исключением. Для создания iOS-приложений чаще всего используется проверенный временем RxSwift, а также нативный инструмент от Apple — Combine. 

Что такое реактивное программирование

Реактивное программирование — это новый и самый высокий уровень абстракции для асинхронной работы с данными.

В его основе лежит паттерн проектирования Observer — это паттерн коммуникации, который используется в качестве уведомления объектов о том, что произошло какое-то событие и нужно на него как-то отреагировать.

Для реализации этого паттерна требуется всего 2 компонента:

В отличие от имеющихся подходов работы с асинхронными событиями (делегаты, коллбэки и прочие), этот подход основан на потоках данных и распространении изменений с течением времени. 

Поток данных — это своего рода конвейер, по которому данные идут от паблишера к подписчику. В отличие от привычного @escaping closure, данные могут поступать порционно, а не один раз.

Распространение изменений — это уведомление всех подписчиков о том, что произошло с данными.

Под течением времени можно понимать упорядоченность изменений.

Данные, которые паблишер может отправить подписчикам, бывают трех видов: значения (например, Bool, String, массив, структура и другие), ошибки и сигнал о том, что паблишер закончил работу и больше ничего не пришлет.

Концепцию проще всего будет объяснить на примере аналогии с социальной сетью:

Допустим, какой-то блогер постоянно публикует контент (фотографии, истории и так далее). У него есть подписчики, которые следят за его публикациями и как-то на них реагируют (лайкают, комментируют или жалуются в техподдержку за оскорбительный пост).

Блогер — это Publisher. Он распространяет изменения (например, в аккаунте была одна фотография, теперь стало две).

Его контент — это Values и Error. Те самые данные, которые он распространяет с течением времени (вчера выложил одну фотографию, сегодня другую).

Его подписчики — это Subscribers. Они воспринимают («обрабатывают») полученные от блогера данные. Например, если пост понравился (валидные данные — Values), подписчики поставили лайки, а если пост не прошел модерацию (ошибка от сервера — Error), подписчики не увидят оскорбительный пост, а вместо него будет висеть баннер о том, что аккаунт заблокирован — это и есть сигнал о том, что Publisher больше ничего не опубликует.

Отличия между реактивным и классическим подходом

Звучит так, будто реактивное программирование — это просто способ сделать привычные вещи по-новому. Однако по сравнению с императивным подходом есть ряд отличий:

1) push вместо pull

Разница в том, как именно мы работаем с данными. Вместо самостоятельного извлечения каких-либо данных (например, из массива или UserDefaults), мы пишем код таким образом, чтобы объект сам отправлял актуальные на текущий момент данные, а также все последующие изменения.

Другими словами, массив (или сервис) перестает быть статичным набором элементов и превращается в поток данных, который сам отправляет содержащиеся в нем элементы один за другим.

2) auto update

В императивном подходе нам надо самостоятельно следить за данными: проверять на актуальность и обновлять.

В реактивном подходе данные всегда будут в консистентном виде.

Например, в социальной сети для авторизованного пользователя могут быть доступны функции, недоступные анонимному пользователю. А также в зависимости от состояния может по-разному выглядеть интерфейс.

Смена статуса авторизации может произойти на одном табе, а перерисовать экраны мы должны во всем приложении. Пользователь может разлогиниться самостоятельно, а также его может выкинуть система, если, допустим, «протухнет» токен. Что делать?

В императивном подходе без использования паттерна наблюдателя у нас был бы сервис с таймером, время от времени проверяющий статус пользователя. Как только статус изменится, мы бы сохранили его и принудительно перерисовали интерфейс, начав заново опрашивать сервис о статусе. В этом случае мы похожи на ослика из Шрека: «А сейчас? Уже можно? Может, пора?».

В реактивном подходе мы бы просто подписались на изменения в сервисе на нужных экранах и перерисовывали интерфейс при необходимости:

Примером из мира iOS-разработки может послужить переход от MRC к ARC — когда было необходимо самостоятельно следить за количеством сильных ссылок на объект и вызывать методы retain / release.

3) Декларативный стиль

Высокий уровень абстракции позволяет писать лаконичные конструкции с точечным синтаксисом.

Императивный стиль:

Декларативный стиль:

Подведем итоги разбора реактивного подхода:

Реактивное программирование в iOS

В iOS-разработке время от времени появляются новые инструменты для создания приложений в реактивном подходе, но фаворитов только два: RxSwift и Combine.

RxSwift появился раньше Combine на 4 года. Он проверен временем, по нему есть множество материалов здесь на Хабре и на Stackoverflow.

Одно из существенных преимуществ RxSwift — обвязки с UIKit. Нам прямо из коробки доступны инструменты, позволяющие в декларативном стиле работать с UI-элементами:

В Combine есть возможность добавить аналогичную механику, но для этого придется написать пару сотен строк кода самостоятельно. Все-таки этот инструмент был представлен Apple совместно с новым фреймворком для верстки — SwiftUI.

Подводя черту, отметим, что у обоих инструментов есть свои за и против. Для более подробного сравнения фреймворков и возможности реализации обвязок с UIkit на Combine оставлю несколько полезных ссылок, и перейдем к основной теме статьи:

Hello, Combine

В современных iOS-приложениях существует множество асинхронных событий. Загрузка данных из сети, нажатия на кнопки, push-уведомления, смена жизненного цикла при входящем звонке или открытие шторки с панелью управления и так далее.

Для обработки этих асинхронных событий есть множество инструментов и подходов:

Combine предоставляет единый API для обработки множества асинхронных событий в одном стиле. Мы даже можем комбинировать разные потоки данных как URLSession + Notification Center. И нам не придется писать свой костыль или тащить в проект стороннюю зависимость. Отмечу, что Combine обладает строгой типизацией и обработкой ошибок.

Для того чтобы начать работать с Combine, необходимо познакомиться с тремя основными компонентами: паблишерами, подписчиками и операторами.

Publisher

Publisher — это начальная точка потока данных. По сути это объект, который создает какие-либо данные. Паблишером может быть любой объект, удовлетворяющий требованиям протокола Publisher, с двумя ассоциированными типами: Output и Failure.

Output — это и есть генерируемые данные какого-либо типа (например, String).

Failure — это ошибка, которую может сгенерировать паблишер при неудачной операции. Она бывает двух типов: Error и Never, который используется в том случае, если мы уверены, что ошибка произойти не может.

Мы только что создали первый паблишер на базе массива строк. Паблишер сам по себе бесполезен, если нет подписчика, ведь генерируемые данные надо как-то обработать.

Например, если мы захотим вывести все данные, полученные от паблишера (элементы массива), то нам потребуется подписчик.

Subscriber

Subscriber — это конечная точка потока данных, далее будем называть его подписчик. По сути это объект, который подписывается на паблишер и взаимодействуют с полученными и паблишера данными.

Он представлен протоколом с двумя ассоциированными типами: Input и Failure.

Input — данные определенного типа, который он может обработать.

Failure — ошибка, которая может прийти от паблишера.

.sink — это и есть подписчик. Посмотрите на его сигнатуру:

Этот метод не только отдает нам данные, полученные от паблишера с помощью @escaping closure, но также возвращает что-то с типом AnyCancellable. Что это и зачем оно надо?

Как только подписчик подписывается на паблишер, эту подписку надо где-то хранить. Зачем?

Для сохранения подписки создается коллекция с типом AnyCancellable. Обычно подписку сохраняют с помощью метода .store(in:), но можно и с помощью знака =

Publisher + Subscriber

По сути для создания потока данных нам достаточно только два элемента — паблишер и подписчик. Но при условии, что нам не нужно осуществлять дополнительных операций над данными. Единственное требование, чтобы у них совпадали ассоциированные типы Output — Input и Failure.

В то же время, если полученные от паблишера данные необходимо предварительно обработать, прежде чем передать дальше по конвейеру подписчику, используется третий компонент комбайна — оператор.

Operator

Operator — это промежуточная точка потока данных. Оператор может быть один, несколько или вообще ни одного.

На примере .map(), оператор — это тоже паблишер, но с одним важным отличием: оператору обязательно нужна начальная точка в виде паблишера-предшественника. Без него оператор сам по себе существовать не может.

Операторы — это чистые функции. Они выступают в качестве «мостика» между паблишером и подписчиком. Их основная задача — обработать полученные от паблишера данные, прежде чем отдать их подписчику. Например, если паблишер отдает объект с типом данных Int, а подписчик настроен на получение данных типа String, оператор .map() может преобразовать эти данные в нужный тип.

Общая картина работы паблишеров, подписчиков и операторов может выглядеть следующим образом: 

Есть начальная точка потока данных (паблишер), конечная точка, которая их получает (подписчик), и операторы, которые поочередно преобразуют данные.

Вся эта цепочка — это и есть Data stream.

Вывод

Итак, в этой статье мы познакомились с концепцией реактивного программирования и базовыми частями Combine.

Использование этого фреймворка на проекте позволяет писать код быстрее и безопаснее с точки зрения консистенции данных. В то же время Combine достаточно молодой, поэтому его применение актуально только для приложений с минимальной версией iOS 13. Если по какой-то причине требуется поддержка более ранних версий операционных систем, но хочется получить те же преимущества, стоит присмотреться к другим альтернативам, например, RxSwift.

В следующей части мы разберем виды паблишеров и их особенности, сравним подписчиков, а также познакомимся с наиболее часто используемыми операторами.

Спасибо за внимание!

Полезные материалы для разработчиков мы также публикуем в наших соцсетях – ВК и Telegram.

Добавить комментарий Отменить ответ

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Категории

  • Клиенты
  • Фриланс
  • Общий
  • Новости

Рассылка новостей

Язык

  • English
  • Español
  • Русский

Поддержка

  • О нас
  • Контакты
  • Политика конфиденциальности

Информация

Адрес: 9210 W Lisbon Ave, Milwaukee, WI 53222, USA

Телефон: +14144615305

©2023 Forbmi | Design: Newspaperly WordPress Theme