Le Médiateur est un patron de conception comportemental qui diminue le couplage entre les composants d’un programme, en les faisant communiquer indirectement, via un objet médiateur spécial.
Le médiateur rend la modification, l’extension et la réutilisation de composants individuels aisées, car ils ne dépendent plus d’une dizaine de classes.
Cependant, le médiateur trouve son utilité dans le répartiteur d’événements de nombreux frameworks PHP ou dans certaines implémentations du contrôleur MVC, entre autres.
Exemple conceptuel
Dans cet exemple, nous allons voir la structure du Médiateur et répondre aux questions suivantes :
Que contiennent les classes ?
Quels rôles jouent-elles ?
Comment les éléments du patron sont-ils reliés ?
Après avoir étudié la structure du patron, vous pourrez plus facilement comprendre l’exemple suivant qui est basé sur un cas d’utilisation réel en PHP.
index.php: Exemple conceptuel
<?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: Résultat de l’exécution
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.
Analogie du monde réel
Dans cet exemple, le Médiateur étend l’idée du patron de conception observateur en fournissant un répartiteur d’événements centralisé. Il permet à un objet de suivre et de déclencher des événements chez d’autres objets sans dépendre de leurs classes.
index.php: Exemple du monde réel
<?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: Résultat de l’exécution
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.