Перейти к содержанию

Посредник

Также известен как: Intermediary, Controller, Mediator

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

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

Проблема

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

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

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

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

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

Код элементов нужно трогать при изменении каждого диалога.

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

Решение

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

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

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

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

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

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

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

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

Пример с диспетчерской башней.

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

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

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

Структура

Структура классов паттерна Посредник

  1. Компоненты — это разнородные объекты, содержащие бизнес-логику программы. Каждый компонент хранит ссылку на объект посредника, но работает с ним только через абстрактный интерфейс посредников. Благодаря этому, компоненты можно повторно использовать в другой программе, связав их с посредником другого типа.
  2. Посредник определяет интерфейс для обмена информацией с компонентами. Обычно хватает одного метода, чтобы оповещать посредника о событиях, произошедших в компонентах. В параметрах этого метода можно передавать детали события: ссылку на компонент, в котором оно произошло, и любые другие данные.
  3. Конкретный посредник содержит код взаимодействия нескольких компонентов между собой. Зачастую этот объект не только хранит ссылки на все свои компоненты, но и сам их создаёт, управляя дальнейшим жизненным циклом.
  4. Компоненты не должны общаться друг с другом напрямую. Если в компоненте происходит важное событие, он должен оповестить своего посредника, а тот сам решит — касается ли событие других компонентов, и стоит ли их оповещать. При этом компонент-отправитель не знает кто обработает его запрос, а компонент-получатель не знает кто его прислал.

Псевдокод

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

Структура классов примера паттерна Посредник

Пример структурирования классов UI-диалогов.

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

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

// Общий интерфейс посредников.
interface Mediator is
    method notify(sender: Component, event: string)


// Конкретный посредник. Все связи между конкретными
// компонентами переехали в код посредника. Он получает
// извещения от своих компонентов и знает, как на них
// реагировать.
class AuthenticationDialog implements Mediator is
    private field title: string
    private field loginOrRegisterChkBx: Checkbox
    private field loginUsername, loginPassword: Textbox
    private field registrationUsername, registrationPassword,
                  registrationEmail: Textbox
    private field okBtn, cancelBtn: Button

    constructor AuthenticationDialog() is
        // Здесь нужно создать объекты всех компонентов, подав
        // текущий объект-посредник в их конструктор.

    // Когда что-то случается с компонентом, он шлёт посреднику
    // оповещение. После получения извещения посредник может
    // либо сделать что-то самостоятельно, либо перенаправить
    // запрос другому компоненту.
    method notify(sender, event) is
        if (sender == loginOrRegisterChkBx and event == "check")
            if (loginOrRegisterChkBx.checked)
                title = "Log in"
                // 1. Показать компоненты формы входа.
                // 2. Скрыть компоненты формы регистрации.
            else
                title = "Register"
                // 1. Показать компоненты формы регистрации.
                // 2. Скрыть компоненты формы входа.

        if (sender == okBtn && event == "click")
            if (loginOrRegister.checked)
                // Попробовать найти пользователя с данными из
                // формы логина.
                if (!found)
                    // Показать ошибку над формой логина.
            else
                // 1. Создать пользовательский аккаунт с данными
                // из формы регистрации.
                // 2. Авторизировать этого пользователя.
                // ...


// Классы компонентов общаются с посредниками через их общий
// интерфейс. Благодаря этому одни и те же компоненты можно
// использовать в разных посредниках.
class Component is
    field dialog: Mediator

    constructor Component(dialog) is
        this.dialog = dialog

    method click() is
        dialog.notify(this, "click")

    method keypress() is
        dialog.notify(this, "keypress")

// Конкретные компоненты не связаны между собой напрямую. У них
// есть только один канал общения — через отправку уведомлений
// посреднику.
class Button extends Component is
    // ...

class Textbox extends Component is
    // ...

class Checkbox extends Component is
    method check() is
        dialog.notify(this, "check")
    // ...

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

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

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

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

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

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

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

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

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

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

  3. Реализуйте этот интерфейс в классе конкретного посредника. Поместите в него поля, которые будут содержать ссылки на все объекты компонентов.
  4. Вы можете пойти дальше и переместить код создания компонентов в класс посредника, после чего он может напоминать фабрику или фасад.
  5. Компоненты тоже должны иметь ссылку на объект посредника. Связь между ними удобнее всего установить, подавая посредника в параметры конструктора компонентов.
  6. Измените код компонентов так, чтобы они вызывали метод оповещения посредника, вместо методов других компонентов. С противоположной стороны, посредник должен вызывать методы нужного компонента, когда получает оповещение от компонента.

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

Преимущества

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

Недостатки

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

Примеры реализации паттерна

Python

  • Сложность: 2/3
  • Популярность: 0/3
  • Применимость: Пожалуй, самое популярное применение Посредника в Python-коде — это связь нескольких компонентов GUI одной программы.

Этот пример показывает структуру паттерна Посредник, а именно — из каких классов он состоит, какие роли эти классы выполняют и как они взаимодействуют друг с другом.

from __future__ import annotations
from abc import ABC


class Mediator(ABC):
    """
    Интерфейс Посредника предоставляет метод, используемый
    компонентами для уведомления посредника о различных событиях.
    Посредник может реагировать на эти события и передавать исполнение
    другим компонентам.
    """

    def notify(self, sender: object, event: str) -> None:
        pass


