Страницы

четверг, 20 марта 2014 г.

Паттерн Посредник

Пред. запись: RAII в C#. Локальный Метод Шаблона vs. IDisposable

Назначение: Определяет объект, инкапсулирующий способ взаимодействия множества объектов.

Другими словами: Посредник – это клей, связывающий несколько независимых классов между собой.

Описание: Посредник обеспечивает слабую связанность (low coupling) системы, избавляя объекты от необходимости явно ссылаться друг на друга, позволяя тем самым независимо изменять их и взаимодействия между ними.

Подробнее – Mediator on Wiki

Мотивация

В качестве примера, давайте отойдем от темы анализа паттернов проектирования и посмотрим на пример, приведенный Робертом Мартином в своей статье "Принцип инверсии зависимостей" (Robert Martin – Dependency Inversion Principle) и в своей книге "Принципы, паттерны и методики гибкой разработки на языке C#".

Итак, у нас есть класс Lamp (лампа) и Button (кнопка). При этом в исходном варианте дизайна кнопка принимает лампу в аргументах конструктора и включает/отключает ее в случае нажатия:

clip_image002

Такой подход делает класс кнопки зависимым от класса лампы, а это значит, что класс Button не может быть использовать повторно, но, что не менее важно, эти его нельзя анализировать и развивать независимо. Можно пойти по пути, придложенному Робертом Мартином: выделить интерфейс ButtonServer и застравить класс Lamp реализовывать его. В этом случае класс Button может развиваться самостоятельно, но класс Lamp должен реализовать интерфейс ButtonServer и будет знать, каким именно образом им будут управлять.

Вместо этого можно пойти другим путем. Достаточно сделать кнопку "наблюдаемым" объектом и изменить ее таким образом, чтобы она могла уведомлять своих клиентов о нажатии. После этого, достаточно добавить некоторый класс-посредник, например, LampController, который будет знать о кнопке и лампе и будет решать более высокоуровневую задачу – управление лампой с помощью кнопки:

clip_image004

В данном случае мы разделяем систему на части, изолируя одну часть от другой. Такой подход упрощает развитие каждой из этих частей, поскольку Посредник выступает барьером, который гасит изменения в одной части системы, не давая им распространятся на другие части! Любые изменения в классе Button приведут к модификации LampController, но не потребует изменений класса Lamp, и наоборот.

ПРИМЕЧАНИЕ
Подробнее о принципе инверсии зависимостей и моем отношению к нему можно почитать в статье "Критический взгляд на принцип инверсии зависимостей".

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

Обсуждение

Основная суть борьбы со сложность заключается в изоляции информации, чтобы у разработчика была возможность сосредоточиться на одной проблеме и игнорировать другие. Именно по этой причине мы стараемся объединять логически связанные куски системы в отдельные модули (отсюда сильная связаность – high cohesion), максимально изолируя эти модули друг от друга (отсюда слабая связанность – low coupling).

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

ПРИМЕЧАНИЕ
Посредник – это один из самых распространенных паттернов проектирования, который используется десятками в любом приложении, даже если вы об этом и не подозреваете.

Давайте вспомним классы Lamp и Button. Мы пришли к дизайну, в котором сделали оба эти класса независимыми и объединили их лишь на более высоком уровне с помощью посредника – LampController. Мы можем не называть подобные классы посредниками явно, но они выполняют именно эту роль: связывают воедино несколько автономных классов или компонентов между собой.

Если же вернуться к более формальному обсуждению паттерна Посредник, то можно выделить две разновидности этого паттерна: посредник может быть явным, и о нем могут знать "объединяемые компоненты", или же он может быть невидимым для этих компонентов и объединять их между собой без их ведома.

Явный и неявный посредник

В классической реализации паттерна Посредник, независимые классы не знают друг о друге, но знают о существовании Посредника и все взаимодействие происходит через него явным образом:

clip_image006

