10/10/2010 11:00:28 AM

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

Итак, Rx – это библиотека для .NET Framework, полное название которой звучит как “Reactive Extensions”. Reactive не потому, что “круто, как реактивный самолёт”, а потому, что…

Существует два варианта взаимодействия со средой: проактивный (интерактивный) и реактивный. Мы знакомы с обоими:

  • Проактивный – это когда мы предпринимаем какое-то действие над средой для получения результата (мы задаём кому-то вопрос, мы наливаем чай в кружку, мы вызываем метод GetNewPosts(lastDate)). Мы активно производим какие-то действия, чтобы получить от среды какой-то результат (pull-взаимодействие, мы “вытягиваем” что-то из среды).
  • Реактивный – это когда мы реагируем на какие-то события, происходящие вокруг (кто-то звонит нам по телефону и мы отвечаем на звонок, официант приносит нам чай и мы отвлекаемся от созерцания океана чтобы его поглотить, срабатывает обработчик OnMouseClick). То есть, в среде постоянно происходят какие-то события, на которые мы можем (а иногда и должны) реагировать. Среда сама предоставляет нам информацию о произошедшем событии (push-взаимодействие).

Нетрудно догадаться, что Rx посвящена этому самому реактивному (push) взаимодейтсвию.

Создатели Rx обратили внимание на интерфейсы IEnumerable и IEnumerator, заметив, что эти два интерфейса олицетворяют собою проактивный (интерактивный) подход.

А лучше рисовать я и не умею!

Действительно, во что вытекает работа с IEnumerable/IEnumerator (и всеми их “наследниками”)?

   1:  IEnumerable<object> enumerable;              //где-то есть у нас такое
   2:   
   3:  var enumerator = enumerable.GetEnumerator(); //запрашиваем enumerator
   4:  ...
   5:  var moved = enumerator.MoveNext();           //передвигаем "курсор"
   6:  var element = enumerator.Current;            //запрашиваем текущий элемент
   7:  ...
   8:  enumerator.Dispose();                        //"убиваем" enumerator

Да, какой-нибудь foreach делает за нас всю эту работу, от получения IEnumerator и до его dispose, но суть в том, что мы всё время “дёргаем” что-то, чтобы получить результат: проактивное взаимодействие.

Дальше создатели Rx подумали: поскольку реактивное взаимодействие является как бы “зеркальной копией” проактивного, что если сделать зеркальные копии IEnumerable/IEnumerator?
IEnumerator позволяет нам передвигать курсор, изменяя текущее значение? Тогда среда должна иметь возможность “уведомить” его “зеркальную копию” о том, что текущее значение изменилось (где-то кем-то).
IEnumerable позволяет нам получить экземпляра IEnumerator? Тогда его “зеркальная копия” должна дать нам возможность предоставить среде “зеркальную копию” IEnumerator’а, как бы заявив: “вот та сущность, которая будет реагировать на данный аспект.

Если присмотреться, то видно: все эти “зеркальные копии” представляют собой шаблон “Observer”, который описан GoF миллионы лет наза.
“Зеркальной копией” IEnumerator может являться нечто с именем IObserver, а “зеркальную копию” IEnumerable тогда назовём IObservable.

public interface IEnumerator<T> : IDisposable
{
    T Current { get; }
    bool MoveNext();
    void Reset();
}
public interface IObserver<T>
{
    void OnCompleted();
    void OnError(Exception exception);
    void OnNext(T value);
}

Рассмотрим, что представляет собой это “зеркалирование”:

Вызывая IEnumerable.MoveNext() мы перемещаем курсор, после чего получаем текущее значение из IEnumerable.Current.
Значит, IObserver должен иметь метод IObserver.OnNext(currentValue), позволяющий нам реагировать на изменение значения (получить это изменённое значение).

Кроме того, когда мы “делаем” IEnumerable.MoveNext(), метод возвращает нам результат этой операции: true, если курсор переместился и false, если мы дошли до конца.
Значит, среда должна иметь возможность уведомить IObserver о том, больше “обозревать” нечего. Таким образом нам понадобится метод IObserver.OnCompleted().

Ещё один аспект при работе с IEnumerable.MoveNext() состоит в том, что если в процессе перемещения курсора возникает исключение, то код, вызывающий MoveNext(), его тут же и получит. Что, естественно, невозможно в случае IObserver, так как в этом случае мы не контроллируем среду. Если при вычислении нового значения где-то в среде произойдёт исключение, у среды не будет возможности вызвать IObserver.OnNext(currentValue) просто потому, что этот самый currentValue не был вычислен.
Поэтому для уведомления об ошибках используется метод IObserver.OnError(exception).

Глядя на всё это лично я считаю, что интерфейс IObserver даже более “прозрачен” и понятен, чем IEnumerator. Вон, сколько всего скрыто за одним только MoveNext(), в то время, как IObservable имеет все эти аспекты разложенными по полочкам.

public interface IEnumerable<T>
{
    IEnumerator<T> GetEnumerator();
}
public interface IObservable<T>
{
    IDisposable Subscribe(IObserver<T> observer);
}

Как уже говорилось, IObservable позволяет нам предоставить среде некую реализацию IObserver, которая будет реагировать на уведомления среды. Делается это, как видно, с помощью метода IObservable.Subscribe(observer).

Тут необходимо отметить важный момент: IObservable содержит метод Subscribe(observer) но не содержит метода Unsubscribe(observer). Означает ли это, что единожды подписавшись, мы обречены на эту подписку навсегда? Конечно же нет.
IObserver не содержит метода Unsubscribe по ряду простых причин, объединённых одним корнем: а не удобно это ни фига! Пришлось бы “таскать с собой”, либо хранить где-то тот самый observer, чтобы потом иметь возможность передать его в метод Unsubscribe.

