Obtics — функциональное реактивное программирование на .Net

Автор оригинала: Thomas van der Ploeg
  • Перевод
Данная статья — это перевод главной страницы проекта Obtics на Codeplex с небольшими изменениями.

Описание проекта


Цель этого проекта – это создание библиотеки, позволяющей применить принципы функционального реактивного программирования (ФРП) в .Net.

В ФРП ваши вычисления автоматически реагируют на изменение данных, используемых в них.

История


Идея проекта возникла во время работы над большим администрирующим приложением, использующим XAML для описания интерфейса. В этом приложении были обширные предметные модели (domain models) данных и отображения (view).

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

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

WPF binding не был хорошим вариантом для этой задачи поскольку:
  • Цели для binding обязательно должны являться свойствами класса, наследующего DependencyObject.
  • Binding не так уж хорошо работает с observablecollections.
  • Binding не может быть легко применён для более сложных преобразований. Они всегда будут требовать DependencyObject в качестве промежуточного шага.

Хотя я и полагал, что преобразования коллекций были действительно полезными, идея почти сразу провалилась по следующим причинам:
  1. Их использование требовало дополнительных слов в коде, что усложняло его восприятие людьми, не знакомыми с этой технологией. И хотя код типа «register-event-handler-update» был на самом деле более сложным, для моих коллег он оставался более удобным для чтения.
  2. Объекты работали полностью асинхронно, и каждый имел свою собственную очередь сообщений и буфер, что делало их весьма «тяжелыми». Благодаря их успеху они часто применялись в проекте и поэтому создавали значительную нагрузку на приложение.
  3. Отладка pipeline-преобразований в классическом отладчике становится очень сложной. Уже не так легко проследить процесс обновления шаг за шагом.

Этот опыт заставил меня уточнить некоторые дополнительные требования к следующей версии объектов преобразования:
  • Их использование должно быть простым (должен использоваться LINQ для трансформации и статические методы).
  • Они должны быть как можно более легкими.
  • Использование объектов должно происходить без воздействия на остальной код и без препятствования работе существующих библиотек (расширяя LINQ, а не заменяя его).
  • Для отладки я рекомендую использовать другие методы, такие как Unit Testing. Идея заключается в том, чтобы доверить процесс преобразования библиотеке, т.к. я прихожу к выводу, что этот процесс не может быть нормально отображён трассировщиком.

Возможности


Obtics позволяет создавать реактивные и обозреваемые (observable) объекты, освобождая вас от надобности обновления результата и слежением за тем, в какой момент необходимо обновить результат в динамическом приложении. Получив возможность меньше беспокоится об этом, вы сможете быстрее создавать богатые и более надежные приложения.

Меньше абстрактности


Простой кусок кода, как тот что ниже, будет производить статический результат используя стандартный Object LINQ. Если вы добавите привязку (bind) на свойство PeopleNames из XAML, то вы получите «одноразовый» результат, несмотря на то, как вы изменяете коллекцию _People. Используя Obtics, результат PeopleNames будет полностью реактивным и обозреваемым. Это значит:
  1. При изменении коллекции _People или свойства LastName отдельного объекта People, значение PeopleNames будет автоматически обновлено (реактивность).
  2. Ваше привязанное (bound) XAMLприложение будет отображать изменения в PeopleNames автоматически (обозреваемость).
public class Test
{
    ObservableCollection<Person> _People;

    public IEnumerable<string> PeoplesNames_Static
    {
      get
      {
        return
          from p in _People
          orderby p.LastName
          select p.LastName ;
      }
    }

    public IEnumerable<string> PeoplesNames
    {
      get
      {
        return
          ExpressionObserver.Execute(
            this,
            t =>
              from p in t._People
              orderby p.LastName
              select p.LastName
          ).Cascade();
      }
    }
}

* This source code was highlighted with Source Code Highlighter.