Такой подход довольно распространен, и активно применяется в паттернах Producer/Consumer, Event Aggregator или в других случаях, когда классы знают о существовании "общей шины взаимодействия". В этом случае классы взаимодействуют с посредником явно, возлагая на него ответственность за передачу управления другому классу. При этом посредник может содержать ссылки на взаимодействующие классы и "маршрутизировать" вызовы явно. Или же Посредник может быть наблюдаемым объектом и предоставлять набор событий, на которые будут подписываться коллеги для взаимодействия между собой.

С другой стороны, Посредник не обязательно должен быть явным с точки зрения классов более низкого уровня. В некоторых случаях мы можем пойти другим путем и объединить несколько классов, которые не знают вообще ни о ком другом. В этом случае один из участников-коллег должен быть наблюдаемым оъектом, а Посредник – наблюдателем, который реагирует на изменения состояния одного объекта и уведомляет об этом других.

clip_image008

ПРИМЕЧАНИЕ
Наличие в дизайне системы Наблюдателей зачастую говорит о наличии Посредников.

Именно такой подход используется в Windows Forms, когда форма выступает в роли Посредника и собирает события от компонентов формы, передавая их другим компонента или бизнес-объектам.

Вот небольшой пример. При наличии формы (CustomForm) с двумя элементами управления – TextBox и Button, мы можем добавить логику разрешения кнопки сохранения при вводе пользователем данных в TextBox:

public CustomForm()
{
    InitializeComponent();
    buttonSave
.Enabled = false
;

    textBoxName
.TextChanged += (s, ea) =>
    {
        buttonSave
.Enabled = true;
    };
}

Этот же подход мы рассматривали ранее, при обсуждении классов Lamp и Button. В таком виде Посредник не является ярковыраженным, но по своей сути, он продолжает выполнять ту же самую роль.

Явные и неявные связи

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

Не стоит забывать, что слабая связанность (loose coupling) должна всегда идти рука об руку с сильной связностью (high cohesion). А это значит, что дизайн системы в которой никто ни о ком не знает, так же утопичен, как и дизайн, в котором каждый класс знает обо всех остальных.

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

Есть два паталогических случая использования посредников для получения "слабосвязного" дизайна. Один из них основывается на явном Посреднике, а другой – на неявном.

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

clip_image010

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

class Button
{
   
// Метод проверяет состояние кнопки
    public void
CheckState()
    {
       
// Получаем состояние кнопки и увемдоляем всех
        // заинтересованных подписчиков в новом состоянии
        bool turnedOn =
GetButtonState();
       
EventAggregator.
Instance
           
.GetEvent<ButtonClickEvent>().
Raise(turnedOn);
    }

   
private bool GetButtonState() { return true
; }
}


class Lamp : IDisposable
{
   
private readonly SubscribtionToken
_token;

   
public
Lamp()
    {
       
// Для последующей успешной отписки, нам нужно
        // сохранить маркер подписки
        _token =
            EventAggregator.
Instance
               
.GetEvent<ButtonClickEvent
>()
               
.Subscribe(btnTurnedOn =>
                {
                   
if
(btnTurnedOn)
                        TurnOn();
                   
else
                        TurnOff();
                });
    }

   
private void
TurnOn() { }
   
private void
TurnOff() { }

   
public void
Dispose()
    {
       
// Не забываем отписываться от глобального
        // объекта во избежание утечек памяти!
        EventAggregator.
Instance
           
.GetEvent<ButtonClickEvent
>()
           
.Unsubscribe(_token);
    }
}

ПРИМЕЧАНИЕ
Код класса EventAggregator здесь не приводится, но он делает ровно то, ради чего он предназначен: маршрутизирует события между подписчиком события и его инициатором.

Использование подобных техник получения "слабосвязнного" дизайна – это большая фикция. На самом деле, глобальный Event Aggregator приводит к решению, в котором любые два класса могут коммуницировать между собой. По большому счету, обильное использование Event Aggregator приводит к невероятно связанным (tightly coupled) решениям, просто явные связи между классами заменяются неявными.

