Edit Page

Mediators


Медиаторы используются для взаимодействия пользователей с одним или более компонентами Представления, (например DOM elements, UI Component, HTTP request, RabbitMQ message, ...) в браузере или для обработки Запросов, Событий, Сообщений на NodeJS, и остальными частями PureMVC приложения.

Во Frontend-приложении Медиатор является местом, где обычно устанавливают обработчики событий Представления для обработки пользовательских действий и запросов данных от компонента. Он посылает и принимает Оповещения (Notifications) для взаимодействия c приложением.

Задачи конкретного Медиатора

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

Так же для NodeJS - существует не мало библиотек для работы с различными каналами данных, протоколами передачи данных или через непосредственную работу с UNIX-сокетами.

Задача PureMVC - оставаться нейтральным к используемым технологиям в приложении, предоставляя простые идиомы для работы с любыми компонентами графического интерфейса, каналами и протоколами данными или Моделью Данных.

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

Конкретный Медиатор позволяет использовать один или более компонентов Представления в приложении используя только ссылки и предоставленное API.

Основная задача Медиатора - обработка событий (Events), инициированных компонентом Представления и касающихся его Оповещений (Notifications).

Медиаторы также часто взаимодействуют с Прокси. Довольно распространенная практика получения и хранения локальных ссылок на часто используемые экземпляры Прокси в конструкторе. Это уменьшает многочисленные вызовы retrieveProxy для получения одних и тех же ссылок.

Неявное преобразование типов компонентов Представления

Базовый класс Медиатор реализованный в PureMVC принимает в качестве аргументов своего конструктора имя и объект Представления типа Object.

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

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

После присвоения вы часто будете приводить этот объект к конкретному типу, что может быть неудобно, а также способствовать распространению повторяемого кода.

Язык JavaScript предоставляет возможность называемую геттеры и сеттеры (getters/setters). Геттер выглядит как метод, но используется как свойство класса. Эта возможность очень полезна для решения проблемы частого приведения типа.

Полезная идиома применяемая в вашем конкретном Медиаторе, - использование геттера для приведения типа Представления к его настоящему типу с понятным именем.

Например так:

@property get controlBar(): MyAppControlBar {
  return this.getViewComponent();
}

Затем, в любом месте вашего Медиатора чем делать это так:

const vc: MyAppControlBar = this.getViewComponent();
vc.searchSelection = MyAppControlBar.NONE_SELECTED;

Мы вместо этого делаем так:

this.controlBar.searchSelection = MyAppControlBar.NONE_SELECTED;

Взаимодействие с Представлением

Медиатор обычно имеет только один компонент Представления, но может взаимодействовать и с несколькими, например: с ApplicationToolBar и содержащимися в нем кнопками, либо другими аналогичными компонентами. Мы можем иметь группу связанных компонентов (как форма) в одном Представлении и обращаться Медиатором к её элементам как к свойствам. Но лучше инкапсулировать в Представлении как можно больше компонентов. Иметь специализированный объект для обмена данными тоже хорошая практика.

Медиатор отвечает за взаимодействие уровня Контроллера и Модели, обновляя Представление когда получает соответствующие Оповещения (Notifications). Во Flash, мы обычно подписываем слушателей событий к компоненту Представления, в момент создания Медиатора или по вызову метода setViewComponent, определяя метод обработчика:

@method onRegister() {
  super.onRegister();
  // The STDOUT pipe from the shell to all modules
  this._junction.registerPipe(STDOUT, OUTPUT, TeeSplit.new());
  // The STDIN pipe to the shell from all modules
  this._junction.registerPipe(STDIN, INPUT, TeeMerge.new());
  this._junction.addPipeListener(STDIN, this, this.handlePipeMessage);
  // The STDLOG pipe from the shell to the logger
  this._junction.registerPipe(STDLOG, OUTPUT, Pipe.new());
  this.send(CONNECT_SHELL_TO_LOGGER, this._junction);
}

Действия Медиатора, в ответ на возникшее Событие, определяется требованиями логики.

Обычно, конкретный метод обработчика События Медиатора выполняет такие действия:

  • Изучает тип События либо поля специализированного типа События, который ожидается.
  • Читает или модифицирует доступные свойства (либо вызывает методы) компоненты Представления.
  • Читает или модифицирует доступные свойства (либо вызывает методы) Прокси.
  • Шлет одно или более Оповещение, на которое будут реагировать Медиаторы и Команды.