class ConcreteMediator(Mediator):
    def __init__(self, component1: Component1, 
        component2: Component2) -> None:
        self._component1 = component1
        self._component1.mediator = self
        self._component2 = component2
        self._component2.mediator = self

    def notify(self, sender: object, event: str) -> None:
        if event == "A":
            print("Mediator reacts on A and triggers following "
                + "operations:")
            self._component2.do_c()
        elif event == "D":
            print("Mediator reacts on D and triggers following "
                + "operations:")
            self._component1.do_b()
            self._component2.do_c()


class BaseComponent:
    """
    Базовый Компонент обеспечивает базовую функциональность хранения
    экземпляра посредника внутри объектов компонентов.
    """

    def __init__(self, mediator: Mediator = None) -> None:
        self._mediator = mediator

    @property
    def mediator(self) -> Mediator:
        return self._mediator

    @mediator.setter
    def mediator(self, mediator: Mediator) -> None:
        self._mediator = mediator


"""
Конкретные Компоненты реализуют различную функциональность. Они не
зависят от других компонентов. Они также не зависят от каких-либо
конкретных классов посредников.
"""


class Component1(BaseComponent):
    def do_a(self) -> None:
        print("Component 1 does A.")
        self.mediator.notify(self, "A")

    def do_b(self) -> None:
        print("Component 1 does B.")
        self.mediator.notify(self, "B")


class Component2(BaseComponent):
    def do_c(self) -> None:
        print("Component 2 does C.")
        self.mediator.notify(self, "C")

    def do_d(self) -> None:
        print("Component 2 does D.")
        self.mediator.notify(self, "D")


if __name__ == "__main__":
    # Клиентский код.
    c1 = Component1()
    c2 = Component2()
    mediator = ConcreteMediator(c1, c2)

    print("Client triggers operation A.")
    c1.do_a()

    print("\n", end="")

    print("Client triggers operation D.")
    c2.do_d()
Client triggers operation A.
Component 1 does A.
Mediator reacts on A and triggers following operations:
Component 2 does C.


Client triggers operation D.
Component 2 does D.
Mediator reacts on D and triggers following operations:
Component 1 does B.
Component 2 does C.

PHP

  • Сложность: 2/3
  • Популярность: 0/3
  • Применимость: Посредник не столь актуален в PHP, как в других языках, из-за того, что разные компоненты приложения не часто общаются друг с другом в пределах одной сессии скрипта.
    Тем не менее, примерами паттерна могут служить EventDispatcher-ы многих фреймворков, а также некоторые реализации контроллеров в MVC фреймворках.

Этот пример показывает структуру паттерна Посредник, а именно — из каких классов он состоит, какие роли эти классы выполняют и как они взаимодействуют друг с другом.

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

<?php
/**
* Интерфейс Посредника предоставляет метод, используемый компонентами для
* уведомления посредника о различных событиях. Посредник может реагировать на
* эти события и передавать исполнение другим компонентам.
*/

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

/**
* Конкретные Посредники реализуют совместное поведение, координируя отдельные
* компоненты.
*/

class ConcreteMediator implements Mediator
{
    private $component1;

    private $component2;

    public function __construct(Component1 $c1, Component2 $c2)
    {
        $this->component1 = $c1;
        $this->component1->setMediator($this);
        $this->component2 = $c2;
        $this->component2->setMediator($this);
    }

    public function notify(object $sender, string $event): void
    {
        if ($event == "A") {
            echo "Mediator reacts on A and triggers following operations:\n";
            $this->component2->doC();
        }

        if ($event == "D") {
            echo "Mediator reacts on D and triggers following operations:\n";
            $this->component1->doB();
            $this->component2->doC();
        }
    }
}

/**
* Базовый Компонент обеспечивает базовую функциональность хранения экземпляра
* посредника внутри объектов компонентов.
*/

class BaseComponent
{
    protected $mediator;

    public function __construct(Mediator $mediator = null)
    {
        $this->mediator = $mediator;
    }

    public function setMediator(Mediator $mediator): void
    {
        $this->mediator = $mediator;
    }
}

/**
* Конкретные Компоненты реализуют различную функциональность. Они не зависят от
* других компонентов. Они также не зависят от каких-либо конкретных классов
* посредников.
*/

class Component1 extends BaseComponent
{
    public function doA(): void
    {
        echo "Component 1 does A.\n";
        $this->mediator->notify($this, "A");
    }

    public function doB(): void
    {
        echo "Component 1 does B.\n";
        $this->mediator->notify($this, "B");
    }
}

class Component2 extends BaseComponent
{
    public function doC(): void
    {
        echo "Component 2 does C.\n";
        $this->mediator->notify($this, "C");
    }

    public function doD(): void
    {
        echo "Component 2 does D.\n";
        $this->mediator->notify($this, "D");
    }
}

/**
* Клиентский код.
*/
$c1 = new Component1();
$c2 = new Component2();
$mediator = new ConcreteMediator($c1, $c2);

echo "Client triggers operation A.\n";
$c1->doA();

echo "\n";
echo "Client triggers operation D.\n";
$c2->doD();
Client triggers operation A.
Component 1 does A.
Mediator reacts on A and triggers following operations:
Component 2 does C.

Client triggers operation D.
Component 2 does D.
Mediator reacts on D and triggers following operations:
Component 1 does B.
Component 2 does C.

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

