С примерами реализации
Возможно, вы уже изучили нативный фреймворк Combine, выпущенный Apple на WWDC19, и запомнили такой огромный список издателей, подписчиков и операторов.
Возможно, вы знаете только то, что первый отвечает за генерацию новых асинхронных значений, которые должны обновляться вторым. Хорошо, из чего он состоит! Но часто многие из нас не вникали в то, как эти протоколы взаимодействуют между собой, и некоторые могут не знать, что это протоколы (ага!).
Итак, в этой статье мы поговорим о том, как создавать своих настраиваемых издателей, подписки и подписчиков, реализуя эти протоколы таким образом, чтобы, когда издатель выдает какое-то новое значение, все его подписки могли сообщать об этом подписчикам, чтобы получить правильная обработка (и нет, sink
и assign
вообще не единственные доступные подписчики).
Понимание каждого из них
Для начала важно понять истинные отношения между этими тремя сущностями: Publisher
, Subscription
и Subscriber
, и, в основном, из чего состоят их интерфейсные контракты.
Apple не рекомендует вам реализовывать тип Publisher
, потому что все нативные и предопределенные уже выполняют правильные задачи, чтобы поддерживать идеальную синхронность между ними и их подписчиками, но эта статья направлена на то, чтобы понять, как на самом деле работает эта синхронность, поэтому давайте двигаться дальше!
1. Издатель
Издатель — это тип, соответствующий протоколу Publisher
, и, как уже понятно из названия, он предназначен для публикации значений. Он имеет два общих типа внутри: Output
, который является типом значения, которое он должен передавать своему subscribers
, и тип Failure
, который наследуется от Swift Error
и предназначен для доставки подписчику некоторой ошибки, которая произошла во время подписки.
Как видите, Publisher
состоит из двух связанных типов и одного метода, который получает subscriber
в качестве параметра. При вызове этого метода пользовательский Publisher
должен создать объект Subscription
и отправить его в Subscriber
.
Кратко описывая жизненный цикл Publisher
, он начинается с вызова этого метода receive
, а затем отправляет subscription
методу subscriber
. publisher
продолжает отправлять значения асинхронно, а затем останавливается двумя исключительными способами: по завершению или по сбою, что соответствует общему типу Failure
.
2. Подписка
Объект подписки не так хорошо известен разработчикам Combine, как следовало бы. Важно сказать об этом протоколе то, что объект, который его реализует, отвечает за связывание subscriber
с publisher
. Пока он находится в памяти, subscriber
продолжает получать значения. Он состоит всего из одного метода:
Метод request
вызывается subscriber
, как только он получает subscription
от publisher
, и он отвечает за определение того, сколько значений subscriber
запрашивает у publisher
, что определяется перечислением Demand
.
Это может быть none
, что соответствует отсутствию значения для получения, max(value)
, которое определяет, что подписчик запрашивает value
раз, или unlimited
, что соответствует получению бесконечного значения, пока subscription
жив (не получает завершение от publisher
).
Важно напомнить вам, что то, что действительно связывает publisher
и subscriber
, — это объект Subscription
, который содержит ссылку на subscriber
, чтобы поддерживать его в актуальном состоянии, и ссылку на объект, связанный с publisher
, чтобы уведомить об отправке нового выходного значения. .
Несмотря на то, что Subscription
не всегда упоминается, когда речь идет о Combine, я бы сказал, что это самая важная сущность, связанная с этой концепцией в реактивном программировании.
Поскольку протокол Subscription
наследуется от Cancellable
, по умолчанию в нем есть метод cancel
, отвечающий за отмену ссылки на абонента. Обычно он просто устанавливает свойство подписчика как nil
.
Еще одна важная вещь, о которой следует сказать, это то, что способ получения значений от издателя действительно зависит от пользовательского subscription
, но мы поговорим об этом подробнее при реализации одного из них позже.
3. Подписчик
Наконец, у нас есть Subscriber
, который является объектом, который поддерживается в актуальном состоянии с жизненным циклом publisher
и запрашивает спрос на эти значения через Subscription
. Это subscriber
, который действительно обрабатывает события от publisher
, и у него есть три метода:
Первый метод receive
отправляется самим Publisher
и содержит Subscription
в качестве параметра. Идея, стоящая за кулисами, заключается в том, что Subscriber
вызывает метод request
для Subscription
, чтобы сообщить ему, сколько значений он готов получить от Publisher
. Внутри реализации subscriber
может выполнять любое другое дополнительное действие.
Второй метод receive
предназначен для получения значений, поступающих из Publisher
, и соответствующей обработки. Как видите, подписке, вызвавшей ее, она возвращает тип Demand
, и она должна скорректировать количество значений, которое ей действительно требуется. Важно отметить, что он не полностью обновляет спрос, который хранится в Subscription
, а просто добавляет больше к существующему.
Последний метод receive
предназначен для получения события завершения от publisher
и обработки события завершения или, возможно, входящего сбоя.
Обратите внимание, что для того, чтобы этот процесс работал, общие типы Input
и Output
из Subscriber
и Publisher
должны быть такими же, как тот, который публикуется в том же, что и полученный.
Жизненный цикл подписки
Соблюдая эти протоколы, теперь мы в безопасности, чтобы понять, как устанавливается связь между publisher
и subscriber
:
- Новому подписчику требуется подписка от издателя, вызывая метод
receive
на издателе, передавая подписчика в качестве параметра. - Издатель создает пользовательскую подписку, объект, который отвечает за поддержание подписчика в актуальном состоянии с издателем, и отправляет его подписчику с помощью метода
receive(Subscription)
. - В методе
receive(Subscription)
подписчик вызывает методrequest
для только что полученного объектаSubscription
, устанавливая истинное требование, которое он требует от издателя. - При получении метода
request
объектSubscription
имеетDemand
, который требуется подписчику, и знает, сколько значений он должен получить от издателя. - Поскольку
Subscription
имеет некоторый механизм для отслеживания значений, выдаваемых издателем, ему просто нужно отправить их подписчику с помощью методаreceive(Input)
. - Когда какое-то событие требует завершения подписки,
Subscription
вызывает методreceive(Completion)
дляSubscriber
, и процесс завершается.
Создание собственного издателя, подписки и подписчика
Чтобы понять, как этот мощный фреймворк работает под капотом, мы собираемся реализовать публикатор для класса, который просто содержит целочисленное значение, и когда оно изменяется, мы сообщаем об этом его подписчикам для обработки. Хватит говорить, давайте начнем.
Это наш пользовательский класс HoldValue
, как вы видите, он предназначен только для хранения одного целочисленного значения, которое может измениться. Теперь нам нужен publisher
, чтобы отслеживать некоторые изменения значений экземпляра и обрабатывать их через subscriber
. Просто, не так ли? Это даже похоже на publisher
, сгенерированный оболочкой свойства Published
при объявлении некоторого класса ObservableObject
. Мы реализуем очень похожий случай.
Реализация нашего издателя
Нам нужен publisher
, чтобы получать значения, когда наш экземпляр HoldValue
изменяет свое внутреннее свойство, поэтому давайте создадим новый класс, реализующий протокол Combine Publisher
:
Теперь у нас есть новый класс Publisher
, определяющий два связанных типа, которые нам нужны: Int
как Output
, который наш издатель должен передать подписчику, и Never
как Failure
, поскольку наш класс никогда не завершится ошибкой. Как видите, мы также объявляем экземпляр HoldValue
как свойство в нашем publisher
. Нам нужен этот экземпляр, чтобы отслеживать его события для отправки на наш subscriber
.
Поскольку нам нужен какой-то механизм для прослушивания нашего объекта HoldValue
, и мы хотим, чтобы наш издатель был доступен для подключения нескольких подписчиков, мы собираемся определить набор дополнений, которые будут обновлять несколько слушателей:
Теперь у нас есть массив замыканий, которые будут иметь дело с изменением значения, каждое из которых позже будет соответствовать subscription
. Обратите внимание, что каждый раз, когда наше значение изменяется, наблюдатель перебирает все обработчики и выполняет их с нашим новым значением. Это будет очень важно в ближайшее время.
Создание нашей подписки
Как мы уже говорили ранее, Subscription
является наиболее важной сущностью для Combine, поскольку именно она поддерживает связь между нашим издателем и подписчиком.
Подписчик продолжает обновляться, пока Subscription
находится там в качестве посредника. Мы собираемся создать пользовательский тип Subscription
, который будет реализовывать некоторую логику для выполнения требований подписчика. А для этого у него должен быть способ прослушивания publisher
и отправки нового выходного значения в subscriber
, за который он отвечает, чтобы выполнить синхронизацию.
Это наш тип Subscription
для нашего HoldValuePublisher
. Он получает subscriber
, который будет отвечать за отправку значений, и экземпляр нашего класса HoldValue
, который он должен прослушивать.
Поскольку это тип Cancellable
, он реализует метод cancel
, который обрывает связь между подпиской и подписчиком (а затем и издателем). Он просто присваивает подписчику nil
.
Поскольку мы хотим отслеживать значение HoldValue
, сейчас нам нужно добавить к нему новое замыкание, которое будет обрабатывать наше изменение значения, как мы видели ранее:
Итак, наша логика работает следующим образом: мы устанавливаем нашу ссылку subscriber
внутри нашей subscription
, а также ссылку на HoldValue
. Затем мы добавляем новое замыкание к нашему экземпляру HoldValue
, которое будет отправлять новое значение подписчику через метод receive(Input)
. Легко, верно?
Теперь subscription
выступает посредником между нашими publisher
и subscriber
. Но это еще не все. Нам еще нужно показать, как издатель устанавливает subscription
, когда к нему присоединяется новый подписчик.
Помните тот метод receive(Subscriber)
в протоколе Publisher
? Что ж, это первый шаг в нашем конвейере объединения, и он фактически отвечает за создание экземпляра Subscription
и отправку в Subscriber
, тем самым создавая связь между двумя объектами:
Publisher
при получении нового subscriber
создает новый subscription
и отправляет subscriber
для обработки. Subscription
получает сам Subscriber
, а ссылка на класс Publisher
публикует свои значения (извините за избыточность!). Теперь пришло время для subscriber
запросить требование к subscription
.
Работа со спросом
Как мы видели ранее, у Subscription
есть метод request
, который получает Subscribers.Demand
, но мы еще не реализовали его. Этот метод отвечает за получение объекта Demand
, который внутренне содержит количество раз, когда наш подписчик действительно хочет получить значения, и с этим наша подписка создает некоторую логику для доставки именно того, что ожидает наш подписчик.
Прежде чем реализовать это, вы должны знать одну вещь о требованиях и жизненном цикле нашего издателя. Спрос полностью связан с подписчиком, в то время как жизненный цикл принадлежит издателю, а это означает, что если наш подписчик перестанет получать новые входные данные из-за отсутствия спроса, он не обязательно получит событие завершения от подписки. Мы увидим это дальше, а пока просто следуйте коду:
Добавьте два новых свойства в наш класс подписки: counter
, которое будет отслеживать, сколько значений уже получил наш подписчик, и maximum
, которое соответствует нашему требованию.
В нашем методе request
просто назначьте свойство max
из введенного запроса свойству maximum
из подписки HoldValue
.
Реализация логики спроса
Теперь, когда мы сохранили максимум, который хотим отправить на subscriber
, нам нужно реализовать некоторую логику внутри нашего subscription
, чтобы выполнить это требование. Внутри инициализатора добавьте новый обработчик к экземпляру HoldValue
.
Теперь мы реализуем магию: внутри нашего нового замыкания holdValue
проверяем, существует ли значение maximum
(не nil
), и если оно существует, проверяем, меньше ли наше counter
maximum
. Если это так, это означает, что наш данный подписчик все еще может получать новые входные данные, поэтому мы просто вызываем метод receive(Input)
с нашим новым целым числом из HoldValue
и, естественно, увеличиваем counter
для обновления нашей логики. Если counter
достигло нашего maximum
спроса, в этом случае мы отправляем событие завершения finished
нашему подписчику и отменяем subscription
.
Если у нас даже нет maximum
из-за спроса unlimited
, мы просто оставляем наших подписчиков получать значения на неопределенный срок. Обратите внимание, что мы инициируем событие завершения только потому, что больше нет значений для генерации, но в других издателях, таких как созданный с помощью оболочки свойства Published
, этого не происходит вообще.
Великолепно, теперь наш subscriber
готов принимать запросы и просто отправляет значения нашим подписчикам, когда это необходимо. До сих пор мы реализовали publisher
, который создает subscription
, отправляет его подписчику, а подписчик самостоятельно запрашивает новые значения в соответствии со своим запросом.
Реализация нашего подписчика
И последнее, но не менее важное: мы реализуем наш класс подписчика. Как мы говорили ранее, subscriber
отвечает только за обработку новых входных значений, событие завершения и запрос количества событий, которые он должен получить.
Давайте создадим новый пользовательский класс:
Что мы сделали?
Мы создали новый тип HoldValueSubscriber
, определяя связанные с ним значения как те же самые от издателя: Int
в качестве входных данных и Never
в качестве сбоя, поскольку он не ожидает никакой ошибки.
Давайте посмотрим на методы интерфейса:
.receive(Subscription)
: он вызывается со стороны издателя, как мы видели ранее, и отправляет новый запрос наSubscription
, передавая объект запроса. Поскольку ему нужны только три значения, мы устанавливаем максимум три в нашем запросе. В конце концов, чтобы сохранить нашу подписку, она сохраняется в набореcancellables
.receive(Input)
: Этот метод вызывается со стороны подписки и просто отправляет подписчику новое входное значение. Мы справляемся с этим, просто печатая значение. Возвращаемый спрос — это значение, которое увеличивает спрос в классе подписки. Поскольку мы не хотим, чтобы оно увеличивалось, мы возвращаемnone
, что совпадает сmax(0)
.receive(Completion)
: Этот метод также вызывается со стороны подписки и отправляет объект завершения, который уведомляет нашего подписчика о завершении подписки с завершениемfinished
вместоfailure
. Он просто печатает событие завершения
Сделать издателя доступным по его исходному классу
Мы создали издатель, который будет обслуживать класс HoldValue
. Это класс, который мы на самом деле хотим слушать.
Чтобы получить к нему доступ из исходного класса HoldValue
, как это делают большинство издателей Combine, реализуйте это расширение Swift:
Теперь, если вы хотите оформить подписку, просто получите доступ к publisher
из экземпляра HoldValue
.
Тестируем нашу подписку
Теперь, когда мы создали три наших пользовательских типа Combine, давайте протестируем эту подписку:
Что мы делаем? Мы создали новый экземпляр для HoldValue
, новый HoldValue
subscriber
и заставили его подписаться на наш экземпляр publisher
. Это устанавливает конвейер, который мы видели ранее: publisher
внутренне создает новый subscription
, отправляет его подписчику, подписчик запрашивает запрос на подписку, и теперь он готов получать новые входы (выходы от publisher
).
Теперь мы перебираем целые числа от 0 до 9 и обновляем наше значение holdValue
для каждого числа. Когда мы печатаем каждое из входных значений в нашем подписчике, взгляните на консоль:
Он получает три значения по требованию, после чего subscription
отправляет событие завершения в subscriber
, которое также печатается.
Заключение
Теперь вы полностью понимаете конвейер подписки Combine. Вы знаете, что наш publisher
отвечает за отправку новой подписки на subscriber
, когда она запрашивается, вы знаете, что наш subscriber
на самом деле определяет свой спрос, и что >subscription
реализует логику, чтобы subscriber
получал завершение.
Как мы уже говорили ранее, Apple не рекомендует нам внедрять издателей и подписки, поскольку логическая ошибка может поставить под угрозу все соединения между издателем и подписчиком, что может привести к неожиданным результатам.
Но очень важно понимать, как работает фреймворк Combine, и я очень надеюсь, что вам понравилась эта статья ;)