Туториал: Сравнение ReactiveCocoa и RxSwift

Если вы нашли опечатку в тексте, выделите ее и нажмите CTRL + ENTER.

Xcode: 
7.3
Swift: 
2.2

Functional Reactive Programming (Функциональное Реактивное Программирование) становится все более популярным среди Swift разработчиков. Оно упрощает сложный асинхронный код для написания и понимания.

В этой статье мы сравним две наиболее популярные библиотеки для Functional Reactive Programming: RxSwift и ReactiveCocoa.

Начнем с краткого обзора того, что же такое Functional Reactive Programming, а затем вы увидите детальное сравнение двух фреймворков. В заключение вы сможете выбрать тот фреймворк, который подходит вам больше!

Давайте реактивно приступать к работе!

Что такое Functional Reactive Programming?

Заметка

Примечание: Если вы уже знакомы с понятием Functional Reactive Programming, перейдите к следующему разделу "ReactiveCocoa против RxSwift".

Еще до того, как был анонсирован Swift, Functional Reactive Programming (FRP) стало необыкновенно популярным по сравнению с объектно-ориентированным программированием. Начиная от Haskell, Go и Javascript, вы найдете реализации на основе FRP. Почему? Что такого особенного в FRP? Возможно, самое главное то, как вы сможете использовать эту парадигму в Swift?

Functional Reactive Programming - это парадигма программирования, которая была создана Коналом Эллиотом. Определение программирования имеет очень специфическую семантику, и вы можете ее здесь изучить. Чтобы представить более простое/нестрогое определение, давайте  рассмотрим Functional Reactive Programming как комбинацию двух других понятий:

  1. Reactive Programming фокусируется на асинхронных потоках данных, исходя из которых вы можете реагировать соответствующим образом. Чтобы узнать больше, изучите следующее введение.
  2. Functional Programming, в котором подчеркиваются расчеты с помощью функций в математическом стиле, неизменность и выразительность, а также сводится к минимуму использование переменных и состояния. Подробнее можно узнать здесь.

Заметка

Примечание: Андре Стальц рассматривает разницу между первоначальной формулировкой FRP и практическим подходом в своей статье "Почему я не могу сказать FRP, но я только что это сделал" ( “Why I cannot say FRP but I just did”.)

Простой пример

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

Если бы вы программировали по - FRP:

  1. Вы бы создали объект, который будет эмитировать поток событий местоположения, на которые вы можете среагировать.
  2. Затем вы должны отфильтровать события, эмитированные местоположением, чтобы понять, какие из них находятся рядом кафе, и отправить оповещения для тех, что соответствуют.

Вот как может выглядеть соответствующий код в ReactiveCocoa:

locationProducer // 1
  .filter(ifLocationNearCoffeeShops) // 2
  .startWithNext {[weak self] location in // 3
    self?.alertUser(location)
}

Рассмотрим пошагово:

  1. locationProducer генерирует событие каждый раз при изменении местоположения. Обратите внимание, что в ReactiveCocoa это называется "сигнал", а в RxSwift это называется "последовательность".
  2. Затем вы можете использовать функциональные методы программирования, чтобы среагировать на обновления местоположения. Метод filter выполняет точно такую ​​же функцию, как если бы он имел дело с массивом, передавая каждое значение функции ifLocationNearCoffeeShops. Если функция возвращает true, событию разрешено перейти к следующему шагу.
  3. И, наконец, startWithNext формирует подписку под этот (отфильтрованный) сигнал, где код с выражением замыкания выполняется каждый раз, когда происходит событие.

Приведенный выше код очень похож на код, который вы могли бы использовать для преобразования массива значений. Но тут есть умная “фишка” ... этот код выполняется в асинхронном режиме, функция фильтра и замыкание с кодом вызываются «по требованию», в момент, когда происходят события c нашим location.

Синтаксис может, конечно, и странноватый, но основная цель этого кода, надеюсь, ясна. В этом красота функционального программирования и это естественное соответствие всей концепции значений со временем: оно декларативное. Оно показывает, что происходит, а не детали того, как это сделано.

Заметка

Примечание: Если вы хотите узнать больше о синтаксисе ReactiveCocoa, взгляните на пару примеров, созданных Руи Перес, на GitHub.

Трансформирующиеся события

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

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

Как и следовало ожидать, мы приходим к нашим уже известным (по Swift functional programming tutorial) map, filter, reduce, combine, и zip.

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

