Pull to refresh

Реактивное программирование в Objective-C

Reading time 11 min
Views 6.5K
Original author: Dima Vorona
Со временем языки программирования постоянно изменяются и развиваются из-за появления новых технологий, современных требований или простого желания освежить стиль написания кода. Реактивное программирование можно реализовать с помощью различных фреймворков, таких как Reactive Cocoa. Он изменяет рамки императивного стиля языка Objective-C и у такого подхода к программированию есть что предложить стандартной парадигме. Это, безусловно, и привлекает внимание iOS разработчиков.

ReactiveCocoa привносит декларативный стиль в Objective-C. Что мы подразумеваем под этим? Традиционный императивный стиль, который используют такие языки как: C, С++, Objective-C, и Java и т. д. можно описать так: Вы пишете директивы для компьютерной программы, которые должны быть выполнены определенным способом. Другими словами, вы говорите «как сделать» что-то. В то время как декларативное программирование позволяет описать поток управления как последовательность действий, «что сделать», не определяя, «как делать».

ReactiveCocoa

Императивное vs Функциональное программирование


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

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

Вот основные различия языков:

1. Изменения состояния


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

Чтобы прояснить суть дела, у чистых функций есть следующие атрибуты:

  • единственный заметный вывод — возвращаемое значение
  • единственная зависимость входных параметров – аргументы
  • аргументы полностью определяются перед генерированием любого вывода

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

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

Как насчет ReactiveСocoa? Этот функциональный фреймворк для Objective-С, который является концептуально императивным языком, не включая явно чистые функции. При попытке избежать изменения состояния побочные эффекты не ограничиваются.

2. Объекты первого класса


В функциональном программировании есть объекты и функции, которые являются объектами первого класса. Что это значит? Это означает, что функции могут передаваться в качестве параметра, присваиваться переменной, возвращаться из функции. Почему это удобно? Это позволяет легко управлять блоками выполнения, создавать и объединять функции различными способами без затруднений, таких как указатели функции (char *(*(**foo[][8])())[]; — развлекайтесь!).

У языков, которые используют императивный подход, есть свои собственные особенности относительно выражений первого класса. Как насчет Objective-C? У него есть блоки в качестве реализаций замыкания. Функции высшего порядка (ФВП) могут быть смоделированы путем принятия блоков в качестве параметров. В этом случае, блок является замыканием, и функция высшего порядка может быть создана из определенного набора блоков.

Однако процесс манипулирования с ФВП в функциональных языках является более быстрым способом и требует меньше строк кода.

3. Управление основным потоком


Циклы в императивном стиле представлены как вызовы функции рекурсии в функциональном программировании. Итерация в функциональных языках обычно выполняется через рекурсию. Почему? Наверное, ради сложности. Для Objective-C разработчиков, циклы кажутся гораздо более благоприятными для программиста. Рекурсии могут вызвать трудности, например, чрезмерное потребление оперативной памяти.

Но! Мы можем написать функцию без использования циклов или рекурсий. Для каждого из бесконечно возможных специализированных действий, которые могут быть применены к каждому элементу коллекции, функциональное программирование использует многоразовые итеративные функции, такие как “map”, “fold”, “filter”. Эти функции полезны для реорганизации исходного кода. Они уменьшают дублирование и не требуют записи отдельной функции. (читайте дальше, у нас есть больше информации об этом!)

4. Порядок выполнения


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

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

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

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

5. Количество кода


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

Главные компоненты ReactiveCocoa


Функциональное программирование работает с понятиями, известными как future (представление переменной, доступное только для чтения) и promise (представление переменной, доступное только для чтения future). Что же хорошего в них? В императивном программировании Вы должны работать с уже существующими значениями, что приводит к необходимости синхронизации асинхронного кода и других трудностей. Но понятия futures и promises позволяют работать со значениями, которые еще не созданы (асинхронный код записан синхронным способом).

Signal

Сигнал


Future и promise представлены как сигналы в реактивном программировании. RACSignal — основной компонент ReactiveCocoa. Он дает возможность представить поток событий которые будут представлены в будущем. Вы подписываетесь на сигнал и получаете доступ к событиям, которые произойдут со временем. Сигнал — это push-driven поток и может представлять собой нажатие кнопки, асинхронные сетевые операции, таймеры, другие события UI или что-либо еще, что изменяется в течение долгого времени. Они могут связать результаты асинхронных операций и эффективно объединить многократные источники события.

Последовательность