<?php
/**
* Класс Диспетчера Событий выполняет функции Посредника и содержит логику
* подписки и уведомлений. Хотя классический Посредник часто зависит от
* конкретных классов компонентов, этот привязан только к их абстрактным
* интерфейсам.
*
* Достичь слабой связанности между компонентами можно благодаря особому способу
* установления связей между ними. Компоненты сами могут подписаться на
* интересующие их конкретные события через интерфейс подписки Посредника.
*
* Обратите внимание, что мы не можем использовать здесь встроенные в PHP
* интерфейсы Subject/Observer, так как они не дадут нам реализовать расширенные
* методы подписки и оповещений.
*/

class EventDispatcher
{
    /**
    * @var array
    */
    private $observers = [];

    public function __construct()
    {
        // Специальная группа событий для наблюдателей, которые хотят слушать
        // все события.
        $this->observers["*"] = [];
    }

    private function initEventGroup(string &$event = "*"): void
    {
        if (!isset($this->observers[$event])) {
            $this->observers[$event] = [];
        }
    }

    private function getEventObservers(string $event = "*"): array
    {
        $this->initEventGroup($event);
        $group = $this->observers[$event];
        $all = $this->observers["*"];
        return array_merge($group, $all);
    }

    public function attach(Observer $observer, string $event = "*"): void
    {
        $this->initEventGroup($event);
        $this->observers[$event][] = $observer;
    }

    public function detach(Observer $observer, string $event = "*"): void
    {
        foreach ($this->getEventObservers($event) as $key => $s) {
            if ($s === $observer) {
                unset($this->observers[$event][$key]);
            }
        }
    }

    public function trigger(string $event, object $emitter, $data = null): void
    {
        echo "EventDispatcher: Broadcasting the '$event' event.\n";
        foreach ($this->getEventObservers($event) as $observer) {
            $observer->update($event, $emitter, $data);
        }
    }
}

/**
* Простая вспомогательная функция для предоставления глобального доступа к
* диспетчеру событий.
*/

function events(): EventDispatcher
{
    static $eventDispatcher;
    if (!$eventDispatcher) {
        $eventDispatcher = new EventDispatcher();
    }

    return $eventDispatcher;
}

/**
* Интерфейс Наблюдателя определяет, как компоненты получают уведомления о
* событиях.
*/

interface Observer
{
    public function update(string $event, object $emitter, $data = null);
}

/**
* В отличие от нашего примера паттерна Наблюдатель, этот пример заставляет
* ПользовательскийРепозиторий действовать как обычный компонент, который не
* имеет никаких специальных методов, связанных с событиями. Как и любой другой
* компонент, этот класс использует ДиспетчерСобытий для трансляции своих
* событий и прослушивания других.
*
* @see \RefactoringGuru\Observer\RealWorld\UserRepository
*/

class UserRepository implements Observer
{
    /**
    * @var array Список пользователей приложения.
    */
    private $users = [];

    /**
    * Компоненты могут подписаться на события самостоятельно или через
    * клиентский код.
    */
    public function __construct()
    {
        events()->attach($this, "users:deleted");
    }

    /**
    * Компоненты могут принять решение, будут ли они обрабатывать событие,
    * используя его название, источник или какие-то контекстные данные,
    * переданные вместе с событием.
    */
    public function update(string $event, object $emitter, $data = null): void
    {
        switch ($event) {
            case "users:deleted":
                if ($emitter === $this) {
                    return;
                }
                $this->deleteUser($data, true);
                break;
        }
    }

    // Эти методы представляют бизнес-логику класса.

    public function initialize(string $filename): void
    {
        echo "UserRepository: Loading user records from a file.\n";
        // ...
        events()->trigger("users:init", $this, $filename);
    }

    public function createUser(array $data, bool $silent = false): User
    {
        echo "UserRepository: Creating a user.\n";
        $user = new User();
        $user->update($data);
        $id = bin2hex(openssl_random_pseudo_bytes(16));
        $user->update(["id" => $id]);
        $this->users[$id] = $user;
        if (!$silent) {
            events()->trigger("users:created", $this, $user);
        }
        return $user;
    }

    public function updateUser(User $user, array $data, bool $silent = false): ?User
    {
        echo "UserRepository: Updating a user.\n";
        $id = $user->attributes["id"];
        if (!isset($this->users[$id])) {
            return null;
        }
        $user = $this->users[$id];
        $user->update($data);
        if (!$silent) {
            events()->trigger("users:updated", $this, $user);
        }
        return $user;
    }

    public function deleteUser(User $user, bool $silent = false): void
    {
        echo "UserRepository: Deleting a user.\n";

        $id = $user->attributes["id"];
        if (!isset($this->users[$id])) {
            return;
        }
        unset($this->users[$id]);
        if (!$silent) {
            events()->trigger("users:deleted", $this, $user);
        }
    }
}

/**
* Давайте сохраним класс Пользователя тривиальным, так как он не является
* главной темой нашего примера.
*/

class User
{
    public $attributes = [];

    public function update($data): void
    {
        $this->attributes = array_merge($this->attributes, $data);
    }

    /**
    * Все объекты могут вызывать события.
    */
    public function delete(): void
    {
        echo "User: I can now delete myself without worrying about the repository.\n";
        events()->trigger("users:deleted", $this, $this);
    }
}

/**
* Этот Конкретный Компонент регистрирует все события, на которые он подписан.
*/
class Logger implements Observer
{
    private $filename;

    public function __construct($filename)
    {
        $this->filename = $filename;
        if (file_exists($this->filename)) {
            unlink($this->filename);
        }
    }

