La Chaîne de responsabilité est un patron de conception comportemental qui permet de faire circuler une demande tout au long d’une chaîne de handlers, jusqu’à ce que l’un d’entre eux la traite.
Ce patron permet à plusieurs objets de traiter une demande sans coupler la classe du demandeur aux classes concrètes des récepteurs. La chaîne peut être assemblée dynamiquement à l’exécution à l’aide de tout handler implémentant l’interface standard des handlers.
Exemple conceptuel
Dans cet exemple, nous allons voir la structure de la Chaîne de responsabilité 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 qui est basé sur un cas d’utilisation réel en PHP.
index.php: Exemple conceptuel
<?php
namespace RefactoringGuru\ChainOfResponsibility\Conceptual;
/**
* The Handler interface declares a method for building the chain of handlers.
* It also declares a method for executing a request.
*/
interface Handler
{
public function setNext(Handler $handler): Handler;
public function handle(string $request): ?string;
}
/**
* The default chaining behavior can be implemented inside a base handler class.
*/
abstract class AbstractHandler implements Handler
{
/**
* @var Handler
*/
private $nextHandler;
public function setNext(Handler $handler): Handler
{
$this->nextHandler = $handler;
// Returning a handler from here will let us link handlers in a
// convenient way like this:
// $monkey->setNext($squirrel)->setNext($dog);
return $handler;
}
public function handle(string $request): ?string
{
if ($this->nextHandler) {
return $this->nextHandler->handle($request);
}
return null;
}
}
/**
* All Concrete Handlers either handle a request or pass it to the next handler
* in the chain.
*/
class MonkeyHandler extends AbstractHandler
{
public function handle(string $request): ?string
{
if ($request === "Banana") {
return "Monkey: I'll eat the " . $request . ".\n";
} else {
return parent::handle($request);
}
}
}
class SquirrelHandler extends AbstractHandler
{
public function handle(string $request): ?string
{
if ($request === "Nut") {
return "Squirrel: I'll eat the " . $request . ".\n";
} else {
return parent::handle($request);
}
}
}
class DogHandler extends AbstractHandler
{
public function handle(string $request): ?string
{
if ($request === "MeatBall") {
return "Dog: I'll eat the " . $request . ".\n";
} else {
return parent::handle($request);
}
}
}
/**
* The client code is usually suited to work with a single handler. In most
* cases, it is not even aware that the handler is part of a chain.
*/
function clientCode(Handler $handler)
{
foreach (["Nut", "Banana", "Cup of coffee"] as $food) {
echo "Client: Who wants a " . $food . "?\n";
$result = $handler->handle($food);
if ($result) {
echo " " . $result;
} else {
echo " " . $food . " was left untouched.\n";
}
}
}
/**
* The other part of the client code constructs the actual chain.
*/
$monkey = new MonkeyHandler();
$squirrel = new SquirrelHandler();
$dog = new DogHandler();
$monkey->setNext($squirrel)->setNext($dog);
/**
* The client should be able to send a request to any handler, not just the
* first one in the chain.
*/
echo "Chain: Monkey > Squirrel > Dog\n\n";
clientCode($monkey);
echo "\n";
echo "Subchain: Squirrel > Dog\n\n";
clientCode($squirrel);
Output.txt: Résultat de l’exécution
Chain: Monkey > Squirrel > Dog
Client: Who wants a Nut?
Squirrel: I'll eat the Nut.
Client: Who wants a Banana?
Monkey: I'll eat the Banana.
Client: Who wants a Cup of coffee?
Cup of coffee was left untouched.
Subchain: Squirrel > Dog
Client: Who wants a Nut?
Squirrel: I'll eat the Nut.
Client: Who wants a Banana?
Banana was left untouched.
Client: Who wants a Cup of coffee?
Cup of coffee was left untouched.
Analogie du monde réel
L’exemple le plus connu pour le patron Chaîne de responsabilité (CoR) peut être trouvé dans le HTTP request middleware. Ce dernier se retrouve dans les frameworks PHP les plus populaires et est même devenu standard dans la PSR-15.
Son fonctionnement est le suivant : une requête HTTP passe dans une pile d’objets middleware afin d’être prise en charge dans l’application. Chaque middleware peut soit rejeter la requête et la faire sortir de la chaîne, soit la passer au prochain middleware. Une fois que la requête est passée par tous les middlewares, le handler principal de l’application peut finalement la traiter.
Vous avez sans doute remarqué que c’est un peu l’inverse de l’intention d’origine du patron. En effet, dans l’implémentation normale, une requête continue la chaîne si le handler actuel ne peut PAS la traiter. Dans le cas d’un middleware, la requête continue plus loin dans la chaîne si le handler pense que l’application PEUT la traiter. Mais comme les objets middleware sont chaînés, ce concept est tout de même considéré comme un exemple du patron de conception chaîne de responsabilité.
index.php: Exemple du monde réel
<?php
namespace RefactoringGuru\ChainOfResponsibility\RealWorld;
/**
* The classic CoR pattern declares a single role for objects that make up a
* chain, which is a Handler. In our example, let's differentiate between
* middleware and a final application's handler, which is executed when a
* request gets through all the middleware objects.
*
* The base Middleware class declares an interface for linking middleware
* objects into a chain.
*/
abstract class Middleware
{
/**
* @var Middleware
*/
private $next;
/**
* This method can be used to build a chain of middleware objects.
*/
public function linkWith(Middleware $next): Middleware
{
$this->next = $next;
return $next;
}
/**
* Subclasses must override this method to provide their own checks. A
* subclass can fall back to the parent implementation if it can't process a
* request.
*/
public function check(string $email, string $password): bool
{
if (!$this->next) {
return true;
}
return $this->next->check($email, $password);
}
}
/**
* This Concrete Middleware checks whether a user with given credentials exists.
*/
class UserExistsMiddleware extends Middleware
{
private $server;
public function __construct(Server $server)
{
$this->server = $server;
}
public function check(string $email, string $password): bool
{
if (!$this->server->hasEmail($email)) {
echo "UserExistsMiddleware: This email is not registered!\n";
return false;
}
if (!$this->server->isValidPassword($email, $password)) {
echo "UserExistsMiddleware: Wrong password!\n";
return false;
}
return parent::check($email, $password);
}
}
/**
* This Concrete Middleware checks whether a user associated with the request
* has sufficient permissions.
*/
class RoleCheckMiddleware extends Middleware
{
public function check(string $email, string $password): bool
{
if ($email === "admin@example.com") {
echo "RoleCheckMiddleware: Hello, admin!\n";
return true;
}
echo "RoleCheckMiddleware: Hello, user!\n";
return parent::check($email, $password);
}
}
/**
* This Concrete Middleware checks whether there are too many failed login
* requests.
*/
class ThrottlingMiddleware extends Middleware
{
private $requestPerMinute;
private $request;
private $currentTime;
public function __construct(int $requestPerMinute)
{
$this->requestPerMinute = $requestPerMinute;
$this->currentTime = time();
}
/**
* Please, note that the parent::check call can be inserted both at the
* beginning of this method and at the end.
*
* This gives much more flexibility than a simple loop over all middleware
* objects. For instance, a middleware can change the order of checks by
* running its check after all the others.
*/
public function check(string $email, string $password): bool
{
if (time() > $this->currentTime + 60) {
$this->request = 0;
$this->currentTime = time();
}
$this->request++;
if ($this->request > $this->requestPerMinute) {
echo "ThrottlingMiddleware: Request limit exceeded!\n";
die();
}
return parent::check($email, $password);
}
}
/**
* This is an application's class that acts as a real handler. The Server class
* uses the CoR pattern to execute a set of various authentication middleware
* before launching some business logic associated with a request.
*/
class Server
{
private $users = [];
/**
* @var Middleware
*/
private $middleware;
/**
* The client can configure the server with a chain of middleware objects.
*/
public function setMiddleware(Middleware $middleware): void
{
$this->middleware = $middleware;
}
/**
* The server gets the email and password from the client and sends the
* authorization request to the middleware.
*/
public function logIn(string $email, string $password): bool
{
if ($this->middleware->check($email, $password)) {
echo "Server: Authorization has been successful!\n";
// Do something useful for authorized users.
return true;
}
return false;
}
public function register(string $email, string $password): void
{
$this->users[$email] = $password;
}
public function hasEmail(string $email): bool
{
return isset($this->users[$email]);
}
public function isValidPassword(string $email, string $password): bool
{
return $this->users[$email] === $password;
}
}
/**
* The client code.
*/
$server = new Server();
$server->register("admin@example.com", "admin_pass");
$server->register("user@example.com", "user_pass");
// All middleware are chained. The client can build various configurations of
// chains depending on its needs.
$middleware = new ThrottlingMiddleware(2);
$middleware
->linkWith(new UserExistsMiddleware($server))
->linkWith(new RoleCheckMiddleware());
// The server gets a chain from the client code.
$server->setMiddleware($middleware);
// ...
do {
echo "\nEnter your email:\n";
$email = readline();
echo "Enter your password:\n";
$password = readline();
$success = $server->logIn($email, $password);
} while (!$success);
Output.txt: Résultat de l’exécution
Enter your email:
asd
Enter your password:
123
UserExistsMiddleware: This email is not registered!
Enter your email:
admin@example.com
Enter your password:
wrong
UserExistsMiddleware: Wrong password!
Enter your email:
admin@example.com
Enter your password:
letmein
ThrottlingMiddleware: Request limit exceeded!
Enter your email:
admin@example.com
Enter your password:
admin_pass
RoleCheckMiddleware: Hello, admin!
Server: Authorization has been successful!