Несколько хороших правил:

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

Обработка оповещений в Медиаторе

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

После регистрации Представления, Медиатор опрашивается на наличие заинтересованных оповещений и в ответ возвращает массив имен Оповещений (Notifications name), которые он хотел бы обрабатывать.

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

Создать список заинтересованных Оповещений (Notification Interests) для конкретного Медиатора можно следующим образом:

@method listNotificationInterests(): string[] {
  return [ CONNECT_MODULE_TO_LOGGER, CONNECT_SHELL_TO_LOGGER ];
}

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

Внутри обработчика оповещений, для удобства и читабельности кода вместо «if / else if» лучше использовать конструкцию «switch / case».

По сути, для каждого оповещения достаточно небольшого обработчика, а вся нужная информация должна находиться в самом объекте оповещения. Правда иногда информация может быть получена от Посредника, основанная на данных из объекта оповещения. Желательно избегать нагромождения логики в одном обработчике, иначе это верный признак того, что вы стараетесь перенести бизнес логику из Команды (Command) в обработчик оповещений (notification) медиатора.

@method handleNotification<T = ?any>(note: NotificationInterface<T>): ?Promise<void> {
  switch (note.getName()) {
    // Connect any Module's STDLOG to the logger's STDIN
    case (CONNECT_MODULE_TO_LOGGER):
      const module = note.getBody();
      const pipe = Pipe.new();
      module.acceptOutputPipe(STDLOG, pipe);
      this.logger.acceptInputPipe(STDIN, pipe);
      break;
    // Bidirectionally connect shell and logger on STDLOG/STDSHELL
    case (CONNECT_SHELL_TO_LOGGER):
      // The junction was passed from ShellJunctionMediator
      const junction = note.getBody();
      // Connect the shell's STDLOG to the logger's STDIN
      const shellToLog = junction.retrievePipe(STDLOG);
      this.logger.acceptInputPipe(STDIN, shellToLog);
      // Connect the logger's STDSHELL to the shell's STDIN
      const logToShell = Pipe.new();
      const shellIn = junction.retrievePipe(STDIN);
      shellIn.connectInput(logToShell);
      this.logger.acceptOutputPipe(STDSHELL, logToShell);
      break;
  }
}

Практика показывает, что лучше всего обрабатывать 4-5 оповещений в одном обработчике.

Если оповещений больше, желательно разбить этот обработчик на несколько менее громоздких. Создавая медиаторы для отдельных компонентов Представления, можно избежать накопления оповещений в одном обработчике.

Использование единого, заранее определенного метода оповещения, является главной отличительной чертой Медиатора, как он обрабатывает события и оповещения.

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

Используя Оповещения, у вас есть единственный метод-обработчик, в котором вы обрабатываете все Оповещения нужные для Медиатора.

Лучше всего обрабатывать все оповещения в одном методе, используя switch конструкцию для различия оповещения по именам.

Было много обсуждений по поводу использования switch конструкции, многие разработчики считают, что эта конструкция ограничивает одним способом обработки. Однако рекомендуемая практика это использования одного метода обработчика оповещения и switch-конструкция.

Медиатор является посредником между Представлением и остальной частью системы.

Рассмотрим роль переводчицы беседы между послом и остальными членами конференции ООН. Она редко делает что-либо большее чем перевод и получение сообщений, разве что иногда подбирая подходящие метафоры или факты. Тоже самое можно сказать о роли Медиатора в рамках PureMVC.

Связка Медиатора с Прокси и другими Медиаторами

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

Представление должно знать о Модели, но Модель, в свою очередь, не должна знать о Представлении.

Медиатор может легко получить доступ к Прокси через Модель и читать и манипулировать данными, используя программный интерфейс Прокси. Если выполнять те же операции с помощью Команд, будет потеряна связь между Моделью и Представлением. - Однако это можно рассматривать и с положительной точки зрения, т.к. уменьшается связность между Моделью и Представлением. Чтобы понять, как поступить в этой ситуации, стоит руководствоваться правилом Данные вниз - Экшены ввер (DDAU), т.е. можно использовать Прокси непосредственно в Медиаторе только для отображения данных (только на чтение), а для изменения каких либо данных в ответ на Событие в Представлении менять данные в Прокси только внутри Команды через отправку Оповещения.

Так же Медиатор может получить ссылки на другие Медиаторы из Представления и манипулировать данным уже из других Медиаторов.

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