    public function update(string $event, object $emitter, $data = null)
    {
        $entry = date("Y-m-d H:i:s") . ": '$event' with data '" . json_encode($data) . "'\n";
        file_put_contents($this->filename, $entry, FILE_APPEND);
        echo "Logger: I've written '$event' entry to the log.\n";
    }
}

/**
* Этот Конкретный Компонент отправляет начальные инструкции новым
* пользователям. Клиент несёт ответственность за присоединение этого компонента
* к соответствующему событию создания пользователя.
*/

class OnboardingNotification implements Observer
{
    private $adminEmail;

    public function __construct(string $adminEmail)
    {
        $this->adminEmail = $adminEmail;
    }

    public function update(string $event, object $emitter, $data = null): void
    {
        // mail($this->adminEmail,
        //     "Onboarding required",
        //     "We have a new user. Here's his info: " .json_encode($data));

        echo "OnboardingNotification: The notification has been emailed!\n";
    }
}

/**
* Клиентский код.
*/

$repository = new UserRepository();
events()->attach($repository, "facebook:update");

$logger = new Logger(__DIR__ . "/log.txt");
events()->attach($logger, "*");

$onboarding = new OnboardingNotification("1@example.com");
events()->attach($onboarding, "users:created");

// ...

$repository->initialize(__DIR__ . "users.csv");

// ...

$user = $repository->createUser([
    "name" => "John Smith",
    "email" => "john99@example.com",
]);

// ...

$user->delete();
UserRepository: Loading user records from a file.
EventDispatcher: Broadcasting the 'users:init' event.
Logger: I've written 'users:init' entry to the log.
UserRepository: Creating a user.
EventDispatcher: Broadcasting the 'users:created' event.
OnboardingNotification: The notification has been emailed!
Logger: I've written 'users:created' entry to the log.
User: I can now delete myself without worrying about the repository.
EventDispatcher: Broadcasting the 'users:deleted' event.
UserRepository: Deleting a user.
Logger: I've written 'users:deleted' entry to the log.

Задачи

Задача 1. Чат-комната

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

from __future__ import annotations
from abc import ABC, abstractmethod
from datetime import datetime


class ChatMediator(ABC):
    @abstractmethod
    def send_message(self, sender: "User", message: str,
                     recipient: "User" = None) -> None: ...

    @abstractmethod
    def add_user(self, user: "User") -> None: ...


class ChatRoom(ChatMediator):
    def __init__(self, name: str) -> None:
        self.name = name
        # TODO: хранить список пользователей и историю сообщений

    def add_user(self, user: "User") -> None:
        # TODO: добавить пользователя и оповестить комнату
        pass

    def send_message(self, sender: "User", message: str,
                     recipient: "User" = None) -> None:
        # TODO: если recipient указан — приватное сообщение,
        #       иначе — broadcast всем кроме отправителя
        pass

    def get_history(self) -> list[str]:
        # TODO: вернуть историю сообщений
        pass


class User:
    def __init__(self, name: str) -> None:
        self.name = name
        self._mediator: ChatMediator | None = None

    def join(self, room: ChatMediator) -> None:
        # TODO: подключиться к комнате
        pass

    def send(self, message: str, to: "User" = None) -> None:
        # TODO: отправить сообщение через посредника
        pass

    def receive(self, message: str) -> None:
        print(f"[{self.name}] {message}")


# Ожидаемый результат:
# room = ChatRoom("General")
# alice, bob, carol = User("Alice"), User("Bob"), User("Carol")
# alice.join(room); bob.join(room); carol.join(room)
# alice.send("Всем привет!")
# bob.send("Привет, Алиса!", to=alice)
# carol.send("Как дела?")
# print(room.get_history())
<?php
interface ChatMediator
{
    public function sendMessage(User $sender, string $message, ?User $recipient = null): void;
    public function addUser(User $user): void;
}

class ChatRoom implements ChatMediator
{
    private string $name;
    // TODO: хранить список пользователей и историю сообщений

    public function __construct(string $name)
    {
        $this->name = $name;
    }

    public function addUser(User $user): void
    {
        // TODO: добавить пользователя и оповестить комнату
    }

    public function sendMessage(User $sender, string $message, ?User $recipient = null): void
    {
        // TODO: если $recipient задан — приватное сообщение,
        //       иначе — broadcast всем кроме отправителя
    }

    public function getHistory(): array
    {
        // TODO: вернуть историю сообщений
    }
}

class User
{
    private string $name;
    private ?ChatMediator $mediator = null;

    public function __construct(string $name)
    {
        $this->name = $name;
    }

    public function getName(): string { return $this->name; }

    public function join(ChatMediator $room): void
    {
        // TODO: подключиться к комнате
    }

    public function send(string $message, ?User $to = null): void
    {
        // TODO: отправить через посредника
    }

    public function receive(string $message): void
    {
        echo "[{$this->name}] $message\n";
    }
}

// Ожидаемый результат:
// $room = new ChatRoom("General");
// $alice = new User("Alice"); $bob = new User("Bob"); $carol = new User("Carol");
// $alice->join($room); $bob->join($room); $carol->join($room);
// $alice->send("Всем привет!");
// $bob->send("Привет, Алиса!", $alice);
// $carol->send("Как дела?");
// print_r($room->getHistory());

Задача 2. Система управления воздушным движением

Реализуйте систему управления полётами через диспетчерскую башню (посредник). Самолёты не общаются напрямую — только через башню. Башня следит за взлётно-посадочными полосами и координирует посадку и взлёт.

