
Mediator を PHP で
Mediator は、 振る舞いに関するデザインパターンの一つで、 プログラムのコンポーネント間の通信を特別なメディエーター・オブジェクトを通して行うことで、 結合を疎にします。
Mediator により、 個々のコンポーネントは、 何十ものクラスへの依存がなくなるため、 変更、 拡張、 再利用が容易になります。
複雑度:
人気度:
使用例: 純粋な Mediator パターンの実装は、 Java や C# といった、 GUI 目的の言語に比べて、 PHP ではあまりみかけません。 PHP アプリケーションには、 十数個のコンポーネントがあるかもしれませんが、 それが一つのセッション中で直接通信することは稀です。
しかしそれでも、 多くの PHP フレームワークのイベント・ディスパッチャーや、 MVC コントローラーの実装のように、 Mediator パターンの使い道がないわけではありません。
概念的な例
この例は、 Mediator デザインパターンの構造を説明するためのものです。 以下の質問に答えることを目的としています:
- どういうクラスからできているか?
- それぞれのクラスの役割は?
- パターンの要素同士はどう関係しているのか?
ここでパターンの構造を学んだ後だと、 これに続く、 現実世界の PHP でのユースケースが理解しやすくなります。
index.php: 概念的な例
<?php namespace RefactoringGuru\Mediator\Conceptual; /** * The Mediator interface declares a method used by components to notify the * mediator about various events. The Mediator may react to these events and * pass the execution to other components. */ interface Mediator { public function notify(object $sender, string $event): void; } /** * Concrete Mediators implement cooperative behavior by coordinating several * components. */ 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(); } } } /** * The Base Component provides the basic functionality of storing a mediator's * instance inside component objects. */ class BaseComponent { protected $mediator; public function __construct(Mediator $mediator = null) { $this->mediator = $mediator; } public function setMediator(Mediator $mediator): void { $this->mediator = $mediator; } } /** * Concrete Components implement various functionality. They don't depend on * other components. They also don't depend on any concrete mediator classes. */ 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"); } } /** * The client code. */ $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.
現実的な例
この例では、 Mediator パターンが、 集中型イベント・ディスパッチャーによって、 Observer パターンのアイディアを拡張しています。 どんなオブジェクトでも、 他のオブジェクト内で、 クラスに依存することなく、 イベントを追跡、 トリガーできます。
index.php: 現実的な例
<?php namespace RefactoringGuru\Mediator\RealWorld; /** * The Event Dispatcher class acts as a Mediator and contains the subscription * and notification logic. While a classic Mediator often depends on concrete * component classes, this one is only tied to their abstract interfaces. * * We are able to achieve this level of indirection thanks to the way the * connections between components are established. The components themselves may * subscribe to specific events that they are interested in via the Mediator's * subscription interface. * * Note, we can't use the PHP's built-in Subject/Observer interfaces here * because we'll be stretching them too far from what they were designed for. */ class EventDispatcher { /** * @var array */ private $observers = []; public function __construct() { // The special event group for observers that want to listen to all // events. $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); } } } /** * A simple helper function to provide global access to the event dispatcher. */ function events(): EventDispatcher { static $eventDispatcher; if (!$eventDispatcher) { $eventDispatcher = new EventDispatcher(); } return $eventDispatcher; } /** * The Observer interface defines how components receive the event * notifications. */ interface Observer { public function update(string $event, object $emitter, $data = null); } /** * Unlike our Observer pattern example, this example makes the UserRepository * act as a regular component that doesn't have any special event-related * methods. Like any other component, this class relies on the EventDispatcher * to broadcast its events and listen for the other ones. * * @see \RefactoringGuru\Observer\RealWorld\UserRepository */ class UserRepository implements Observer { /** * @var array List of application's users. */ private $users = []; /** * Components can subscribe to events by themselves or by client code. */ public function __construct() { events()->attach($this, "users:deleted"); } /** * Components can decide whether they'd like to process an event using its * name, emitter or any contextual data passed along with the event. */ public function update(string $event, object $emitter, $data = null): void { switch ($event) { case "users:deleted": if ($emitter === $this) { return; } $this->deleteUser($data, true); break; } } // These methods represent the business logic of the class. 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); } } } /** * Let's keep the User class trivial since it's not the focus of our example. */ class User { public $attributes = []; public function update($data): void { $this->attributes = array_merge($this->attributes, $data); } /** * All objects can trigger events. */ public function delete(): void { echo "User: I can now delete myself without worrying about the repository.\n"; events()->trigger("users:deleted", $this, $this); } } /** * This Concrete Component logs any events it's subscribed to. */ 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"; } } /** * This Concrete Component sends initial instructions to new users. The client * is responsible for attaching this component to a proper user creation event. */ 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"; } } /** * The client code. */ $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.