habrahabr

Пишите чистый код с Реактивными Расширениями (Reactive Extensions)

  • воскресенье, 9 ноября 2014 г. в 02:11:02
http://habrahabr.ru/post/242613/

Если у вас есть некий процесс, который может выполняться долго и возвращать несколько промежуточных результатов с течением времени, то Реактивные Расширения (.NET Framework Reactive Extensions) позволят вам упростить код и лучше управлять им.
чистый код с реактивными расширениями

В большинстве случаев вы просто вызываете метод и получаете результат на выходе. Но некоторые процессы устроены по-другому. Например, метод может выполняться в течение продолжительного промежутка времени. Или, что хуже, метод не только выполняется долго, а ещё и нерегулярно возвращает какие-то промежуточные результаты во время исполнения. Конечно, в том числе и для этого, в .NET Framework есть события, с помощью событий один объект может вызвать метод второго объекта, передавая некоторую информацию, в тот момент времени, когда это необходимо.

Но есть решение этой проблемы по-лучше, чем использования событий, — Реактивные Расширения. Если у вас есть процесс, работающий долго и время от времени возвращающий промежуточные результаты, то Реактивные Расширения помогут вам обрабатывать такие результаты всякий раз, когда они приходят. Код от использования Реактивных Расширений вместо событий не только становится проще, но вы ещё получаете более богатую функциональность (например, вы можете использовать LINQ для отсеивания любых ненужных данных).

В документации Реактивные Расширения описываются как способ обработки потока данных. При работе с Расширениями не трудно себе представить какой-либо процесс, который перебирает данные в коллекции, периодически ищет что-то интересное и отправляет в приложение то, что нашёл, — это вынуждает приложение реагировать (to react), отсюда и название «Реактивные» (reactive)). В нашем же примере мы будем считать, что мы хотим что-то сделать пока наше приложение выполняет ряд изменений в заказе на продажу. Для этого мы должны написать метод StatusChanged и вызывать его каждый раз, когда в приложении происходит изменение заказа. Либо мы можем добавить событие StatusChanged в наш класс SalesOrder и вызывать его при каждом изменении состояния — мы можем просто подключить наш код к этому событию.

Решение этой задачи с помощью Реактивных Расширений, на мой взгляд, не только проще, чем предложенные выше, но и — спасибо за их интеграцию с LINQ — более гибкое. Определённо, когда дело доходит до подключения или отключения источника информации, решение с использованием Реактивных Расширений более простое, чем аналогичный код с использованием событий.

Установка Реактивных Расширений

Для начала необходимо подключить Реактивные Расширения к проекту с помощью NuGet. Есть несколько пакетов Реактивных Расширений, включая реализацию для JavaScript, Android и обработку LINQ-запросов к веб-сервисам. Чтобы найти нужный пакет, достаточно запустить поиск по фразе «Reactive Extensions» и добавить главную библиотеку (Main Library package) к проекту.

На втором шаге требуется решить, какие данные мы хотим возвращать при каждом изменении заказа. Вместе с текущим состоянием заказа имеет смысл также возвращать и идентификатор заказа. Сделаем для этого класс с необходимыми свойствами:
public class StatusChange
{
	public int OrderId { get; set; }
	public string OrderStatus { get; set; }
}

На следующем шаге определим Субъект Реактивных Расширений (Reactive Extensions Subject), который будет работать с данным типом, проинициализируем его при запуске нашего приложения и подпишемся, с помощью метода Subscribe, на вызовы методов Субъекта, чтобы быть в курсе произошедших изменений:
ISubject<StatusChange> statChange = new Subject<StatusChange>();
statChange.Subscribe(sc => MessageBox.Show(sc.OrderStatus));

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

Теперь везде в нашем приложении когда мы меняем статус заказа мы будем создавать экземпляр класса StatusChange, заполнять его свойства и вызывать метод OnNext созданного Субъекта. Типичный код уведомления о первичном создании заказа может выглядеть так:
statChange.OnNext(new StatusChange() { OrderId = 1, OrderStatus = "New" });

При каждом вызове метода OnNext будет показано сообщение со значением свойства StatusChange.OrderStatus, что мы и определили в лямбда-выражении.

Расширение решения

Конечно, в реальном проекте обработка изменения статуса может потребовать более одной строчки кода, даже больше, чем мы захотим уместить в лямбда-выражении. Вместо лямбда-выражения мы всегда можем в метод Subscribe передать указатель на другой метод, например так:
statChange.Subscribe(StatusChanged);