locationProducer
  .skipRepeats() // 1
  .filter(ifLocationNearCoffeeShops) 
  .map(toHumanReadableLocation) // 2
  .startWithNext {[weak self] readableLocation in
    self?.alertUser(readableLocation)
}

Давайте рассмотрим две новые добавленные строки:

  1. Первый шаг применяет операции skipRepeats к событиям, эмиттрированным сигналом locationProducer. Это операция (шаг) не имеет аналога в массивах, она специфична для ReactiveCocoa. Выполняемая функция очевидна: повторяющиеся события (основанные на равенстве) отфильтровываются.
  2. После того, как функция фильтрации выполнена, map используется для преобразования данных события от одного типа в другой, возможно, из CLLocation в String.

К этому моменту, вы должны начать видеть некоторые преимущества FRP:

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

ReactiveCocoa против RxSwift

Теперь, когда вы уже лучше понимаете, что из себя представляет FRP и как оно помогает лучше управлять вашим комплексом асинхронных потоков, давайте рассмотрим два самых популярных фреймворка FRP - ReactiveCocoa и RxSwift - и когда какой из них стоит выбирать.

Перед тем, как подробно начать рассматривать каждый, давайте пробежимся по истории каждого фреймворка.

ReactiveCocoa

Фреймворк ReactiveCocoa появился на GitHub. Работая в GitHub Mac client, разработчики сталкивались с проблемами в управлении потоками данных в своих приложениях. Вдохновившись Microsoft ReactiveExtensions, фреймворком FRP для C #, они создали свою собственную реализацию Objective-C.

Когда был анонсирован Swift, команда работала над релизом v3.0 на Objective-C. Поняв, что функциональная природа Swift хорошо дополняет ReactiveCocoa, они сразу начали работать над реализацией на Swift, которая стала версией v3.0. Синтаксис версии 3.0 глубоко функциональный, используя currying and pipe-forward.

Со Swift 2.0 пришло и протокольно-ориентированное программирование, что привело к существенному изменению ReactiveCocoa API для версии 4.0: отбрасыванию оператора pipe-forward оператора (|>) в пользу расширения протокола.

ReactiveCocoa является чрезвычайно популярной библиотекой и на момент написания этой статьи имеет уже более 13000 звезд на GitHub.

 

RxSwift

ReactiveExtensions Microsoft вдохновило множество других фреймворков и они привнесли концепции FRP в JavaScript, Java, Scala и многие другие языки. В конечном итоге это привело к образованию ReactiveX, группы, создавшей общий API для реализаций FRP, и это позволило авторам различных фреймворков работать вместе. В результате, разработчик, знакомый с RxScala Scala, должен относительно легко перейти к эквиваленту Java- RxJava.

RxSwift является относительно недавним дополнением ReactiveX, и из-за этого уступает в популярности ReactiveCocoa (около 4000 звезд на GitHub на момент написания статьи). Тем не менее, тот факт, что RxSwift является частью ReactiveX, несомненно, будет способствовать развитию его популярности и долгому использованию.

Интересно отметить, что у RxSwift и ReactiveCocoa есть общий предок в ReactiveExtensions!

RxSwift против ReactiveCocoa

Пришло время рассмотреть вопрос глубже. RxSwift и ReactiveCocoa по-разному обрабатывают несколько аспектов FRP, давайте взглянем на некоторые из них.

Горячие сигналы против Холодных сигналов (Hot vs. Cold Signals)

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

let requestFlow = networkRequest.flatMap(parseResponse)
 
requestFlow.startWithNext {[weak self] result in
  self?.showResult(result)
}

Сетевой запрос будет инициирован при подписке на сигнал (при использовании startWithNext). Эти сигналы называются cold (холодными), потому что, как вы могли догадаться, что они находятся в «замороженном» состоянии, пока вы на них не подпишитесь.

С другой стороны, hot (горячие) сигналы. Когда вы подписываетесь на один, то, возможно, он уже запущен, и это может быть уже третьем или четвертым событием. Это как например постукивать по клавиатуре, вместо того, чтобы начать на ней печатать. Постукивание, в данном случае, не означает “начать” печатать, как в случае с отправкой запросов на сервер.

