Mediator es un patrón de diseño de comportamiento que reduce el acoplamiento entre los componentes de un programa haciendo que se comuniquen indirectamente a través de un objeto mediador especial.
El patrón Mediator facilita la modificación, extensión y reutilización de componentes individuales porque ya no son dependientes de todas las demás clases.
Sin embargo, sigue habiendo usos para el patrón Mediator, como los despachadores de eventos de muchos frameworks PHP, o algunas implementaciones de controladores MVC.
Ejemplo conceptual
Este ejemplo ilustra la estructura del patrón de diseño Mediator y se centra en las siguientes preguntas:
¿De qué clases se compone?
¿Qué papeles juegan esas clases?
¿De qué forma se relacionan los elementos del patrón?
Después de conocer la estructura del patrón, será más fácil comprender el siguiente ejemplo basado en un caso de uso real de PHP.
index.php: Ejemplo conceptual
<?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: Resultado de la ejecución
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.
Ejemplo del mundo real
En este ejemplo, el patrón Mediator amplía la idea del patrón Observer proporcionando un despachador de eventos centralizado. Permite a cualquier objeto rastrear y disparar eventos en otros objetos sin depender de sus clases.
index.php: Ejemplo del mundo real
<?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: Resultado de la ejecución
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.