from __future__ import annotations
from abc import ABC, abstractmethod
from enum import Enum


class RunwayStatus(Enum):
    FREE = "free"
    OCCUPIED = "occupied"


class AirTrafficMediator(ABC):
    @abstractmethod
    def request_landing(self, aircraft: "Aircraft") -> bool: ...

    @abstractmethod
    def request_takeoff(self, aircraft: "Aircraft") -> bool: ...

    @abstractmethod
    def notify_landed(self, aircraft: "Aircraft") -> None: ...

    @abstractmethod
    def notify_taken_off(self, aircraft: "Aircraft") -> None: ...


class ControlTower(AirTrafficMediator):
    def __init__(self, runways: int = 2) -> None:
        # TODO: хранить состояние полос и очередь ожидания
        pass

    def request_landing(self, aircraft: "Aircraft") -> bool:
        # TODO: если есть свободная полоса — разрешить посадку,
        #       иначе добавить в очередь ожидания
        pass

    def request_takeoff(self, aircraft: "Aircraft") -> bool:
        # TODO: аналогично для взлёта
        pass

    def notify_landed(self, aircraft: "Aircraft") -> None:
        # TODO: освободить полосу и разрешить следующему из очереди
        pass

    def notify_taken_off(self, aircraft: "Aircraft") -> None:
        # TODO: освободить полосу
        pass


class Aircraft:
    def __init__(self, flight_id: str, tower: AirTrafficMediator) -> None:
        self.flight_id = flight_id
        self._tower = tower

    def land(self) -> None:
        # TODO: запросить посадку через башню
        pass

    def take_off(self) -> None:
        # TODO: запросить взлёт через башню
        pass


# Ожидаемый результат:
# tower = ControlTower(runways=1)
# f1 = Aircraft("SU-101", tower)
# f2 = Aircraft("SU-202", tower)
# f3 = Aircraft("SU-303", tower)
# f1.land()   # SU-101: Посадка разрешена, полоса 1
# f2.land()   # SU-202: Ожидание...
# f3.land()   # SU-303: Ожидание...
# f1.take_off()  # башня освобождает полосу, SU-202 получает разрешение
<?php
enum RunwayStatus { case Free; case Occupied; }

interface AirTrafficMediator
{
    public function requestLanding(Aircraft $aircraft): bool;
    public function requestTakeoff(Aircraft $aircraft): bool;
    public function notifyLanded(Aircraft $aircraft): void;
    public function notifyTakenOff(Aircraft $aircraft): void;
}

class ControlTower implements AirTrafficMediator
{
    // TODO: хранить состояние полос и очередь ожидания

    public function __construct(int $runways = 2)
    {
        // TODO: инициализировать полосы
    }

    public function requestLanding(Aircraft $aircraft): bool
    {
        // TODO: выдать разрешение или добавить в очередь
    }

    public function requestTakeoff(Aircraft $aircraft): bool
    {
        // TODO: аналогично для взлёта
    }

    public function notifyLanded(Aircraft $aircraft): void
    {
        // TODO: освободить полосу и разрешить следующему из очереди
    }

    public function notifyTakenOff(Aircraft $aircraft): void
    {
        // TODO: освободить полосу
    }
}

class Aircraft
{
    public function __construct(
        private readonly string $flightId,
        private readonly AirTrafficMediator $tower,
    ) {}

    public function getFlightId(): string { return $this->flightId; }

    public function land(): void
    {
        // TODO: запросить посадку через башню
    }

    public function takeOff(): void
    {
        // TODO: запросить взлёт через башню
    }
}

// Ожидаемый результат:
// $tower = new ControlTower(runways: 1);
// $f1 = new Aircraft("SU-101", $tower);
// $f2 = new Aircraft("SU-202", $tower);
// $f3 = new Aircraft("SU-303", $tower);
// $f1->land();      // SU-101: Посадка разрешена, полоса 1
// $f2->land();      // SU-202: Ожидание...
// $f3->land();      // SU-303: Ожидание...
// $f1->takeOff();   // башня освобождает полосу, SU-202 получает разрешение

Задача 3. Форма регистрации с зависимыми полями

Реализуйте посредника для UI-формы регистрации, где поля взаимодействуют через него. Компоненты: чекбокс «Юридическое лицо» (показывает/скрывает поле «ИНН»), поле «Страна» (меняет формат телефона), кнопка «Отправить» (активируется только когда все обязательные поля заполнены).

from __future__ import annotations
from abc import ABC, abstractmethod


class FormMediator(ABC):
    @abstractmethod
    def notify(self, sender: "FormComponent", event: str) -> None: ...


class FormComponent(ABC):
    def __init__(self, name: str) -> None:
        self.name = name
        self._mediator: FormMediator | None = None
        self.enabled = True
        self.visible = True

    def set_mediator(self, mediator: FormMediator) -> None:
        self._mediator = mediator

    def notify(self, event: str) -> None:
        if self._mediator:
            self._mediator.notify(self, event)


class Checkbox(FormComponent):
    def __init__(self, name: str) -> None:
        super().__init__(name)
        self.checked = False

    def toggle(self) -> None:
        self.checked = not self.checked
        self.notify("change")


class TextField(FormComponent):
    def __init__(self, name: str, placeholder: str = "") -> None:
        super().__init__(name)
        self.value = ""
        self.placeholder = placeholder

    def set_value(self, value: str) -> None:
        self.value = value
        self.notify("input")