С Obtics вы можете написать код, приведенный ниже и сделать байндинг на значение Value свойства FullName. Любые изменения свойств LastName или FirstName будут автоматически и немедленно отображены в приложении.
public class Test
{
    Person _Person;

    public IValueProvider<string> FullName
    { get { return ExpressionObserver.Execute(this, t => t._Person.LastName + ", " + t._Person.FirstName); } }
}

* This source code was highlighted with Source Code Highlighter.

(Класс Person из этих примеров – это обозреваемый (observable) класс. Это означает, что экземпляр этого класса отправляет уведомления об изменения каждый раз, когда изменяется значение свойства. Подробнее здесь.)

Типы преобразований


Неявные преобразования значения.


Используемые методы образуют класс Obtics.Values.ExpressionObserver. Просто напишите лямбда функцию, возвращающую желаемый результат, ExpressionObserverпроанализирует её, извлечёт все зависимости и создаст выражение (expression), которое будет полностью реактивным при изменении любого обозреваемого значения, от которого это выражение зависит. Это автоматически перепишет стандартный Object LINQ в реактивную и обозреваемую форму, что является очень удобным путём для создания bindableobject LINQ queries.
using Obtics.Values;

class Test
{
    Person _Person = new Person("Glenn","Miller");

    Person Person
    { get { return _Person; } }

    public IValueProvider<int> PersonFullNameLength
    {
      get
      {
        return
          ExpressionObserver.Execute(this, t => t.Person.FirstName.Length + t.Person.LastName.Length + 1);

        //the below line is even simpler but because the lambda expression depends on the external 'this' variable the
        //expression is re-compiled for every Test class instance. Better use lambda's without external variables.
        //return ExpressionObserver.Execute(() => Person.FirstName.Length + Person.LastName.Length + 1);
      }
    }
}

* This source code was highlighted with Source Code Highlighter.

Явные преобразования значений.


Используются методы из класса Obtics.Values.ValueProvider. Вы можете построить собственный transformation pipeline, собственноручно указывая методы, что позволит легко контролировать процесс преобразований. Вы можете указывать именно те изменяемые значения, которые соответствуют вашим вычислениям, благодаря чему вы можете предотвратить расходы ресурсов на неизменяемые или не обозреваемые зависимости. Этот подход может быть полезен при работе с большим количеством данных.

using Obtics.Values;

class Test
{
    Person _Person = new Person("Glenn","Miller");

    Person Person
    { get { return _Person; } }

    public IValueProvider<int> PersonFirstNameLength
    {
      get
      {
        return
          //select the never changing Person property make a (static) IValueProvider for it.
          ValueProvider.Static(Person)
            //select the FirstName property of Person. This is a classic
            //property and observably mutable
            .Property<Person, string>(Person.FirstNamePropertyName)
            //Calculate the result
            .Select(
              //fn is a string and Length is an
              //immutable property of string
              fn => fn.Length
            );
      }
    }
}

* This source code was highlighted with Source Code Highlighter.

Collection transformations (LINQ).


Используются методы из Obtics.Collections.ObservableEnumerable. Как и в явных преобразованиях, этот подход позволяет указывать, как именно будет реагировать ваша коллекция. Это усложнит использование библиотеки, но вы сможете вручную описать трансформации и быть уведомлённым о зависимостях, которые нужно отслеживать на наличие изменений. Что позволяет вам предотвратить расходование ресурсов на зависимости, которые никогда не будут изменены. Этот стиль может быть полезен при работе с большими коллекциями для предотвращения лишних ресурсных расходов. Чтобы получить полное преимущество использования этого метода, необходимо в коде заменить «using System.Linq;» на «usingObtics.Collections;» иначе будет возникать много ошибок.
using SL = System.Linq;
using Obtics.Values;
using Obtics.Collections;

class Test
{
    ObservableCollection<Person> _People = new ObservableCollection<Person>();

    public ReadOnlyObservableCollection<Person> People
    { get { return new ReadOnlyObservableCollection<Person>(_People); } }

