Посредник (Mediator)
Убирает прямые связи между объектами: каждый говорит только с посредником, а правила их взаимодействия собраны в одном месте.
Проблема
Над аэропортом кружат три самолёта, полоса одна. Если Aircraft договариваются между собой, каждый должен знать всех остальных: кто уже садится, кто ждёт, в каком порядке. Три самолёта — шесть связей, десять — уже девяносто. Правило «садится один, остальные ждут в очереди» размазано по всем бортам, и чтобы поменять его (скажем, пропускать вперёд борт с малым остатком топлива), придётся править каждый.
Решение
Самолёты не знают друг о друге. Каждый знает только диспетчерскую ControlTower: просит посадку и сообщает, что сел и освободил полосу. Всё остальное решает Tower: кому дать полосу, кого поставить в очередь, кого вызвать следующим.
Связи «каждый с каждым» превращаются в звезду: N объектов — N связей с посредником. Правило взаимодействия живёт в одном классе, и самолёты от его смены не меняются.
Структура
Коллега Aircraft делает две вещи: сообщает посреднику о своих действиях (requestLanding, land) и выполняет его команды (hold, clear). Ссылок на другие самолёты у него нет, только на ControlTower. Статус меняет сам самолёт, но решение о статусе принимает диспетчерская. land() сначала сообщает о посадке и только потом отмечает себя севшим: если диспетчерская откажет, статус не изменится.
Как это собирается
Диспетчерские создаёт система управления аэропортами, по одной Tower на аэропорт. Самолёты появляются из расписания прилётов: для каждого рейса создаётся Aircraft, и в конструктор ему передаётся диспетчерская аэропорта назначения. Это единственная связь, которую самолёт получает: о других бортах и о второй диспетчерской он не знает.
В примере два борта летят в SVO, один в LED. LH300 сразу получает полосу, хотя SVO занят: у каждой диспетчерской своя очередь. AF200 пытается сесть без разрешения, NotCleared ловит бортовая система и уходит на второй круг, а состояние диспетчерской не меняется. Когда SU100 освобождает полосу, Tower сам вызывает следующего из очереди.
Сценарий целиком с выводом: пример на PHP, он же на Go, Python и TypeScript в панели кода.
Реализации
Tower хранит два куска состояния: кто сейчас на полосе и очередь ожидающих. requestLanding либо отдаёт полосу, либо ставит в конец очереди; landed проверяет, что сел именно тот, кому дали полосу, и вызывает следующего. Самолёт, который садится без разрешения, получает ошибку NotCleared, и состояние диспетчерской не меняется.
| Язык | Особенность |
|---|---|
| PHP | Статус — enum Status, свойство public private(set) читается снаружи, меняется только самолётом; ошибка — NotCleared от LogicException |
| Go | ControlTower — интерфейс, Landed возвращает error с сентинелом ErrNotCleared; нулевое значение Tower{} готово к работе |
| Python | Контракт — Protocol, статус — StrEnum, очередь — deque |
| TypeScript | Статус — объединение строковых литералов, поле #status закрыто и читается через геттер |
Запуск: make test T=patterns/behavioral/mediator
Плюсы и минусы
Тесты запускают три борта, SU100, AF200 и LH300, и проверяют только статусы самолётов: первый получил полосу, двое ждут; после посадки SU100 полосу получает AF200, а LH300 всё ещё ждёт; AF200, который садится без разрешения, получает ошибку, и ни его статус, ни статус SU100 не меняются. Ни один тест не связывает самолёты напрямую: очередь существует только в диспетчерской.
- ✅ Коллеги не знают друг о друге: их можно добавлять, убирать и переиспользовать с другим посредником.
- ✅ Правило взаимодействия в одном месте: приоритет для борта с малым остатком топлива — правка одного
Tower. - ✅ Связей N вместо N x (N - 1).
- ❌ Посредник собирает в себе всю логику связей и легко вырастает в огромный класс, который знает всё обо всех.
- ❌ Поток управления менее нагляден: чтобы понять, почему самолёт перешёл в
cleared, надо читать посредника, а не самолёт.
Когда не применять
- Объектов два-три и связи между ними стабильны: прямой вызов понятнее, чем посредник ради посредника.
- Объекту нужно просто оповестить заинтересованных и не важно, кто и как отреагирует: это Наблюдатель (Observer) или шина событий, а не посредник с правилами.
- Нужен один упрощённый вход в набор подсистем, которые сами друг с другом не говорят: это Фасад.
Отличия между языками
- PHP —
public private(set)(PHP 8.4) даёт читаемый снаружи, но закрытый на запись статус без геттера. Посредник и коллеги ссылаются друг на друга, интерфейсControlTowerразрывает эту зависимость на уровне типов. - Go — циклическая ссылка «самолёт -> диспетчерская -> самолёт» не проблема внутри одного пакета, но интерфейс всё равно нужен, чтобы тест мог подставить другую диспетчерскую. Ошибка — значение, а не исключение:
Landвозвращает её и не меняет статус. - Python —
Protocolпроверяет форму, наследоваться от негоTowerне обязан. Аннотация"Aircraft"в кавычках: класс объявлен ниже контракта. - TypeScript —
implements ControlTowerнеобязателен (структурной типизации хватило бы), но делает роль класса явной. Приватное поле#statusзакрыто и в рантайме, не только для компилятора.
Практика
Учебная диспетчерская обрабатывает два события и ни одно её действие не вызывает новых событий. В форме оформления заказа всё сложнее: посредник сам меняет поля, а поля об этом сообщают ему же, и без защиты уведомления ходят по кругу. Практика: форма оформления заказа показывает CheckoutForm, где способ доставки, страна и промокод связаны только через посредника, и одно изменение пользователя даёт ровно один проход правил.
Связанные темы
- Практика: форма оформления заказа — посредник между полями формы и защита от зацикливания уведомлений
- Фасад (Facade) — тоже центральный объект, но подсистемы о фасаде не знают, а коллеги посредника знают и сами к нему обращаются.
- Стратегия (Strategy) — правило выбора следующего борта можно вынести в стратегию и подставлять в
Tower.