class SelectField(FormComponent):
    def __init__(self, name: str, options: list[str]) -> None:
        super().__init__(name)
        self.options = options
        self.selected: str | None = None

    def select(self, option: str) -> None:
        self.selected = option
        self.notify("change")


class Button(FormComponent):
    def click(self) -> None:
        if self.enabled:
            self.notify("click")
        else:
            print(f"Button '{self.name}' is disabled")


class RegistrationForm(FormMediator):
    def __init__(self) -> None:
        # TODO: создать компоненты: is_legal (Checkbox), inn (TextField),
        #       country (SelectField), phone (TextField),
        #       email (TextField), submit (Button)
        # TODO: зарегистрировать self как медиатор для каждого
        pass

    def notify(self, sender: FormComponent, event: str) -> None:
        # TODO:
        # - is_legal.change → показать/скрыть inn
        # - country.change → обновить placeholder у phone
        # - любой TextField.input → проверить активность submit
        pass

    def _update_submit_state(self) -> None:
        # TODO: submit активен если email и phone заполнены,
        #       и если is_legal — то inn тоже заполнен
        pass


# Ожидаемый результат:
# form = RegistrationForm()
# print(form.inn.visible)          # False (скрыто по умолчанию)
# form.is_legal.toggle()
# print(form.inn.visible)          # True
# form.country.select("Russia")
# print(form.phone.placeholder)    # "+7 (___) ___-__-__"
# form.email.set_value("a@b.com")
# form.phone.set_value("+71234567890")
# form.inn.set_value("1234567890")
# print(form.submit.enabled)       # True
# form.submit.click()              # Отправка формы...
<?php
interface FormMediator
{
    public function notify(FormComponent $sender, string $event): void;
}

abstract class FormComponent
{
    protected ?FormMediator $mediator = null;
    public bool $enabled = true;
    public bool $visible = true;

    public function __construct(protected readonly string $name) {}

    public function getName(): string { return $this->name; }

    public function setMediator(FormMediator $mediator): void
    {
        $this->mediator = $mediator;
    }

    protected function notify(string $event): void
    {
        $this->mediator?->notify($this, $event);
    }
}

class Checkbox extends FormComponent
{
    public bool $checked = false;

    public function toggle(): void
    {
        $this->checked = !$this->checked;
        $this->notify("change");
    }
}

class TextField extends FormComponent
{
    public string $value = "";
    public string $placeholder = "";

    public function setValue(string $value): void
    {
        $this->value = $value;
        $this->notify("input");
    }
}

class SelectField extends FormComponent
{
    public ?string $selected = null;

    public function __construct(string $name, private readonly array $options)
    {
        parent::__construct($name);
    }

    public function select(string $option): void
    {
        $this->selected = $option;
        $this->notify("change");
    }
}

class Button extends FormComponent
{
    public function click(): void
    {
        if ($this->enabled) {
            $this->notify("click");
        } else {
            echo "Button '{$this->name}' is disabled\n";
        }
    }
}

class RegistrationForm implements FormMediator
{
    public Checkbox $isLegal;
    public TextField $inn;
    public SelectField $country;
    public TextField $phone;
    public TextField $email;
    public Button $submit;

    public function __construct()
    {
        // TODO: создать компоненты и связать с посредником
    }

    public function notify(FormComponent $sender, string $event): void
    {
        // TODO:
        // - isLegal.change → показать/скрыть inn
        // - country.change → обновить placeholder у phone
        // - TextField.input → проверить активность submit
    }

    private function updateSubmitState(): void
    {
        // TODO: submit активен если email и phone заполнены,
        //       и если isLegal — то inn тоже заполнен
    }
}

// Ожидаемый результат:
// $form = new RegistrationForm();
// var_dump($form->inn->visible);       // false
// $form->isLegal->toggle();
// var_dump($form->inn->visible);       // true
// $form->country->select("Russia");
// echo $form->phone->placeholder;      // "+7 (___) ___-__-__"
// $form->email->setValue("a@b.com");
// $form->phone->setValue("+71234567890");
// $form->inn->setValue("1234567890");
// var_dump($form->submit->enabled);    // true
// $form->submit->click();              // Отправка формы...

Задача 4. Торговая площадка (биржа)

Реализуйте упрощённую биржу как посредника между покупателями и продавцами. Трейдеры не знают друг о друге — они только подают заявки через биржу. Биржа сводит встречные заявки (ордера) по принципу «лучшая цена».

from __future__ import annotations
from dataclasses import dataclass, field
from enum import Enum


class OrderType(Enum):
    BUY = "buy"
    SELL = "sell"


@dataclass
class Order:
    trader: "Trader"
    order_type: OrderType
    asset: str
    quantity: int
    price: float
    order_id: int = field(default=0)


class Exchange:
    def __init__(self) -> None:
        # TODO: хранить книгу ордеров (buy_orders, sell_orders)
        self._next_id = 1

    def place_order(self, order: Order) -> None:
        # TODO: назначить order_id, попробовать исполнить заявку,
        #       если нет встречной — добавить в книгу ордеров
        pass

    def _match_orders(self, order: Order) -> None:
        # TODO: для BUY ищем SELL с price <= order.price (наименьшую),
        #       для SELL ищем BUY с price >= order.price (наибольшую).
        #       При совпадении оповестить обоих трейдеров через notify_trade()
        pass

    def cancel_order(self, order_id: int, trader: "Trader") -> bool:
        # TODO: снять заявку из книги ордеров
        pass

    def get_order_book(self, asset: str) -> dict:
        # TODO: вернуть {"buy": [...], "sell": [...]}
        pass


