Посредник

Посредник (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() разрастается тем же способом, каким раньше разрастались методы участников, — просто теперь в одном классе вместо многих, и это ничуть не легче поддерживать.
Итог
Посредник нужен, когда несколько объектов должны согласованно реагировать друг на друга, но прямые связи между ними превращают систему в трудно читаемый клубок зависимостей. Он переносит эти связи в один центральный объект, который явно знает всех участников и все правила их взаимодействия, — ценой риска, что сам этот объект станет новым узким местом, если не следить за тем, сколько логики в нём накапливается.