Le design pattern chaîne de responsabilité

Effectuer plusieurs actions indépendantes sur un objet.

La chaîne de responsabilité (chain-of-responsibility) est un design pattern qui permet de faire passer une entité (objet, tableau etc...) à travers une série de fonctions qui vont chacune réaliser une action dessus. Il existe plusieurs variantes de la chaîne de responsabilité et nous allons en voir 4.

Exemple 1 : un système de logs

Dans le premier exemple, chaque maillon de la chaîne connait le maillon suivant et l'appelle lui-même, la chaîne n'est jamais brisée, l'entité la traverse intégralement.

Pour illustrer cet exemple, nous allons développer un loggueur. En fonction du niveau de sévérité du message d'erreur, le loggueur doit écrire dans un fichier, envoyer un mail, requêter un syslog etc... Tous les clients de notre loggueur ne sont pas obligés d'adopter la même stratégie de logs, les différentes actions doivent donc être séparées dans des sous-loggueurs et pouvoir être branchées à la volée.

Chaîne de responsabilité

abstract class Logger { protected $nextLogger = null; /* Les niveaux disponibles de sévérité de l'erreur */ const DEBUG = 0; const ERROR = 1; /* Le niveau par défaut à partir duquel l'erreur sera prise en compte */ abstract protected function getDefaultSeverityLevel(); /* * Ajoute un loggueur à la fin de la chaine. */ final public function addLogger(Logger $logger) { if (!$this->nextLogger) { $this->nextLogger = $logger; } else { $this->nextLogger->addLogger($logger); } } final public function log($message, $severity) { /* Si le logger doit prendre en compte ce message, * on lance la véritable action qui manipulera le message : * envoie de mail, écriture dans un fichier etc... * * Note : dans le pattern chaine de responsabilité, la vérification pour savoir si un maillon de la chaîne * doit prendre en compte l'entité entrante se fait traditionnellement dans la méthode handleMessage * du maillon lui même. Comme ici tous les maillons sont du même type et fonctionnent de la * même manière, nous pouvons factoriser ce code. */ if ($severity >= $this->getDefaultSeverityLevel()) { $this->handleMessage($message, $severity); } /* S'il y a un autre loggueur inscrit dans la chaine, il est lancé */ if ($this->nextLogger) { $this->nextLogger->log($message, $severity); } } abstract protected function handleMessage($message, $severity); } /* Les maillons qui constitueront la chaine */ class FileLogger extends Logger { protected function getDefaultSeverityLevel() { return self::DEBUG; } protected function handleMessage($message, $severity) { // ... $fp = fopen('tuto2.dev', 'w'); fwrite($fp, $message); echo "je viens d'écrire dans un fichier".PHP_EOL; // ... } } class MailLogger extends Logger { protected function getDefaultSeverityLevel() { return self::ERROR; } protected function handleMessage($message, $severity) { // ... mail('adresse@domaine.com', 'Une erreur est survenue', $message); echo "je viens d'envoyer un mail".PHP_EOL; // ... } } /* Construction de la chaine */ $logger = new FileLogger(); $logger->addLogger(new MailLogger()); /* Lancement du premier maillon */ $logger->log('un message de debug', Logger::DEBUG); /* Sortie : * je viens d'écrire dans un fichier */ $logger->log("un message d'erreur", Logger::ERROR); /* Sortie : * je viens d'écrire dans un fichier * je viens d'envoyer un mail */

Exemple 2 : traiter une nouvelle requête

Le second exemple décrira un système similaire au premier exemple sauf que la chaîne de responsabilité se terminera dès qu'un maillon a réussi à traiter l'entité.

Pour illustrer cet exemple, nous allons simuler une demande d'un service marketing vers une équipe de développeurs. Certains développeurs ne veulent / peuvent pas traiter la demande et la redirige vers l'un de leur collègue. Dès qu'un développeur traite la demande, la chaîne s'arrête.

Chaîne de responsabilité qui s'arrête

abstract class Developpeur { protected $autreDeveloppeur = null; /* * Ajoute un développeur sur lequel envoyer la demande. */ final public function ajouterUnAutreDeveloppeur(Developpeur $dev) { if (!$this->autreDeveloppeur) { $this->autreDeveloppeur = $dev; } else { $this->autreDeveloppeur->ajouterUnAutreDeveloppeur($dev); } } final public function luiFaireUneDemande($demande) { $laDemandeAEteTraitee = $this->traiterDemande($demande); if (!$laDemandeAEteTraitee AND $this->autreDeveloppeur) { $laDemandeAEteTraitee = $this->autreDeveloppeur->luiFaireUneDemande($demande); } return $laDemandeAEteTraitee; } abstract protected function traiterDemande($demande); } class Nicolas extends Developpeur { protected function traiterDemande($demande) { echo "Nicolas : désolé je ne m'occupe plus de ça, je transmets la demande.".PHP_EOL; return false; } } class Igor extends Developpeur { protected function traiterDemande($demande) { echo "Igor : J'ai pas le temps, je transmets la demande.".PHP_EOL; return false; } } class Marcio extends Developpeur { protected function traiterDemande($demande) { echo "Marcio : ok je vais le faire.".PHP_EOL; return true; } } class Maher extends Developpeur { protected function traiterDemande($demande) { echo "Maher : oui bien sûr j'ai rien d'autre à faire.".PHP_EOL; return true; } } /* Création de la chaîne des développeurs */ $nicolas = new Nicolas(); $nicolas->ajouterUnAutreDeveloppeur(new Igor()); $nicolas->ajouterUnAutreDeveloppeur(new Marcio()); $nicolas->ajouterUnAutreDeveloppeur(new Maher()); $nicolas->luiFaireUneDemande("Marketing : il faut absolument avoir fait blablabla d'ici demain, c'est très impactant pour le C.A"); /* Sortie : * Nicolas : désolé je ne m'occupe plus de ça, je transmets la demande. * Igor : J'ai pas le temps, je transmets la demande. * Marcio : ok je vais le faire. */

Cet exemple démontre clairement certains des points négatifs du design pattern chaîne de responsabilité :

