image

EDA – event driven architecture или событийно-ориентированная архитектура. Довольно известный подход к проектированию веб-приложений, который сильно облегчает разработку, когда связанные компоненты находятся на разных ветвях иерархии, делая их связь более прозрачной.



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

Про EDA


Как нам говорит википедия, событие в EDA можно определить как «существенное изменение состояния». Известно, что компоненты в большинстве Фреймворков, находящиеся в прямом родстве, спокойно могут наследовать и передавать состояние, определяющее их поведение. Тем более с этим не возникает проблем у Angular, который реализует всеми нелюбимый two-way binding. Но все посылы команды разработчиков говорят о том, что это не хорошо и нужно привыкать к мысли, что компоненты — это чистые функции и возвращение состояния от дочерних компонентов оправдано лишь в крайних случаях.

Про Angular


Я некоторое время занимаюсь разработкой на Angular и использование EDA подхода сильно помогает в создании приложений. Ниже я бы хотел поделиться простой реализацией основных возможностей, описанной выше архитектуры, на примере Angular 1.x

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

Листинг нашего сервиса.
'use strict';

  function EventService($rootScope) {

    /* ----- api нашего сервиса ------*/ 
    this.on = on;
    this.broadcast = invoke;

    /* ----- функции для внутреннего использования --------*/ 
    function _on(scope, event, func) {
      /* ------- создание watcher для scope необходимого компонента ------*/
      var off = scope.$on(event, func);

      /* -------- подчищаем за собой -------*/      
      scope.$on("$destroy", off);
    };

    function _broadcast(event, params) {
      if (!params) params = [];

      params = angular.copy(params);

      /*------ каррируем исходные параметры ------*/
      params = [event].concat(params);

      $rootScope.$broadcast.apply($rootScope, params);
    };

    /* ------- используя замыкания, создаем методы генерирования и отлова событий -------*/
    function on(handler) {
      return function(scope, func) {
        _on(scope, handler, func);
      };
    };

    function invoke(handler) {
      return function() {
        _broadcast(handler, arguments);
      };
    };
  };

  EventService.$inject = ['$rootScope'];

Некоторые высказываются, что Angular не приспособлен для реализации подобного рода вещей, мол циклы дайджеста съедают все рабочее время и если необходимо реализовать большое количество возможных событий, то количество watchers, которое понадобится для этого, будет избыточным. Но это все в прошлом и начиная с версии 1.4.х магическое число 2000 давно перевалило за 100к.

Далее, имея такой сервис, можно легко создавать собственный event-manager для отдельно взятого компонента либо группы компонентов.

'use strict';

  function EventManager(EventService, EVENTS) {
    /* ----- набор событий определенного компонента -----*/
    var handlers = {
      onScroll: EVENTS.scroll
    };

    /* ------ установка генератора и листенера событий ------*/
    this.onScroll = EventService.on(handlers.onScroll);
    this.scroll = EventService.broadcast(handlers.onScroll);
  };

  /* ------ централизованное или распределенное хранение событий -------*/
  EventManager.$inject = ['EventService', 'EVENTS'];

И собственно примеры использования данного event-manager'a

'use strict';

  function ComponentOne(EventManager) {
    /*----- some code before -----*/
    EventManager.scroll();
    /*----- some code after -----*/
  };
  ComponentOne.$inject = ['EventManager'];

  function ComponentTwo($scope, EventManager) {
    /*----- some code before -----*/
    EventManager.onScroll($scope, cb);

    function cb(event, params) {
       /* ---- some code -----*/
    };
    /*----- some code after -----*/
  };
  ComponentTwo.$inject = ['$scope', 'EventManager'];


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

Спасибо за внимание.