Медиатор, желающий взаимодействовать с другим областям Представления, должен послать Оповещение, а не манипулировать Представлением напрямую.

Медиатор не должен раскрывать методы для манипулирования своими компонентами - вместо этого он должен отвечать на оповещения и выполнять свою работу.

Если большая часть поведения Компонентов Представления реализована в Медиаторе (в ответ на Событие или Оповещение), желательно часть из них перенести в само Представление, так чтобы в дальнейшем можно было повторно использовать.

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

Взаимодействие пользователя с Представлением и Медиаторами

Рассмотрим медиатор SimpleMediator, инкапсулирующий в себе инстанс readline, для взаимодействия с пользователем через консоль.

Взаимодействие инстанса readline и Медиатора SimpleMediator заключается в том, что утилита запускает обработчик на событие 'line', когда пользователь ввел текст. Медиатор SimpleMediator обрабатывает событие, отсылая Оповещение MSG_FROM_CONSOLE с введенными данными.

import readline from 'readline';

export default (Module) => {
  const {
    START_CONSOLE, MSG_FROM_CONSOLE, MSG_TO_CONSOLE,
    Mediator,
    initialize, partOf, meta, property, method, nameBy
  } = Module.NS;

  @initialize
  @partOf(Module)
  class SimpleMediator extends Mediator {
    @nameBy static __filename = __filename;
    @meta static object = {};

    @method listNotificationInterests(): string[] {
      const interests = super.listNotificationInterests(... arguments);
      interests.push(START_CONSOLE);
      interests.push(MSG_TO_CONSOLE);
      return interests;
    }

    @method handleNotification<T = ?any>(note: NotificationInterface<T>): ?Promise<void> {
      switch (note.getName()) {
        case (START_CONSOLE):
          this.stdinStart();
          break;
        case (MSG_TO_CONSOLE):
          this.stdinComplete(note.getBody());
          break;
        default:
          super.handleNotification(note);
      }
    }

    @method onRegister() {
      super.onRegister();
      this.rl = readline.createInterface({
        input: process.stdin,
        output: process.stdout,
        prompt: '... waiting new text ...\n'
      });
      this.rl.setMaxListeners(Number.MAX_SAFE_INTEGER);
    }

    @method async onRemove(): Promise<void> {
      await super.onRemove();
      this.rl.close()
    }

    @method stdinStart() {
      console.log('Start: ');
      this.rl.prompt();
      this.rl.on('line', (input) => {
        console.log(`Received: ${input}`);
        this.send(MSG_FROM_CONSOLE, input);
      });
    }

    @method stdinComplete(body) {
      console.log('Complete: ', body);
      this.rl.prompt();
    }
  }
}

Метод onRegister вызывается при инстанцировании Медиатора, в нем мы создаем компонент консольного интерфейса с пользователем.

Вся логика компонента находится в Медиаторе, где обрабатывается событие 'line' и отсылается MSG_FROM_CONSOLE Оповещениею

import type { NotificationInterface } from '../interfaces/NotificationInterface';
import type { SimpleProxyInterface } from '../interfaces/SimpleProxyInterface';

export default (Module) => {
  const {
    SIMPLE_PROXY, MSG_TO_CONSOLE,
    Command,
    initialize, partOf, meta, method, property, nameBy, inject,
  } = Module.NS;

  @initialize
  @partOf(Module)
  class SimpleCommand extends Command {
    @nameBy static  __filename = __filename;
    @meta static object = {};

    @inject(`Factory<${SIMPLE_PROXY}>`)
    @property _simpleProxyFactory: () => SimpleProxyInterface;
    @property get _simpleProxy(): SimpleProxyInterface {
      return this._simpleProxyFactory()
    }

    @method execute<T = ?any>(note: NotificationInterface<T>): void {
      this._simpleProxy.setData(note.getBody())
      this.send(MSG_TO_CONSOLE, this._simpleProxy.getData());
    }
  }
}

Mы зарегистрировали команду SimpleCommand для этого Оповещения. Эта команда будет вызывать метод setData Прокси SimpleProxy, передавая объект, полученный от медиатора.

Медиатор SimpleMediator имеет список заинтересованных оповещений, в который входят START_CONSOLE и MSG_TO_CONSOLE. Оповещение START_CONSOLE отправляется после запуска приложения, чтобы активировать утилиту внутри этого медиатора, а MSG_TO_CONSOLE - это Оповещение, на которое подписан Медиатор, чтобы вывести на консоль пользователю некоторое сообщение.