  • Les premiers maillons de la chaîne peuvent se retrouver sur-exploités alors que les derniers maillons ne sont jamais appelés.
  • Si la chaîne est trop longue, la requête peut mettre beaucoup de temps avant d'être traitée.

Exemple 3 : un système de filtrage

Dans les deux derniers exemples, nous allons utiliser un manager pour assembler la chaîne : les maillons ne se connaîtront pas entre eux.

Pour illustrer cet exemple, nous allons développer un système qui permet de filtrer une liste de nombres. Le client indique au manager les filtres qu'il souhaite appliquer. Le manager se chargera de les lancer l'un après l'autre.

Chaîne de responsabilité avec un manager

abstract class Filter { abstract public function execute(array $toFilter); } class RemoveMultipleOf3 extends Filter { public function execute(array $toFilter) { /* Par exemple : 9 % 3 = 0, * la callback retourne false, l'élément est supprimé du tableau */ return array_filter($toFilter, function($entry){ return (bool)($entry % 3);}); } } class RemoveMultipleOf4 extends Filter { public function execute(array $toFilter) { return array_filter($toFilter, function($entry){ return (bool)($entry % 4);}); } } class FilterManager { protected $filters = array(); public function addFilter(Filter $filter) { $this->filters[] = $filter; return $this; } public function execute(array $toFilter) { foreach($this->filters AS $filter) { $toFilter = $filter->execute($toFilter); } return $toFilter; } } $filterManager = new FilterManager(); $filterManager->addFilter(new RemoveMultipleOf3()) ->addFilter(new RemoveMultipleOf4()); $filtered = $filterManager->execute(array(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13)); print_r($filtered); /* Sortie : * Array * ( * [0] => 1 * [1] => 2 * [4] => 5 * [6] => 7 * [9] => 10 * [10] => 11 * [12] => 13 * ) */

Exemple 4 : un système de chiffrement

Jusqu'à présent, tous les maillons de la chaine étaient des objets. Dans l'exemple 3, ces objets n'étaient composés que d'une méthode, que l'on peut donc considérer comme étant elle-même le maillon. Dans une vision plus large de la chaîne de responsabilité, rien n'empêche que les maillons soient d'autres types de fonctions : fonctions anonymes, fonctions natives PHP, fonctions globales etc... La chaîne pourrait être composée de types de maillons différents, qui seraient totalement indépendant entre eux.

Chaîne de responsabilité avec différents types de maillons

class EncryptManager { protected $algos = array(); public function addAlgo($name, $algo) { $this->algos[$name] = $algo; return $this; } public function encrypt($input) { foreach($this->algos AS $name => $algo) { if (is_callable($algo)) { $input = $algo($input); } } return $input; } } $encryptManager = new EncryptManager(); $encryptManager->addAlgo( 'aes', function ($input) { $algo = MCRYPT_RIJNDAEL_256; $mode = MCRYPT_MODE_CBC; $key = 'cle_secrete'; $ivSize = mcrypt_get_iv_size($algo, $mode); $iv = mcrypt_create_iv($ivSize, MCRYPT_RAND); return mcrypt_encrypt($algo, $key, $input, $mode, $iv); } ); $encryptManager->addAlgo('base64', 'base64_encode') ->addAlgo('url', 'urlencode'); $encrypted = $encryptManager->encrypt("tuto sur le design pattern chaîne de responsabilité"); var_dump($encrypted); /* Sortie : * string(102) "W7mVJ%2B1xNyODnZ0Av2%2BiFyb%2FZ9fzwLSZIJH%2FhCuzwGnyrsXCq41HJ9KZXZ25NVsp7kdYKWbu25OdEEg%2FHWhGXA%3D%3D" */