Комментарии (10)



  1. Demetros
    05.03.2016 07:23

    Использование $rootScope.$broadcast — не лучший пример, такое событие будет спускаться по иерархии скоупов и возникать на каждом.
    Ну и конечно неизбежно возникают гонки, когда событие сгенерировано, а получатель еще не подписался на него.


    1. silentvick
      06.03.2016 15:56

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

      Scope.$broadcast() Is Surprisingly Efficient In AngularJS


      1. enepomnyaschih
        07.03.2016 10:19

        Оптимизация $broadcast ценой тормознутого $on и отписки. Окупится только если событие бросается из scopes, приближенных к корневому. Ближе к веткам порождает больше накладных расходов, чем приносит пользы. Отличный пример того, как Angular скрывает проблему архитектуры, не решая ее.

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


  1. extempl
    05.03.2016 09:03
    +6

    Довольно известный подход к проектированию веб-приложений, который сильно облегчает разработку, когда связанные компоненты находятся на разных ветвях иерархии, делая их связь более прозрачной.
    Облегчает? Да. Прозрачной? Серьёзно? Да код превращается в макароны, где не видно, что откуда растёт и где заканчивается. А если продебажить? Попробуйте. А лучше в проекте, который вы видите впервые.


    1. extempl
      05.03.2016 09:09
      +1

      Хотя, справедливости ради, стоит отметить, что при правильном подходе именования ивентов с использованием NS, вполне можно проследить код. Не дебаггером, конечно, но глобальным поиском по коду можно.


  1. lalaki
    08.03.2016 15:10
    +1

    [описываемый подход] сильно облегчает разработку, когда связанные компоненты находятся на разных ветвях иерархии

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

    • Компонента (часть UI) может знать только о своих непосредственных потомках (которых она так или иначе и создала)
    • Компонента-родитель при создании передает непосредственному потомку данные, нужные ему для работы (+ опционально дергает его методы интерфейса), и слушает его события — тупо передает ему обработчик, который надо дернуть
    • Компонента-потомок не знает о родителе — только генерирует события, а точнее, просто дергает полученные обработчики
    • Все дополнительное взаимодействие организуется через сервисы («вечные» единицы бизнес-логики): сервисы уже генерируют полноценные события, на которые может подписываться множество слушателей (других сервисов или компонент), с учетом жизненного цикла слушателей (для Angular это — автоотписка компоненты при уничтожении ее scope, как в примере в статье


  1. Fesor
    08.03.2016 19:21

    Я некоторое время занимаюсь разработкой на Angular и использование EDA подхода сильно помогает в создании приложений.

    А вы другие подходы пробовали? EDA далеко не для всех задач будет удобен. По своему опыту, EDA подходит для сильно связанных систем, например игрушки. А web приложеньки — там в 99% слуачев больше проблем.

    Далее, использование событий $scope вне жизненного цикла директив/компонентов — антипаттерн, не для того эта штука там нужна. Я даже правило в eslint добавить вот планирую, что бы не пропускало коммиты где $scope используется вне link директив.

    большое количество возможных событий, то количество watchers

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

    магическое число 2000 давно перевалило за 100к.

    Оно изначально было ~100К. Фраза про 2000 была о том, что если у вас 2000 ватчеров на скрин, значит вы что-то делаете неправильно. Да и производительность ватчеров меряется не их количеством, а временем их работы. Простые ватчеры, которые втупую сравнивают все по ссылке — их может быть и 100К, а добавить один дип ватч на жирный объект который часто меняется, и он один уже убьет всю производительность.

    В целом — ватчеры в коде самого приложения вообще не нужны. Они нужны только самому ангуляру для работы биндингов, но не более того. Ну и еще директивы-примитивы для работы с DOM например, которые отслеживают изменение состояния и мутируют DOM.

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

    А вы когда-нибудь дебажили приложение построенное на этом подходе? Это ж маленький ад. А как это тестировать? Мокать везде менеджер событий? EDA используют в геймдеве, потому что там это единственный адекватный вариант как-то снизить связанность между компонентами без потерь производительности. В разработке приложений — это так себе подход.


    1. Werawoolf
      08.03.2016 19:30

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


      1. Fesor
        08.03.2016 19:48

        если грамотно спроектировать

        С этим нынче проблемы. Именно по этому использование таких подходов легко превращаются в боль и страдания.