Вместо этого IObservable.Subscribe(observer) возвращает нечто, реализующее IDisposable. Это а) является “зеркальным отражением” того, что IEnumerable наследует IDisposable, и б) вызов Dispose как раз и означает “нам больше не нужна эта подписка, отписываемся!”.

Забегая вперёд скажу, библиотека Rx автоматически “отписывает” observer “когда это нужно”: после OnCompleted и OnError, поэтому явное отписывание нужно достаточно редко, но всё же нужно, мы увидим это дальше.

Собственно, идея реализации шаблона Observer методом “зеркального отражения” шаблона Enumerator настолько же проста, насколько и гениальна. Наверное поэтому Эрик Мейер (человек, который когда-то придумал LINQ и который теперь придумал Rx) сказал: “когда я смотрю на это, я понимаю, что мне можно уходить на пенсию, потому, что ничего столько же красивого я в своей жизни придумать уже не смогу” Smile

Итак, понятно, что существует два интерфейса IOvservable<T>/IObserver<T> (кстати, в .NET 4.0 эти интерфейсы уже включены в BCL), но что же всё-таки позволяет нам делать библиотека Rx?

О, много чего.
Для начала, Rx содержит множество возможностей получения IObservable из различных источников.

Вот так, например, можно получить IObservable из события:

var observable = Observable.FromEvent<MySpecialEventArgs>(myObject, "MyEventName");

Использование имени события - не единственная возможность, Observable содержит несколько перегрузок FromEvent(..).

А дальше.. Rx реализует великое множетво методов для работы с IObservable. Rx реализует LINQ для IObservable!

Вот пример кода, который возвращает изменения координат объекта при перетаскивании его с помощью мыши:

   1:  public static IObservable<Point> GetMouseDrag(this UIElement uiElement)
   2:  {
   3:      return GetMouseMove(uiElement)
   4:                  .SkipUntil(GetMouseLeftButtonDown(uiElement).Do(o => uiElement.CaptureMouse()))
   5:                  .TakeUntil(GetMouseLeftButtonUp(uiElement).Do(o => uiElement.ReleaseMouseCapture()))
   6:                  .Let(mm => mm.Zip(mm.Skip(1), (prev, cur) =>
   7:                      new Point
   8:                      {
   9:                          X = cur.EventArgs.GetPosition(uiElement).X - prev.EventArgs.GetPosition(uiElement).X,
  10:                          Y = cur.EventArgs.GetPosition(uiElement).Y - prev.EventArgs.GetPosition(uiElement).Y
  11:                      }))
  12:                  .Repeat();
  13:  }
  14:   
  15:  public static IObservable<IEvent<MouseEventArgs>> GetMouseMove(this UIElement uiElement)
  16:  {
  17:      return Observable.FromEvent<MouseEventArgs>(uiElement, "MouseMove");
  18:  }
  19:   
  20:  public static IObservable<IEvent<MouseButtonEventArgs>> GetMouseLeftButtonDown(this UIElement uiElement)
  21:  {
  22:      return Observable.FromEvent<MouseButtonEventArgs>(uiElement, "MouseLeftButtonDown");
  23:  }
  24:   
  25:  public static IObservable<IEvent<MouseButtonEventArgs>> GetMouseLeftButtonUp(this UIElement uiElement)
  26:  {
  27:      return Observable.FromEvent<MouseButtonEventArgs>(uiElement, "MouseLeftButtonUp");
  28:  }

Код собственно логики здесь заключён в методе GetMouseDrag(..), который возвращает наружу IObservable<Point>.
Эта конструкция начинает предоставлять данные как только пользователь нажмёт левую кнопку мыши (SkipUntil просто игнорирует все перемещения до этого момента) и завершится тогда, когда пользователь её отпустит (за это отвечает TakeUntil).
Инструкция “Repeat()” в конце означает, что при получении OnCompleted() нужно снова подписаться на то же самое ещё раз.
Если мы подпишемся на этот IObservable, то мы просто будем получать значения “объект сместился на столько-то пикселей” тогда, когда происходит собственно перетаскивание:

var dragObservable = GetMouseDrag(myImage);
dragObservable.Subscribe(delta => { Console.WriteLine("dX: {0}, dY: {1}", delta.X, delta.Y); });

Поскольку эта конструкция (с помощью инструкции Repeat()) каждый раз “самоподписывается” заново, мы можем осуществлять такое перетаскивание неоднократно и всегда получ��ть изменения координат. Если же нам нужно в какой-то момент перестать это делать, то, как я уже говорил, нужно просто вызвать Dispose у объекта, который возвращает .Subscribe(..), либо просто заключить его в using.

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

P.S. Скачать Rx для .NET 3.5, .NET 4.0, Silverlight 3, Silverlight 4 и даже для JavaScript (!) можно на домашней страничке проекта: http://msdn.microsoft.com/en-us/devlabs/ee794896.aspx

Comments (2) -

10/10/2010 11:10:31 AM

Murtuz Chalabov

Как раз Rx для JavaScript сейчас рассматриваю Smile

Murtuz Chalabov Russia

10/10/2010 4:17:27 PM

Alexey Raga

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

Беда только в том, что официально мне времени на это не дали и всё то, что я делаю, делается только по моей инициативе при моральной поддержке Мэтта Smile

Alexey Raga Australia

Comments are closed

Powered by BlogEngine.NET 2.5.0.6

About the author

Alexey Raga Alexey Raga
.NET software developer.

E-mail me Send mail

Twitter

Widget Twitter not found.

Root element is missing.X


Recent posts

Archive

Disclaimer

The opinions expressed herein are my own personal opinions and do not represent my employer's view in anyway.

© Copyright 2012

Sign in