Посредник (Mediator)

Суть паттерна

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

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

Проблема

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

Беспорядочные связи между элементами пользовательского интерфейса
Беспорядочные связи между элементами пользовательского интерфейса.

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

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

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

Решение

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

В нашем примере посредником мог бы стать диалог. Скорее всего, класс диалога и так знает, из каких элементов состоит, поэтому никаких новых связей добавлять в него не придётся.

Элементы общаются через посредника
Элементы интерфейса общаются через посредника.

Основные изменения произойдут внутри отдельных элементов диалога. Если раньше при получении клика от пользователя объект кнопки сам проверял значения полей диалога, то теперь его единственной обязанностью будет сообщить диалогу о том, что произошёл клик. Получив извещение, диалог выполнит все необходимые проверки полей. Таким образом, вместо нескольких зависимостей от остальных элементов кнопка получит только одну — от самого диалога.

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

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

Аналогия из жизни

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

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

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

Структура

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

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

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

  4. Компоненты не должны общаться друг с другом напрямую. Если в компоненте происходит важное событие, он должен оповестить своего посредника, а тот сам решит — касается ли событие других компонентов, и стоит ли их оповещать. При этом компонент-отправитель не знает кто обработает его запрос, а компонент-получатель не знает кто его прислал.

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

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

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

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

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

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

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

Шаги реализации

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

  2. Создайте общий интерфейс посредников и опишите в нём методы для взаимодействия с компонентами. В простейшем случае достаточно одного метода для получения оповещений от компонентов.

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

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

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

  5. Компоненты тоже должны иметь ссылку на объект посредника. Связь между ними удобнее всего установить, подавая посредника в параметры конструктора компонентов.

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

Преимущества и недостатки

  • Устраняет зависимости между компонентами, позволяя повторно их использовать.

  • Упрощает взаимодействие между компонентами.

  • Централизует управление в одном месте.

  • Посредник может сильно раздуться.

Пример

# include <iostream>
# include <memory>
# include <list>
# include <vector>

using namespace std;

class Message {};         // Request

class Mediator;

class Colleague
{
private:
        weak_ptr<Mediator> mediator;

public:
        virtual ~Colleague() = default;
        
        void setMediator(shared_ptr<Mediator> mdr) { mediator = mdr; }
        
        virtual bool send(shared_ptr<Message> msg);
        virtual void receive(shared_ptr<Message> msg) = 0;
};

class ColleagueLeft : public Colleague
{
public:
        void receive(shared_ptr<Message> msg) override { cout << "Right - > Left;" << endl; }
};

class ColleagueRight : public Colleague
{
public:
        void receive(shared_ptr<Message> msg) override { cout << "Left - > Right;" << endl; }
};

class Mediator
{
protected:
        list<shared_ptr<Colleague>> colleagues;

public:
        virtual ~Mediator() = default;
        
        virtual bool send(const Colleague* coleague, shared_ptr<Message> msg) = 0;
        
        static bool add(shared_ptr<Mediator> mediator, initializer_list<shared_ptr<Colleague>> list);
};

class ConMediator : public Mediator
{
public:
        bool send(const Colleague* coleague, shared_ptr<Message> msg) override;
};

# pragma region Methods Colleague
bool Colleague::send(shared_ptr<Message> msg)
{
        shared_ptr<Mediator> mdr = mediator.lock();

        return mdr ? mdr->send(this, msg) : false;
}
# pragma endregion

# pragma region Methods Mediator
bool Mediator::add(shared_ptr<Mediator> mediator, initializer_list<shared_ptr<Colleague>> list)
{
        if (!mediator || list.size() == 0) return false;

        for (auto elem : list)
        {
                mediator->colleagues.push_back(elem);
                elem->setMediator(mediator);
        }

        return true;
}

bool ConMediator::send(const Colleague* colleague, shared_ptr<Message> msg)
{
        bool flag = false;
        for (auto&& elem : colleagues)
        {
                if (dynamic_cast<const ColleagueLeft*>(colleague) && dynamic_cast<ColleagueRight*>(elem.get()))
                {
                        elem->receive(msg);
                        flag = true;
                }
                else if (dynamic_cast<const ColleagueRight*>(colleague) && dynamic_cast<ColleagueLeft*>(elem.get()))
                {
                        elem->receive(msg);
                        flag = true;
                }
        }

        return flag;
}
#pragma endregion

int main()
{
        shared_ptr<Mediator> mediator = make_shared<ConMediator>();
        
        shared_ptr<Colleague> col1 = make_shared<ColleagueLeft>();
        shared_ptr<Colleague> col2 = make_shared<ColleagueRight>();
        shared_ptr<Colleague> col3 = make_shared<ColleagueLeft>();
        shared_ptr<Colleague> col4 = make_shared<ColleagueLeft>();
        
        Mediator::add(mediator, { col1, col2, col3, col4 });
        
        shared_ptr<Message> msg = make_shared<Message>();
        
        col1->send(msg);
        col2->send(msg);
}

Last updated