Давайте повторим:

  • cold signal (Холодный сигнал) - это та часть работы, с которой вы начали, подписавшись на нее. Каждый новый подписчик начинает эту работу. Подписаться три раза на requestFlow - это значит сделать три сетевых запроса.
  • hot signal (Горячий сигнал) уже может отправлять события. Новые подписчики его не запускают. Обычно, взаимодействия UI - это горячие сигналы.

ReactiveCocoa предоставляет типы как для горячих, так и для холодных сигналов: Signal<T, E> и SignalProducer<T, E> соответственно. RxSwift, однако, имеет одиночный тип, который называется Observable<T>, который обслуживает оба типа сигналов.

Имеет ли значение, что необходимы различные типы для представления горячих и холодных сигналов?

Лично я считаю, что, знать семантику сигнала очень важно, так как он лучше описывает, как используется в определенном контексте. Когда мы имеем дело со сложными системами, это может иметь большое значение.

В независимости от того, есть ли различные типы, или их нет, представление о горячих и холодных сигналах является чрезвычайно важным. Как об этом сказал Андре Стальц:

"Если вы это проигнорируете, то оно (знание) вернется и жестоко вас укусит. Я вас предупредил.”

Если, работая с горячим сигналом, окажется, что это холодный сигнал, то у вас будут появляться побочные эффекты для каждого нового абонента. Это может иметь огромные последствия для вашего приложения. Типичный пример: три или четыре сущности в вашем приложении хотят наблюдать за сетевым запросом и для каждой новой подписки запускается отличный от других запрос.

+1 Балл за ReactiveCocoa!

Обработка ошибок

Прежде чем начать говорить об обработке ошибок, давайте кратко рассмотрим природу событий, которые отправляются в RxSwift и ReactiveCocoa. В обоих фреймворках есть три основных события:

  1. Next<T>: Это событие отправляется каждый раз, когда новое значение (типа Т) проталкивают в поток событий. В нашем примере T будет CLLocation.
  2. Completed: Указывает, что поток событий закончился. После этого события, Next<T> или Error<E> не посылаются.
  3. Error: Указывает на ошибку. В примере с запросом на сервер: это событие будет отправлено, если у вас была ошибка сервера. E представляет собой тип, который соответствует протоколу ErrorType. После этого события Next или Completed не посылаются.

Вы могли заметить, что в разделе о горячих и холодных сигналах, сигналы ReactiveCocoa Signal<T, E> и SignalProducer<T, E>  имеют по два параметризованных типа, в то время как RxSwift Observable<T> только один. Второй тип (Е) относится к типу, который соответствует протоколу ErrorType. В RxSwift этот тип опущен, и вместо этого рассматривается изнутри, как тип, который соответствует протоколу ErrorType.

Так что все это значит?

С практической точки зрения, это означает, что ошибки могут возникать (быть эмитированы) различными способами с RxSwift:

create { observer in
  observer.onError(NSError.init(domain: "NetworkServer", code: 1, userInfo: nil))
}

Код выше создает сигнал (или, в терминологии RxSwift обозримую последовательность) и сразу же выдает сообщение об ошибке.

Вот альтернатива:

create { observer in
  observer.onError(MyDomainSpecificError.NetworkServer)
}

Так как Observable только настаивает на том, что ошибка должна быть типа, соответствующего протоколу ErrorType, то вы можете отправлять по большому счету все, что хотите. Но иногда это может выглядеть неуклюже, вот так например:

enum MyDomanSpecificError: ErrorType {
  case NetworkServer
  case Parser
  case Persistence
}
 
func handleError(error: MyDomanSpecificError) {
  // Show alert with the error
}
 
observable.subscribeError {[weak self] error in
  self?.handleError(error)
 }

Этот код не будет работать, так как функция handleError ожидает MyDomainSpecificError, а не ErrorType. Вы вынуждены будете сделать две вещи:

  1. Попробуйте выбросить error в MyDomanSpecificError.
  2. Обработать те случаи, когда вы не можете привести error к типу MyDomanSpecificError.

Первый шаг легко исправить, добавив as?,  но со вторым будет справиться сложнее. Потенциальное решение - это введение Unknown кейса:

enum MyDomanSpecificError: ErrorType {
  case NetworkServer
  case Parser
  case Persistence
  case Unknown
}
 
observable.subscribeError {[weak self] error in
  self?.handleError(error as? MyDomanSpecificError ?? .Unknown)
}

