Перейти к содержимому
Фёдор Башинский

Посредник

6 min read
Обложка статьи «Посредник»

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

Проблема

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

final class Lift
{
    public function __construct(
        private readonly AirCompressor $compressor,
    ) {}
 
    public function raise(): void
    {
        // ... поднять подъёмник
        $this->compressor->shutOff();
    }
}

Похожим образом устроен и датчик запуска двигателя — при старте двигателя должна включаться вытяжка:

final class EngineStartSensor
{
    public function __construct(
        private readonly ExhaustExtractor $extractor,
    ) {}
 
    public function onEngineStart(): void
    {
        $this->extractor->turnOn();
    }
}

Требования по безопасности продолжают появляться: при подъёме подъёмника нужно ещё включать сигнальную лампу на посту, а при опускании — гасить её обратно. Каждое новое правило означает, что Lift обрастает ещё одной прямой зависимостью — сначала от компрессора, теперь от лампы, — и его конструктор превращается в список всех устройств, на которые подъёмник может повлиять. То же самое произошло бы с датчиком запуска двигателя, появись у него ещё одна связанная реакция.

Проблема не только в разрастании конструкторов. Правила безопасности поста — «поднят подъёмник → выключить компрессор → включить лампу» — размазаны по методам разных устройств, и чтобы понять полный список правил, нужно просмотреть код каждого из них по отдельности. Протестировать Lift в изоляции, не поднимая настоящий компрессор и лампу, становится сложнее с каждым новым правилом.

Решение

Посредник берёт на себя все правила взаимодействия между устройствами. Устройства перестают знать друг о друге и общаются только с посредником через общий интерфейс:

interface BayMediator
{
    public function notify(object $sender, string $event): void;
}

Lift теперь оповещает посредника о событии, не зная, кто и как на это событие отреагирует:

final class Lift
{
    private ?BayMediator $mediator = null;
 
    public function setMediator(BayMediator $mediator): void
    {
        $this->mediator = $mediator;
    }
 
    public function raise(): void
    {
        // ... поднять подъёмник
        $this->mediator?->notify($this, 'lift_raised');
    }
 
    public function lower(): void
    {
        // ... опустить подъёмник
        $this->mediator?->notify($this, 'lift_lowered');
    }
}

Остальные устройства становятся простыми исполнителями команд, без единой ссылки друг на друга:

final class AirCompressor
{
    public function shutOff(): void { /* ... */ }
    public function turnOn(): void { /* ... */ }
}
 
final class ExhaustExtractor
{
    public function turnOn(): void { /* ... */ }
    public function turnOff(): void { /* ... */ }
}
 
final class WarningLight
{
    public function on(): void { /* ... */ }
    public function off(): void { /* ... */ }
}

Конкретный посредник знает обо всех устройствах поста и хранит все правила их взаимодействия в одном месте:

final class ServiceBayController implements BayMediator
{
    public function __construct(
        private readonly AirCompressor $compressor,
        private readonly ExhaustExtractor $extractor,
        private readonly WarningLight $warningLight,
    ) {}
 
    public function notify(object $sender, string $event): void
    {
        match ($event) {
            'lift_raised' => $this->onLiftRaised(),
            'lift_lowered' => $this->onLiftLowered(),
            'engine_started' => $this->extractor->turnOn(),
        };
    }
 
    private function onLiftRaised(): void
    {
        $this->compressor->shutOff();
        $this->warningLight->on();
    }
 
    private function onLiftLowered(): void
    {
        $this->warningLight->off();
    }
}

Сборка поста происходит один раз в клиентском коде:

$controller = new ServiceBayController(
    new AirCompressor(),
    new ExhaustExtractor(),
    new WarningLight(),
);
 
$lift = new Lift();
$lift->setMediator($controller);
 
$lift->raise(); // компрессор выключен, лампа включена — сам Lift об этом ничего не знает

Теперь Lift знает только про интерфейс BayMediator, а не про компрессор, вытяжку или лампу напрямую. Новое правило безопасности — правка ServiceBayController, а не поиск нужного метода среди всех затронутых устройств.

Из чего состоит паттерн

  • Посредник (BayMediator) — интерфейс, через который участники сообщают о своих событиях.
  • Конкретный посредник (ServiceBayController) — знает обо всех участниках и реализует правила их взаимодействия.
  • Коллеги (Lift, AirCompressor, ExhaustExtractor, WarningLight) — знают только о посреднике, не друг о друге.
  • Клиент — связывает коллег с конкретным посредником при сборке поста.

Посредник vs Наблюдатель

Оба паттерна развязывают объекты, которым иначе пришлось бы хранить прямые ссылки друг на друга, — но развязывают по-разному. Посредник знает обо всех участниках и решает, что конкретно должно произойти в ответ на событие: ServiceBayController::onLiftRaised() явно вызывает $this->compressor->shutOff() и $this->warningLight->on() — посредник избавляет от связей отправителя события, но сам остаётся жёстко связан с конкретными получателями и их методами. Наблюдатель устроен иначе: субъект вообще не знает, кто на него подписан и что эти подписчики будут делать, — он просто оповещает список объектов, реализующих общий интерфейс, ничего не зная об их внутренностях.

ПосредникНаблюдатель
Кто знает участниковПосредник знает всех участников и их конкретные методыСубъект не знает ничего о своих наблюдателях, кроме общего интерфейса
Кто решает реакциюПосредник — все правила взаимодействия в одном местеКаждый наблюдатель сам решает, как реагировать на событие
Типичная формаОдин посредник на группу тесно связанных, разнородных объектовУ каждого субъекта свой независимый список подписчиков на одно событие

Когда применять

  • Несколько объектов должны согласованно взаимодействовать по правилам, которые иначе размазались бы по методам каждого из них, — как показано в примере с постом.
  • Объекты должны оставаться переиспользуемыми по отдельности: Lift можно поставить на другой пост с другими правилами безопасности, просто подключив другой посредник.
  • Логика взаимодействия часто меняется, и её удобнее держать и находить в одном месте, а не в методах всех классов-участников по отдельности.

Плюсы и минусы

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

Минус — посредник рискует превратиться в божественный объект, вобравший в себя всю логику системы. Если правил становится очень много, метод вроде notify() разрастается тем же способом, каким раньше разрастались методы участников, — просто теперь в одном классе вместо многих, и это ничуть не легче поддерживать.

Итог

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