PHP: Посредник

Mediator Посредник Mediator

Посредник — это поведенческий паттерн, который упрощает коммуникацию между компонентами системы.

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

Подробней о Посреднике

Особенности паттерна в PHP

Сложность:

Популярность:

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

Тем не менее, примерами паттерна могут служить EventDispatcher-ы многих фреймворков, а также некоторые реализации контроллеров в MVC фреймворках.

Пример: Структура паттерна

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

MediatorStructural.php: Пример структуры паттерна

<?php

namespace RefactoringGuru\Mediator\Structural;

/**
 * Интерфейс Посредника предоставляет метод, используемый компонентами для
 * уведомления посредника о различных событиях. Посредник может реагировать на
 * эти события  и передавать исполнение другим компонентам.
 */
interface Mediator
{
    public function notify($sender, $event);
}

/**
 * Конкретные Посредники реализуют совместное поведение, координируя отдельные
 * компоненты.
 */
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($sender, $event)
    {
        if ($event == "A") {
            print("Mediator reacts on A and triggers following operations:\n");
            $this->component2->doC();
        }

        if ($event == "D") {
            print("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;
    }

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

/**
 * Конкретные Компоненты реализуют различную функциональность. Они не зависят от
 * других компонентов. Они также не зависят от каких-либо конкретных классов
 * посредников.
 */
class Component1 extends BaseComponent
{
    public function doA()
    {
        print("Component 1 does A.\n");
        $this->mediator->notify($this, "A");
    }

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

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

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

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

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

print("\n");
print("Client triggers operation D.\n");
$c2->doD();

Output.txt: Результат выполнения

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.

Пример: Пример из жизни

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

MediatorRealWorld.php: Пример из жизни

<?php

namespace RefactoringGuru\Mediator\RealWorld;

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

/**
 * Класс Диспетчера Событий выполняет функции Посредника и содержит логику
 * подписки и уведомлений. Хотя классический Посредник часто зависит от
 * конкретных классов компонентов, этот привязан только к их абстрактным
 * интерфейсам.
 *
 * Достичь слабой связанности между компонентами можно благодаря особому способу
 * установления связей между ними. Компоненты сами могут подписаться на
 * интересующие их конкретные события через интерфейс подписки Посредника.
 *
 * Обратите внимание, что мы не можем использовать здесь встроенные в PHP
 * интерфейсы  Subject/Observer, так как они не дадут нам реализовать
 * расширенные методы подписки и оповещений.
 */
class EventDispatcher
{
    /**
     * @var array
     */
    private $observers = [];

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

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

    private function getEventObservers(string $event = "*")
    {
        $this->initEventGroup($event);
        $group = $this->observers[$event];
        $all = $this->observers["*"];

        return array_merge($group, $all);
    }

    public function attach(Observer $observer, string $event = "*")
    {
        $this->initEventGroup($event);

        $this->observers[$event][] = $observer;
    }

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

    public function trigger(string $event, object $emitter, $data = null)
    {
        print("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)
    {
        switch ($event) {
            case "users:deleted":
                if ($emitter === $this) {
                    return;
                }
                $this->deleteUser($data, true);
                break;
        }
    }

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

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

    public function createUser(array $data, $silent = false)
    {
        print("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, $silent = false)
    {
        print("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, $silent = false)
    {
        print("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)
    {
        $this->attributes = array_merge($this->attributes, $data);
    }

    /**
     * Все объекты могут вызывать события.
     */
    public function delete()
    {
        print("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);

        print("Logger: I've written '$event' entry to the log.\n");
    }
}

/**
 * Этот Конкретный Компонент отправляет начальные инструкции новым
 * пользователям. Клиент несёт ответственность за присоединение этого компонента
 * к соответствующему событию создания пользователя.
 */
class OnboardingNotification implements Observer
{
    private $adminEmail;

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

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

        print("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();

Output.txt: Результат выполнения

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.