class Trader:
    def __init__(self, name: str, exchange: Exchange) -> None:
        self.name = name
        self._exchange = exchange
        self.trades: list[str] = []

    def buy(self, asset: str, quantity: int, price: float) -> Order:
        # TODO: создать и разместить ордер на покупку
        pass

    def sell(self, asset: str, quantity: int, price: float) -> Order:
        # TODO: создать и разместить ордер на продажу
        pass

    def notify_trade(self, message: str) -> None:
        self.trades.append(message)
        print(f"[{self.name}] {message}")


# Ожидаемый результат:
# exchange = Exchange()
# alice = Trader("Alice", exchange)
# bob = Trader("Bob", exchange)
# alice.sell("AAPL", quantity=10, price=150.0)
# bob.buy("AAPL", quantity=10, price=155.0)
# # [Alice] Сделка: продано 10 AAPL по 150.0
# # [Bob]   Сделка: куплено 10 AAPL по 150.0
# print(exchange.get_order_book("AAPL"))  # {"buy": [], "sell": []}
<?php
enum OrderType: string { case Buy = "buy"; case Sell = "sell"; }

class Order
{
    public int $orderId = 0;

    public function __construct(
        public readonly Trader $trader,
        public readonly OrderType $type,
        public readonly string $asset,
        public readonly int $quantity,
        public readonly float $price,
    ) {}
}

class Exchange
{
    private int $nextId = 1;
    // TODO: хранить книгу ордеров

    public function placeOrder(Order $order): void
    {
        // TODO: назначить orderId, попробовать исполнить,
        //       если нет встречной — добавить в книгу
    }

    private function matchOrders(Order $order): void
    {
        // TODO: найти встречный ордер по лучшей цене,
        //       оповестить трейдеров через notifyTrade()
    }

    public function cancelOrder(int $orderId, Trader $trader): bool
    {
        // TODO: снять заявку
    }

    public function getOrderBook(string $asset): array
    {
        // TODO: вернуть ["buy" => [...], "sell" => [...]]
    }
}

class Trader
{
    public array $trades = [];

    public function __construct(
        private readonly string $name,
        private readonly Exchange $exchange,
    ) {}

    public function getName(): string { return $this->name; }

    public function buy(string $asset, int $quantity, float $price): Order
    {
        // TODO: создать BUY-ордер и разместить на бирже
    }

    public function sell(string $asset, int $quantity, float $price): Order
    {
        // TODO: создать SELL-ордер и разместить на бирже
    }

    public function notifyTrade(string $message): void
    {
        $this->trades[] = $message;
        echo "[{$this->name}] $message\n";
    }
}

// Ожидаемый результат:
// $exchange = new Exchange();
// $alice = new Trader("Alice", $exchange);
// $bob = new Trader("Bob", $exchange);
// $alice->sell("AAPL", 10, 150.0);
// $bob->buy("AAPL", 10, 155.0);
// // [Alice] Сделка: продано 10 AAPL по 150.0
// // [Bob]   Сделка: куплено 10 AAPL по 150.0
// print_r($exchange->getOrderBook("AAPL")); // ["buy" => [], "sell" => []]

Задача 5. Игровой движок: система событий

Реализуйте игровой посредник, который координирует взаимодействие игровых объектов (персонаж, враги, UI, звуковая система). Объекты не знают друг о друге — только генерируют события. Посредник маршрутизирует события: player:damage → обновить HP-бар UI и воспроизвести звук, enemy:killed → обновить счёт и проверить победу, level:complete → показать экран победы.

from __future__ import annotations
from abc import ABC, abstractmethod
from typing import Any


class GameMediator(ABC):
    @abstractmethod
    def notify(self, sender: "GameObject", event: str,
               data: Any = None) -> None: ...


class GameObject(ABC):
    def __init__(self, name: str, mediator: GameMediator) -> None:
        self.name = name
        self._mediator = mediator

    def emit(self, event: str, data: Any = None) -> None:
        self._mediator.notify(self, event, data)


class Player(GameObject):
    def __init__(self, mediator: GameMediator, hp: int = 100) -> None:
        super().__init__("Player", mediator)
        self.hp = hp
        self.score = 0

    def take_damage(self, amount: int) -> None:
        self.hp = max(0, self.hp - amount)
        self.emit("player:damage", {"hp": self.hp, "damage": amount})
        if self.hp == 0:
            self.emit("player:dead")

    def add_score(self, points: int) -> None:
        self.score += points
        self.emit("player:score", {"score": self.score})


class Enemy(GameObject):
    def __init__(self, name: str, mediator: GameMediator,
                 hp: int = 30) -> None:
        super().__init__(name, mediator)
        self.hp = hp
        self.alive = True

    def take_damage(self, amount: int) -> None:
        self.hp = max(0, self.hp - amount)
        if self.hp == 0 and self.alive:
            self.alive = False
            self.emit("enemy:killed", {"name": self.name})


class UISystem(GameObject):
    def __init__(self, mediator: GameMediator) -> None:
        super().__init__("UI", mediator)
        self.hp_bar = 100
        self.score = 0

    def update_hp(self, hp: int) -> None:
        self.hp_bar = hp
        print(f"[UI] HP: {hp}")

    def update_score(self, score: int) -> None:
        self.score = score
        print(f"[UI] Score: {score}")

    def show_game_over(self) -> None:
        print("[UI] GAME OVER")

    def show_victory(self) -> None:
        print("[UI] YOU WIN!")