Метод может не принимать никакие параметры, но если он принимает в качестве параметра объект того же типа, с которым связан Субъект, тогда ему будет передан объект, указанный при вызове OnNext. Такой метод, например, может просто выводить каждое новое состояние в консоль:
public static void StatusChanged(StatusChange status) {
	Console.WriteLine(status.OrderStatus);
}

Если нам необходимо выполнить несколько разных методов при изменении статуса, то нет необходимости упаковывать их в один метод. Вместо этого мы можем несколько раз вызвать Subscribe нашего Субъекта, передав необходимые методы по очереди.
statChange.Subscribe(StatusChanged);
statChange.Subscribe(StatusAudit);

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

Но применение Реактивных Расширений не только упрощает наш код, оно позволяет нам подняться над событиями. Для начала допустим, мы не хотим обрабатывать каждое изменение статуса заказа. Например, мы хотим отловить только те заказы, у которых состояние изменено на «В процессе». Мы можем использовать LINQ для уточнения, какие результаты мы хотим получать от Субъекта. Перед тем, как опробовать это, мы должны добавить namespace System.Reactive.LINQ в наш код.

После подключения этого пространства имён, мы увидим, что можем писать LINQ-выражения или использовать LINQ-методы-расширения для выбора, какие результаты мы хотим получать и обрабатывать. Все три примера ниже показывают, что наш метод будет вызван только для изменений со статусом «Processing»:
statChange.Where(c => c.OrderStatus == "Processing").Subscribe(ReportStatusChange);

var scs = from sc in statChange
		where sc.OrderStatus == "Processing"
		select sc;
scs.Subscribe(ReportStatusChange);

var sub = (from sc in statChange
			where sc.OrderStatus == "Processing"
			select sc).Subscribe(ReportStatusChange);

Также мы можем захотеть иметь специальные обработчики при изменении статуса на ошибочный или при совершении заказа. Мы вполне могли бы определить это по состоянию заказа, но Реактивные Расширения предоставляют решение лучше: методы Субъекта OnError и OnCompleted. Когда мы вызываем метод Субъекта Subscribe, мы можем передать параметрами указатели на методы (или лямбда-выражения), которые должны быть выполнены при вызове методов Субъекта OnError и OnCompleted. В примере ниже изменены имена методов, чтобы сделать код более наглядным:
statChange.Subscribe(OnNext, OnError, OnCompleted);

Метод OnError должен принимать в виде параметра исключение, а метод OnCompleted должен быть без параметров. Типичный пример может быть таким:
public static void OnError(Exception ex) {
	Console.Error.WriteLine(ex.Message);
}
public static void OnCompleted() {
	Console.WriteLine("order processing completed");
}

Теперь, в случае если что-то пошло не так, наше приложение должно вызвать метод Субъекта OnError. При вызове этого метода необходимо передать параметром исключение, которое содержит информацию о проблеме (в реальном проекте будет что-то по-лучше, чем пример ниже):
statChange.OnError(new Exception("Something has gone horribly wrong!"));

Когда приложение заканчивает работу с заказом, оно должно вызвать метод Субъекта OnCompleted. Дополнительно к вызову обработчиков этого изменения состояния, этот метод также инструктирует Субъект, что он не должен посылать больше никакие уведомления (это, кстати, ещё одна вещь, которую нельзя сделать с событиями — отключить подписчиков на стороне события). Также Субъект может освободить себя от всех слушателей вызовом метода Dispose.

Инкапсуляция уведомлений

В нашем приложении осталась одна проблема — в каждом месте, где мы изменяем статус заказа, мы должны не забыть вызвать метод OnNext Субъекта. Хорошо было бы это автоматизировать. В идеале мы можем спрятать этот вызов внутрь сеттера свойства Status класса заказа. Это исключит и дублирование кода и возможность забыть вызвать метод OnNext.

В листинге кода ниже класс SalesOrder содержит свойство типа ISubject, которое инициализируется в конструкторе экземпляром Субъекта. Теперь метод OnNext Субъекта будет вызван везде, где происходит изменение свойства Status (в этот же класс можно добавить дополнительный код, чтобы поддержать также вызов методов Субъекта OnError и OnCompleted)
public class SalesOrder
{
	string _status;
	public ISubject<StatusChange> StatChange { get; private set; }

	public int Id { get; set; }

	public string Status
	{
		get { return _status; }
		set
		{
			_status = value;
			var sc = new StatusChange() { OrderId = this.Id, OrderStatus = this.Status };
			StatChange.OnNext(sc);
		}
	}

	public SalesOrder()
	{
		StatChange = new Subject<StatusChange>();
	}
}