Не стоит забывать, что наличие событий в долгоживущих объектах является опасной практикой, поскольку легко может привести к утечкам памяти в управляемой среде. Если события в долгоживущих объектах не реализованы на основе "слабых событий" (Weak Event Pattern), то все подписчики должны явно от них отписаться. В противном случае долгоживущий объект будет продолжать держать на них ссылку, не давая собрать их сборщиком мусора.

ПРИМЕЧАНИЕ
Именно поэтому Event Aggregator из Prism-а реализован на основе «слабых событий».

СОВЕТ
Слабосвязанный дизайн не означает то, что класс A не имеет ссылки на класс Б. Слабосвязанный дизайн говорит о том, что класс А может успешно функционировать, развиваться и изменяться никак не завися от класса Б. Наличие же неявных связей через события, интерфейсы или глобальные переменные не устраняет связи между этими классами, он лишь прячет их и делает эти связи неявными.
Если предметная область говорит о наличии связи между понятием А и понятием Б, то в дизайне системе лучше всего отразить эти отношения явным образом.

Тестировать или не тестировать? Вот в чем вопрос!

При наличии в системах довольно большого количества Посредников может возникнуть логичный вопрос: стоит ли покрывать их юнит-тестами? У сторонников TDD будет готов ответ на этот вопрос, но что делать всем остальным?

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

Здесь, как и во многих других случаях, следует попытаться найти компромисс между трудозатратами на разработку и поддержку тестов, и выгодой от их наличия. Любая важная логика должна быть покрыта тестами. Точка. Но должны ли посредники содержать критически важную логику? Вполне возможно, что сам факт наличия сложной логики в посреднике говорит о том, что он делает слишком многое. Если же его "посредническая" логика сложная и важная, то стоит подумать о том, нельзя ли ее упростить: нет ли скрытых абстракций или, быть может, связываемые классы просто должны знать друг о друге напрямую и посредник является лишним.

Тестирование поведения осуществляется с помощью Моков – специального вида тестовых подделок, которые поддерживаются большинством современных тестовых фреймворков. Благодаря ним, обычно не составляет особого труда написать набор тестов, которые будут проверять, что в определенных условиях тестируемый класс (CUT – class under test) вызывает те или иные методы своих зависимостей. Однако с такими тестами нужно быть осторожными и уделять особое внимание тому, чтобы проверять лишь ключевую логику (например, лишь факт вызова метода зависимости, без проверки точных аргументов вызова), а также стараться избегать проверок деталей реализации.

ПРИМЕЧАНИЕ
Подробнее о разнице между тестированием состояния и тестированием поведения можно прочитать в моей статье "Моки и стабы" или "Mocks Aren’t Stubs" Мартина Фаулера.

Архитектурные посредники

Как уже говорилось неоднократно, Посредник может применяться на разных уровнях. Существуют классы-посредники, компоненты-посредники, но есть даже целые модули или слои приложения, выполняющие эту же роль.

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

Во время реализации мы можем прийти к набору разнообразных модулей, таких как модули парсинга, анализа и сохранения паттернов проектирования, построения и отображения отчетов и т.п. Затем мы можем создать несколько приложений (executables), которые будут объединять и настраивать эти модули в зависимости от своих целей.

В результате мы получим несколько диаграмм следующего вида:

clip_image012

Таким образом слой приложения (Application Layer) выступает в роли архитектурного Посредника, объединяя воедино модули или целые слои приложения. Обычно именно слой приложения содержит достаточно информации о конкретных типах стратегий, именно здесь инициализируются IoC-контейнеры, и именно здесь происходит "маршрутизация" информации между слоями или модулями приложения.

Примеры в .NET Framework

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

  • В Windows Forms любая форма по своей сути является Посредником, объединяющий элементы управления между собой: форма подписывается на события одного элемента управления и в случае изменения состояния уведомляет бизнес-объекты или другие элементы управления.
  • Event Aggregator, активно используемый в WPF и за его пределами, является примером "глобального" посредника для связи разных независимых компонентов между собой.
  • В паттернах MVC и MVP, Controller и Presenter обычно выступает в роли посредника между представлением и моделью.
  • Паттерн Producer/Consumer является еще одним примером использования паттерна Посредник. В этом паттерне Потребитель и Поставщик не знают друг о друге и общаются между собой за счет общей очереди, которая является Посредником. В случае .NET Framework таким посредником может служить, BlockingCollection.
  •  