    public IEnumerable<string> LastNames
    {
      get
      {
        return
          from p in People
          select ValueProvider.Static(p).Property<Person, string>("LastName") into ln
          orderby ln
          select ln;
      }
    }
}

* This source code was highlighted with Source Code Highlighter.


ExpressionObserver (неявные преобразования) является расширяемым. Была создана библиотека (ObticsToXml), которая построена на его расширении и позволяет создавать полностью динамичные Linq toXML выражения.

Obticsпредлагает полностью обозреваемую поддержку для всего ObjectLinq и большей части Linq to XML.

Больше примеров можно посмотреть по этим ссылкам: Transformation Examples, ObticsExaml и ObticsRaytracer.

Проект ObticsWpfHelper содержит решение, основанное на частичном комбинировании Obtics и WPF.

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

Будущие возможности


Версия 2.0 уже на подходе. Планы на будущие версии:
  • Двунаправленные операции (two way operation). Все трансформации будут поддерживать как изменения от источника к клиенту, так и наоборот. Разработчики смогут указывать обратный путь, что позволит применять автоматические изменения от клиента к источнику.
  • Легкая версия для веба (Silverlight) и мобильных устройств. Куча кода сосредоточена на распространении изменений от источника к клиенту. Есть возможность создать версию, которая не будет использовать уведомления об изменении коллекции, но сможет уведомлять об изменении свойств. Что должно хорошо выглядеть при работе с небольшими коллекциями.
  • Obtics должен легко поддерживаться F#.

Сайт проекта: http://obtics.codeplex.com/
Поделиться публикацией

Комментарии 4

    +2
    Вы простите, но у нас тут в офлайне целый консилиум собрался чтобы понять что автор имел ввиду. Короче, может быть и круто, но вообще ничего не понятно. Идея класная.
      +1
      На самом деле всё не так сложно.
      Суть такая:
      Пусть есть
      class Автомобиль {

      public property int Пробег {
      return Маршрут.Sum(m=>m.Расстояние);
      }

      public property List<Путь> Маршрут;

      void Проехать(Путь п) {
      Маршрут.Add(путь);
      }
      }

      1. Вычисления в properties заменяются на ValueProvider, который инкапсулируют эти вычисления.
      public property ValueProvider<int> Пробег{
      ____ return new ValueProvider(() => Маршрут.Sum(m=>m.Расстояние));
      }
      public property ValueProvider<List<Путь>> Маршрут;
      Таким образом Пробег и Маршрут всё еще отвечает за вычисление пробега, но при попытке получить Пробег, мы получим не резултат этих вычислений, а объект, содержащий функцию. Т.о. мы не даем программе сразу же сделать вычисления, а просто возвращаем ValueProvider, который знает как их вычислить. А значит теперь мы можем контролировать в какой момент вычислять, а в какой просто возвращать закэшированное значение.

      т.е. Пробег зависит от Маршрута, а значит не нужно перерасчитывать значение Пробега, если элементы Маршрута не изменялись.

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

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

      3. Этот ValueProvider, кроме умного подхода к вычислениям, к тому же наследуется от IObservable, а значит если сделать привязку на это свойство (Пробег или Маршрут) в XAML, то привязанный конторол будет автоматически обновляться при изменении какого либо значения, которое используется для вычисления этого свойства.

      т.е. если мы добавим новый Путь в Маршрут, то автоматически обновятся контролы зависящие от Маршрута (в т.ч. и контрол, который будет показывать общий пробег).
        +1
        Представьте, что кроме пробега есть десятки(сотни) свойств, которые зависят от Маршрута.

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

        Нужные свойства обновятся
        1. сами,
        2. сразу же,
        3. и только тогда, когда это действительно нужно.
          0
          мы то уже разобрались… это я намекал что далеко не все будут тратить такое количество времени чтобы разобраться. Я бы вам советовал ваши ответы перенести в статью, плюс перевести Why? секцию с сайта проекта.

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