class AudioSystem(GameObject):
    def __init__(self, mediator: GameMediator) -> None:
        super().__init__("Audio", mediator)

    def play(self, sound: str) -> None:
        print(f"[Audio] Playing: {sound}")


class GameLevel(GameMediator):
    def __init__(self, enemies_to_kill: int = 3) -> None:
        self.enemies_to_kill = enemies_to_kill
        self.killed = 0
        # TODO: создать player, ui, audio; список врагов создаётся снаружи

    def notify(self, sender: GameObject, event: str,
               data: Any = None) -> None:
        # TODO:
        # player:damage  → ui.update_hp, audio.play("hit.wav")
        # player:dead    → audio.play("gameover.wav"), ui.show_game_over
        # player:score   → ui.update_score
        # enemy:killed   → player.add_score(100), killed++,
        #                  проверить если killed == enemies_to_kill →
        #                  emit level:complete
        # level:complete → audio.play("victory.wav"), ui.show_victory
        pass


# Ожидаемый результат:
# level = GameLevel(enemies_to_kill=2)
# e1 = Enemy("Goblin", level)
# e2 = Enemy("Orc", level)
# level.player.take_damage(20)
# # [UI] HP: 80
# # [Audio] Playing: hit.wav
# e1.take_damage(30)
# # [UI] Score: 100
# # [Audio] Playing: kill.wav
# e2.take_damage(30)
# # [UI] Score: 200
# # [Audio] Playing: kill.wav
# # [Audio] Playing: victory.wav
# # [UI] YOU WIN!
<?php
interface GameMediator
{
    public function notify(GameObject $sender, string $event, mixed $data = null): void;
}

abstract class GameObject
{
    public function __construct(
        protected readonly string $name,
        protected readonly GameMediator $mediator,
    ) {}

    public function getName(): string { return $this->name; }

    protected function emit(string $event, mixed $data = null): void
    {
        $this->mediator->notify($this, $event, $data);
    }
}

class Player extends GameObject
{
    public int $hp = 100;
    public int $score = 0;

    public function __construct(GameMediator $mediator, int $hp = 100)
    {
        parent::__construct("Player", $mediator);
        $this->hp = $hp;
    }

    public function takeDamage(int $amount): void
    {
        $this->hp = max(0, $this->hp - $amount);
        $this->emit("player:damage", ["hp" => $this->hp, "damage" => $amount]);
        if ($this->hp === 0) {
            $this->emit("player:dead");
        }
    }

    public function addScore(int $points): void
    {
        $this->score += $points;
        $this->emit("player:score", ["score" => $this->score]);
    }
}

class Enemy extends GameObject
{
    public int $hp;
    public bool $alive = true;

    public function __construct(string $name, GameMediator $mediator, int $hp = 30)
    {
        parent::__construct($name, $mediator);
        $this->hp = $hp;
    }

    public function takeDamage(int $amount): void
    {
        $this->hp = max(0, $this->hp - $amount);
        if ($this->hp === 0 && $this->alive) {
            $this->alive = false;
            $this->emit("enemy:killed", ["name" => $this->name]);
        }
    }
}

class UISystem extends GameObject
{
    public int $hpBar = 100;
    public int $score = 0;

    public function __construct(GameMediator $mediator)
    {
        parent::__construct("UI", $mediator);
    }

    public function updateHp(int $hp): void { $this->hpBar = $hp; echo "[UI] HP: $hp\n"; }
    public function updateScore(int $score): void { $this->score = $score; echo "[UI] Score: $score\n"; }
    public function showGameOver(): void { echo "[UI] GAME OVER\n"; }
    public function showVictory(): void { echo "[UI] YOU WIN!\n"; }
}

class AudioSystem extends GameObject
{
    public function __construct(GameMediator $mediator)
    {
        parent::__construct("Audio", $mediator);
    }

    public function play(string $sound): void { echo "[Audio] Playing: $sound\n"; }
}

class GameLevel implements GameMediator
{
    public Player $player;
    public UISystem $ui;
    public AudioSystem $audio;
    private int $killed = 0;

    public function __construct(private readonly int $enemiesToKill = 3)
    {
        // TODO: создать player, ui, audio с $this как посредником
    }

    public function notify(GameObject $sender, string $event, mixed $data = null): void
    {
        // TODO:
        // player:damage  → ui->updateHp, audio->play("hit.wav")
        // player:dead    → audio->play("gameover.wav"), ui->showGameOver
        // player:score   → ui->updateScore
        // enemy:killed   → player->addScore(100), killed++,
        //                  если killed == enemiesToKill → emit level:complete
        // level:complete → audio->play("victory.wav"), ui->showVictory
    }
}

// Ожидаемый результат:
// $level = new GameLevel(enemiesToKill: 2);
// $e1 = new Enemy("Goblin", $level);
// $e2 = new Enemy("Orc", $level);
// $level->player->takeDamage(20);
// // [UI] HP: 80
// // [Audio] Playing: hit.wav
// $e1->takeDamage(30);
// // [UI] Score: 100
// // [Audio] Playing: kill.wav
// $e2->takeDamage(30);
// // [UI] Score: 200
// // [Audio] Playing: kill.wav
// // [Audio] Playing: victory.wav
// // [UI] YOU WIN!