Другим типом потока является последовательность. В отличие от сигнала, последовательность — это pull-driven поток. Это своего рода коллекция, которая имеет аналогичное назначение, что и NSArray. RACSequence позволяет определенным операциям выполняться, когда Вы в них нуждаетесь, а не последовательно, как с коллекцией NSArray. Значения в последовательности оцениваются только когда это указано по умолчанию. Использование только части последовательности потенциально улучшает производительность. RACSequence позволяет коллекциям Cocoa обрабатываться универсальным и декларативным способом. RAC добавляет метод -rac_sequence к большинству классов коллекции Cocoa, чтобы их можно было использовать в качестве RACSequences.

Команда


В ответ на определенные действия создается RACCcommand и подписывается на сигнал. Это применяется, прежде всего, к UI взаимодействиям. Категории UIKit, предусмотренных ReactiveCocoa для большинства средств управления UIKit, дают нам корректный способ обработки событий UI. Давайте представим, что мы должны зарегистрировать пользователя в ответ на нажатие кнопки. В этом случае команда может представлять сетевой запрос. Когда начинается выполнение процесса кнопка меняет свое состояние на «неактивно» и наоборот. Что еще? Мы можем передать активный сигнал в команде (Достижимость — хороший пример). Поэтому, если сервер будет недоступен (который является нашим “включенным сигналом”), то команда будет недоступна, и каждая команда сопоставленного элемента управления будет отражать это состояние.

Примеры основных операций


Вот некоторые схемы о том, как работают основные операции с RACSignals:

Слияние/Merge


+ (RACSignal *)merge:(id<NSFastEnumeration>)signals;

Merge

У потоков результата есть оба потока событий, объединенных вместе. Таким образом, "+ merge" является полезным, когда вы не заботитесь о конкретном источнике событий, но хотели бы обработать их в одном месте. В нашем примере stateLabel.text использует 3 различных сигнала: выполнение, завершение, ошибки.

RACCommand *loginCommand = [[RACCommand alloc] initWithSignalBlock:^RACSignal *(id input) {
	// let's login!
}];

RACSignal *executionSignal = [loginCommand.executionSignals map:^id(id value) {
	return @"Connecting..";
}];

RACSignal *completionSignal = [loginCommand.executionSignals flattenMap:^RACStream *(RACSignal *next) {
	return [[[next materialize] filter:^BOOL(RACEvent *event) {
		return event.eventType == RACEventTypeCompleted;
	}] map:^id(id value) {
		return @"Done";
	}];
}];

RACSignal *errorSignal = [loginCommand.errors map:^id(id value) {
	return @"Sorry :(";
}];

RAC(self.stateLabel, text) = [RACSignal merge:@[executionSignal, completionSignal, errorSignal]];

CombineLatest


+ (RACSignal *)combineLatest:(id<NSFastEnumeration>)signals reduce:(id (^)())reduceBlock;

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

CombineLatest

Когда мы можем использовать его? Давайте возьмем наш предыдущий пример и добавим больше логики к нему. Полезно включить кнопку входа в систему только в случае, когда пользователь ввел правильный email и пароль, верно? Мы можем объявить это правило следующим образом:

ACSignal *enabledSignal = [RACSignal combineLatest:@[self.emailField.rac_textSignal, self.passwordField.rac_textSignal]
 reduce:^id (NSString *email, NSString *password) {
	return @([email isValidEmail] && password.length > 3);
}];

* Теперь давайте немного изменим нашу команду входа в систему и подключим ее к фактическому loginButton

RACCommand *loginCommand = [[RACCommand alloc] initWithEnabled:enabledSignal signalBlock:^RACSignal *(id input) {
	// let's login!
}];

[self.loginButton setRac_command:loginCommand];

FlattenMap


- (RACSignal *)flattenMap:(RACStream * (^)(id value))block;

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

FlattenMap

Давайте представим, что Ваш запрос авторизаций в систему состоит из двух отдельных частей: получить данные от Facebook (идентификатор, и т.д.) и передать их на Backend. Одно из требований должно быть в состоянии отменить вход в систему. Поэтому клиентский код должен обработать состояние процесса входа в систему, чтобы иметь возможность отменить его. Это дает много шаблонного кода, особенно если Вы можете войти в систему из нескольких мест.

Как ReactiveCocoa помогает Вам? Это могло бы быть реализацией входа в систему:

- (RACSignal *)authorizeUsingFacebook {
	return [[[FBSession rac_openedSession] flattenMap:^RACStream *(FBSession *session) {
		return [session rac_fetchProfile];
	}] flattenMap:^RACStream *(NSDictionary *profile) {
		return [self authorizeUsingFacebookProfile:profile];
	}];
}

Legend:


+ [FBSession rac_openedSession] — сигнал, который приводит к открытию FBSession. Если необходимо, то это может привести к входу в Facebook.

— [FBSession rac_fetchProfile] — сигнал, который извлекает данные профиля через сессию, которая передается как self.

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

Фильтр/Filter