В ReactiveCocoa, так как вы "исправили” тип при создании Signal<T, E> или SignalProducer<T, E>, компилятор будет жаловаться, если вы попытаетесь отправить что-нибудь другое. Итог: в ReactiveCocoa компилятор не позволит вам отправить другую ошибку, только ту, которую вы ожидаете.

Еще одно очко ReactiveCocoa!

UI Привязки

Стандартные интерфейсы iOS, такие как UIKit, не “говорят” на языке FRP. Для того, чтобы использовать RxSwift или ReactiveCocoa вы должны объединить эти API, например, преобразовав прикосновения к экрану (тапы) (кодируемые с использованием target-action) в сигналы или объекты наблюдения.

Представьте как много усилий все это требует! Поэтому и у ReactiveCocoa и у RxSwift есть в запасе “мосты” и “привязки”.

В ReactiveCocoa еще много “багажа” с времен Objective-C. Много работы уже проделано для возможности объединения со Swift. Сюда относятся UI привязки и другие операторы, которые не были переведены на Swift. Это, конечно, немного странно: вы имеете дело с типами, не являющимися частью Swift API (например RACSignal), что вынуждает пользователя преобразовывать типы Objective-C в типы Swift (например, с использованием метода toSignalProducer()).

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

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

С другой стороны, работать с привязками RxSwift - это просто радость! Мало того, что есть обширный каталог, но и тонна примеров,  наряду с полной документацией. Для некоторых людей, это уже достаточная причина для выбора RxSwift, а не ReactiveCocoa.

+1 балл за RxSwift!

Сообщество

ReactiveCocoa существует гораздо дольше, чем RxSwift. Многие смогут вам помочь в вашей работе, есть достаточное количество туториалов в Интернете, а также тег Reactive Cocoa на StackOverflow - хороший источник помощи.

У ReactiveCocoa есть группа в Slack, но она маленькая - всего 209 человек, и много вопросов остаются без ответа. Если мне нужны ответы срочно, я вынужден обращаться к членам PM ReactiveCocoa (PM ReactiveCocoa’s core members), и я думаю, что так поступают и другие. Тем не менее, вы, скорее всего, сможете найти туториал в Интернете, разбирающий вашу конкретную проблему.

RxSwift более новый, и это скорее “театр одного актера”. У него также есть группа в Slack, и она гораздо больше - 961 человек, в ней много тем для обсуждения. Вы всегда сможете найти кого-то, кто бы мог помочь вам с вашей проблемой.

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

 

Что же выбрать?

 

Как сказал Эш Фюрой в “ReactiveCocoa vs RxSwift”:

"Послушайте, если вы новичок, то это не имеет значения. Да, конечно, существуют технические различия, но они не являются значимыми для новичков. Попробуйте сначала один фреймворк, затем другой. Выберете тот, который вам просто по душе! После этого вы уже поймете, почему именно вы его предпочли ".

 

Я бы посоветовал делать то же самое. Тогда, когда у вас будет достаточно опыта, вы оцените тонкости между ними.

Тем не менее, если вам изначально нужно начать с чего-то одного и у вас нет времени на то, чтобы попробовать оба варианта, вот мой вам совет:

Выбирайте ReactiveCocoa, если:

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

Выбирайте RxSwift, если:

  • Если для вашего проекта вам важны UI привязки.
  • Если вы новичок в FRP и вам нужна постоянная поддержка.
  • Если вы уже знаете RxJS или RxJava. Так как они и RxSwift находятся под ReactiveX, и вы уже знаете один, то понимает остальных лишь вопрос синтаксиса.

 

Что дальше?

Если вы выберете RxSwift или ReactiveCocoa, вы не пожалеете об этом. Оба фреймворка очень хорошие, они помогут вам лучше описать вашу систему.

Важно также отметить, что как только вы изучите RxSwift или ReactiveCocoa, то перескакивание от одного к другому будет делом нескольких часов. Из моего опыта при переходе от ReactiveCocoa к RxSwift, наиболее сложная часть - обработка ошибок.

Следующие ссылки должны помочь вам в вашем в дальнейшей изучении Functional Reactive Programming, RxSwift и ReactiveCocoa:

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

Дальше, вы можете продолжить изучать наши туториалы по мере их появления, а также, параллельно читать перевод официальной книги по языку программирования Swift. И, для более подробного изучения языка, вы можете пройти наши курсы!

Урок подготовил: Акулов Иван

Источник урока: Источник