Паттерны для масштабируемых JavaScript-приложений ч.6

Паттерн «Медиатор»

Объяснить, что представляет собой паттерн «медиатор» достаточно просто на примере следующей аналогии — представьте себе контроль траффика в аэропорте: все решения о том, какие самолеты могут взлетать или садиться, принимает диспетчер. Для этого, все сообщения, исходящие от самолетов, поступают в башню управления, вместо того, чтобы пересылаться между самолетами напрямую. Такой централизованный контроллер — это и есть ключ к успеху нашей системы. Это и есть «медиатор».

Медиатор применяется в системах, где взаимодействие между модулями может быть весьма сложными, но, в то же время, хорошо определенными. Если вы полагаете, что связи между модулями вашей системы будут постоянно расти и усложняться, то, возможно, вам стоит добавить центральный элемент управления. Паттерн «медиатор» отлично подходит для этой роли.

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

Какие еще преимущества существуют у «медиатора»? К примеру, медиатор позволяет каждому модулю функционировать абсолютно независимо от других компонентов системы, что приводит к большей гибкости. Если вам ранее уже приходилось использовать паттерн «наблюдатель» в роли системы доставки событий между различными частями в вашей системе, то вам не составит труда разобраться с медиатором.

Давайте посмотрим на модель взаимодействия модулей и медиатора:

модель взаимодействия модулей и медиатора

Мы можем рассматривать модули, как «издателей», публикующих события. Медиатор же является и «издателем» и «подписчиком» одновременно. В примере, Module 1 посылает сообщение, предполагающее некоторую реакцию, медиатору. Затем, медиатор, получив сообщение, уведомляет другие модули об определенных действиях, которые необходимо выполнить для завершения задачи. Module 2 выполняет необходимые Module 1 действия, и сообщает о результате обратно, в медиатор. В это же время, медиатор запускает Module 3 для логирования поступающих сообщений.

Обратите внимание: здесь нет прямого взаимодействия между модулями. Если в Module 3 произойдет ошибка или, к примеру, он просто перестанет работать, то медиатор, теоретически, может приостановить выполнение задач в других модулях, затем перезапустить Module 3 и продолжить работу, практически не влияя на работу всей системы. Такая слабая связанность модулей является одним из самых сильных преимуществ паттерна «медиатор», который я вам предлагаю использовать.

Посмотрим на его преимущества:

Уменьшает связывание модулей, добавляя посредника — центральный элемент управления. Это позволяет модулям отправлять и слушать сообщения, не затрагивая остальной части системы. Сообщения могут быть обработаны любым количеством модулей сразу.

Благодаря слабой связанности кода, внедрение новой функциональности происходит существенно легче.

И недостатки:

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

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

Пример: одна из возможных реализаций паттерна «медиатор», основанная на работе @rpflorence

var mediator = (function() {
    var subscribe = function(channel, fn) {
        if (!mediator.channels[channel]) mediator.channels[channel] = [];
        mediator.channels[channel].push({ context: this, callback: fn });
        return this;
    },
 
    publish = function(channel) {
        if (!mediator.channels[channel]) return false;
        var args = Array.prototype.slice.call(arguments, 1);
        for (var i = 0, l = mediator.channels[channel].length; i < l; i++) {
            var subscription = mediator.channels[channel][i];
            subscription.callback.apply(subscription.context, args);
        }
        return this;
    };
 
    return {
        channels: {},
        publish: publish,
        subscribe: subscribe,
        installTo: function(obj) {
            obj.subscribe = subscribe;
            obj.publish = publish;
        }
    };
 
}());

И два примера использования реализации, написанной выше:

//Pub/sub on a centralized mediator
 
mediator.name = "tim";
mediator.subscribe('nameChange', function(arg) {
    console.log(this.name);
    this.name = arg;
    console.log(this.name);
});
 
mediator.publish('nameChange', 'david'); //tim, david
 
 
//Pub/sub via third party mediator
 
var obj = {name: 'sam'};
mediator.installTo(obj);
obj.subscribe('nameChange', function(arg) {
    console.log(this.name);
    this.name = arg;
    console.log(this.name);
});
 
obj.publish('nameChange', 'john'); //sam, john

Использование фасада: абстракция ядра

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

Фасад также обеспечивает последовательный и доступный в любой момент интерфейс для модулей. Это похоже на песочницу в архитектуре Николаса Закаса.

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

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

К примеру, модуль может отправить сообщение dataValidationCompletedWriteToDB. В подобных случаях задача фасада — убедиться, действительно ли этот модуль имеет права на запись в базу данных. Таким образом, мы пытаемся предотвратить проблемы с модулями, которые пытаются делать то, что они не должны.

Подведем итоги: медиатор представляет собой интерпретацию паттерна «подписчик/издатель», но он получает только те сообщения, которые нас действительно интересуют. За фильтрацию же всех сообщений отвечает фасад.

Использование медиатора: ядро приложения

В этой главе мы кратко пройдемся по некоторым зонам ответственности медиатора, который играет роль ядра приложения. Но, для начала, давайте разберемся, что он представляет собой в целом.

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

В идеальной ситуации, однажды запущенный модуль, должен функционировать самостоятельно. В задачи ядра не входит принятие решений о том, как реагировать, например, на событие DOM ready — в нашей архитектуре у модулей есть достаточно возможностей для того, чтобы принимать такие решения самостоятельно.

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

В дополнение, ядро должно быть в состоянии добавлять или удалять модули не ломая ничего. Типичный пример — определенный набор функций может быть недоступен на начальном этапе загрузки страницы, но эти функции могут быть загружены динамически, на основе определенных действий со стороны пользователя. Возвращаясь к нашему примеру с GMail, google может, по умолчанию, держать чат в свернутом состоянии и загружать соответствующий ему модуль динамически, только в том случае, если пользователь проявит интерес к использованию этой части приложения. С точки зрения оптимизации производительности, это должно дать заметный эффект.

Обслуживание ошибок должно также обрабатываться ядром приложения. В добавок к сообщению об интересных событиях модули также должны сообщать и о любых ошибках которые произошли в их работе. Ядро должно соответствующим образом реагировать на эти ошибки (к примеру, останавливать модули, перезапускать их и тд). Это важно, как часть слабо связанной архитектуры, позволяющей реализовать новый или лучший подход к реализации уведомления пользователя об ошибках без ручного изменения в каждом модуле. Используя механизм для публикации событий и подписки на них в медиаторе мы сможем достичь этого.

Авторизация