- (RACSignal *)filter:(BOOL (^)(id value))block;

В результате поток содержит значения потока “а”, отфильтрованное согласно заданной функции.

Filter

RACSequence *sequence = @[@"Some", @"example", @"of", @"sequence"].rac_sequence;
RACSequence *filteredSequence = [sequence filter:^BOOL(id value) {
	return [value hasPrefix:@"seq"];
}];

Map


- (RACSignal *)map:(id (^)(id value))block;

В отличие от FlattenMap, Map выполняется в синхронном режиме. Значение свойства “а” проходит через заданную функцию f (x + 1) и возвращает отображенное исходное значение.

Map

Допустим, нужно ввести на экран заголовок модели, применяя к ней некоторые атрибуты. Map вступает в игру, когда “Применение некоторых атрибутов” описано как отдельная функция:

RAC(self.titleLabel, text) = [RACObserve(model, title) map:^id(NSString *modelTitle) {
	NSDictionary *attributes = @{/*your custom font, paragraph style, etc*/};
	return [[NSAttributedString alloc] initWithString:modelTitle attributes:attributes];
}];

Как это работаем: объеденяет self.titleLabel.text с изменениями model.title, применив пользовательские атрибуты к нему.

Zip


+ (RACSignal *)zip:(id<NSFastEnumeration>)streams reduce:(id (^)())reduceBlock;

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

Zip

Для некоторых практических примеров, zip можно описать как dispatch_group_notify Например, у Вас есть 3 отдельных сигнала и необходимо объединить их ответы в единственной точке:

NSArray *signals = @[retrieveFacebookContactsSignal, retrieveAddressBookContactsSignal];
return [RACSignal zip:signals reduce:^id (NSArray *facebookContacts, NSArray *addressBookContacts){
	NSArray *mergedContacts = // let's merge them somehow ^_^
	return mergedContacts;
}];

Throttle


- (RACSignal *)throttle:(NSTimeInterval)interval;

С помощью таймера, установленного на определенный промежуток времени, первое значения потока “а” передается к потоку результата только по окончанию таймера. В случае, если новое значение производится в течение заданного временного интервала, он удерживает первое значение, не давая ему передаваться в поток результата. Вместо этого, в потоке результата появляется второе значение.

Throttle

Удивительный случай: нам нужно выполнить поиск по запросу, когда пользователь изменяет searchField. Стандартная задача, да? Впрочем, она не очень эффективна для построения и отправки сетевого запроса при каждом изменении текста, поскольку textField может генерировать много таких событий в секунду, и вы придете к неэффективному использованию сети.
Выход здесь заключается в том, чтобы добавить задержку, после которой мы на самом деле выполним сетевой запрос. Обычно это достигается добавлением NSTimer. С ReactiveCocoa это гораздо проще!

[[[seachField rac_textSignal] throttle:0.3] subscribeNext:^(NSString *text) {
	// perform network request
}];

*Важным замечанием здесь является то, что все «предыдущие» textField изменяются до того, как «последние» будут удалены.

Задержки/Delay


- (RACSignal *)delay:(NSTimeInterval)interval;

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

Delay


Как аналог -[RACSignal throttle:], delay только задержит отправку “следующих” и“завершенных” событий.

[[textField.rac_textSignal delay:0.3] subscribeNext:^(NSString *text) {
}];

Что нам нравится в Reactive Cocoa


  • Знакомит Cocoa Bindings с iOS
  • Возможность создавать операции по будущим данным. Вот немного теории о futures & promises от Scala.
  • Возможность представлять асинхронные операции синхронным способом. Reactive Cocoa упрощает асинхронное программное обеспечение, например сетевой код.
  • Удобная декомпозиция. Код, который связанный с пользовательскими событиями и изменениями состояния приложения, может стать очень сложным и запутанным. Reactive Cocoa делает модели зависимых операций особенно простыми. Когда мы представляем операции в виде объединенных потоков (например, обработка сетевых запросов, пользовательские события, и т.д.), мы можем достигнуть высокой модульности и свободной связи, что приводит к более частому использованию кода повторно.
  • Поведения и отношения между свойствами определены как декларативные.
  • Решает проблемы с синхронизацией — если Вы объединяете несколько сигналов, тогда есть одно единое место для обработки всех результатов (будь то следующее значение, сигнал завершения или ошибки)

С помощью фреймворка RAC Вы можете создавать и преобразовывать последовательности значений в лучший, более высокого уровня, способ. RAC позволяет проще управлять всем тем, что ожидает завершения асинхронной операции: отклика сети, изменения зависимого значения и последующей реакции. На первый взгляд с ним трудно иметь дело, но ReactiveCocoa заразительный!
Tags:
Hubs:
+7
Comments 7
Comments Comments 7

Articles