Применимость

Паттерн Посредник идеально подходит для объединения воедино нескольких автономных классов или компонентов. Каждый раз, когда вы задаетесь вопросом о том, как же изолировать классы А и Б, чтобы они могли жить независимо, подумайте о Посреднике.

При этом остается несколько вариантов решения: классы А и Б могут знать о посреднике или же один из классов может быть «наблюдаемым». Выбор того или иного вариант зависит от "толщины" интерфейса взаимодействия между этими классами. Если "протокол общения" прост, то использование наблюдателя будет вполне оправданым, если же он достаточно сложен, то проще будет использовать посредник явно.

Когда третий лишний

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

Так, если в нашем дизайне с лампой поменяется интерфейс Кнопки (например, изменится имя, добавится третье, но не нужное состояние и т.п.), то изменения не коснутся Лампы, они будут погашены посредником. Но если мы захотим усовершенствовать систему и добавить третье состояние в Кнопку и Лампу, то теперь нам придется изменить 3 класса вместо двух!

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

Дополнительные ссылки

5 комментариев:

  1. Ссылка [TS3] ведет на локалхост

    ОтветитьУдалить
    Ответы
    1. Да их вообще не должно быть. Это ссылки на коменты из оригинального вордовского документа.
      Спасибо, поправил.

      Удалить
  2. Здравствуйте, Сергей. Прочитал вашу статью об паттерне Посредник. Статья понравилась, не с некоторыми вашими выводамы я не соглашусь к сожалению. Не совсем понятно почему вы рассматриваете паттерн EventAggregator как посредник. Тот же Мартин Фаулер http://martinfowler.com/eaaDev/EventAggregator.html предлагает рассматривать этот паттерн как частичный случай паттерна Фасад. Этот паттерн больше можно отнести к паттерну Observer чем к паттерну Mediator, так как он построен вокруг наблюдателя. Перечитал после вашего поста несколько раз Фаулера, и посмотрел ваш пример. Реализация класса Lamp с использованием EventAggregator больше похожа на частный случай наблюдателя, чем на посредника. Хотя здесь довольно тонкая грань.

    ОтветитьУдалить
  3. С моей точки зрения Event Aggregator - это обобщенный посредник, который используется для коммуникации двух и более объектов между собой.
    Так, если взглянуть на рисунок из GoF-а и первый рисунок из статьи Мартина, то сходство будет более очевидным:

    картинка

    Конечно, Event Aggregator - это очень частный случай посредника. При этом главным (и достаточно важным) отличием является то, что Посредник *обычно* инкапсулирует процесс общения двух и более компонентов, а Event Aggregator является лишь инструментом, а за процесс общения отвечают сами компоненты. Тем не менее, с моей точки зрения у этих подходов довольно много общего, к тому же, эти подходы взаимозаменимы.

    Поэтому, ИМХО, Event Aggregator тоже может выступать в роли Посредника.

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

    З.Ы.Ы. Кстати, я не один нахожу общее между Посредником и Event Aggregator-ом, вот, например, аналогичные мысли. Хотя, нужно признать, многие считают Event Aggregator и Посредник - разными паттернами.

    ОтветитьУдалить
  4. Здравствуйте Сергей, у меня возник такой вопрос.
    Допустим у меня есть Delivery и в момент когда статус этого Delivery == sent, я хочу входящие в Delivery продукты добавить на склад. За это отвечает сервис StockService. То есть это выглядит так: StockService->addProduct(Delivery->products);

    Насколько я понял, если я буду использовать посредника DeliveryStockMediator то при получении Event::sent от Delivery, DeliveryStockMediator сделает тоже самое.

    То есть обращаюсь к StockService->addProduct(Event->sender->products)?

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

    ОтветитьУдалить