Посредник — это поведенческий паттерн, который упрощает коммуникацию между компонентами системы.
Посредник убирает прямые связи между отдельными компонентами, заставляя их общаться друг с другом через себя.
Тем не менее, примерами паттерна могут служить EventDispatcher-ы многих фреймворков, а также некоторые реализации контроллеров в MVC фреймворках.
Концептуальный пример
Этот пример показывает структуру паттерна Посредник , а именно — из каких классов он состоит, какие роли эти классы выполняют и как они взаимодействуют друг с другом.
После ознакомления со структурой, вам будет легче воспринимать второй пример, который рассматривает реальный случай использования паттерна в мире PHP.
index.php: Пример структуры паттерна
<?php
namespace RefactoringGuru\Mediator\Conceptual;
/**
* Интерфейс Посредника предоставляет метод, используемый компонентами для
* уведомления посредника о различных событиях. Посредник может реагировать на
* эти события и передавать исполнение другим компонентам.
*/
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();
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.
Пример из реальной жизни
В этом примере паттерн Посредник расширяет базовую идею паттерна Наблюдатель , предоставляя централизованный диспетчер событий. Он позволяет любому объекту отслеживать и запускать события в других объектах, независимо от их классов.
index.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 = "*"): 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();
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.