Accueil / Tutoriels / Chain of responsibility

Chain of responsibility

Publié le 3 Mars 2025
Conception

Chain of responsibility (chaîne de responsabilité en français) est un design pattern comportemental qui permet de faire circuler une requête à travers une chaîne d’objets.

Le principe

La requête et les traitements qui peuvent s’appliquer dessus sont complètement décorrélés. Chaque objet de la chaîne (qu’on appelle par convention des handlers) est indépendant et possède une référence vers le prochain handler de la chaîne. Lorsqu’une requête arrive au niveau d’un handler, le handler à la possibilité d’appliquer un traitement sur la requête, d’ignorer la requête pour la transmettre au handler suivant ou de stopper la transmission.

On se retrouve ainsi avec un code très modulaire et très évolutif. En fonction de nos besoins, on peut ordonner les handlers comme on le souhaite, ajouter ou supprimer des handlers très facilement et avoir un code très adaptable.

Exemples d’utilisations

Les requêtes HTTP

Les middlewares sont un exemple typique d’utilisation de ce pattern. Chaque middleware (handler) va examiner la requête et procéder à une vérification ou un traitement particulier (origine de la requête, méthode utilisé pour la requête (GET, POST, PATCH…), vérifier les autorisations (clé API, JWT, login/mot de passe), procéder à l’écriture dans un fichier de logs.

Validation des formulaires

Lors d’une inscription, vérifier si le format d’une adresse mail est valide par exemple avant de poursuivre le traitement, si un numéro de téléphone est au bon format, etc…

Points d’attentions

Le principe d'une chaîne de responsabilité est de faire passer la requête à travers plusieurs objets. Il existe plusieurs issues possibles pour notre requête. Le débogage peut par conséquent être difficile dans certains cas.

Attention à ne pas mettre trop de handlers dans sa chaîne. Cela peut compliquer la gestion des erreurs et avoir un impact sur la performance.

La modularité que l’on obtient avec les handlers peut aussi devenir un inconvénient majeur. L’ordre d’exécution des handlers peut dans certains cas avoir une importance dans notre programme. Lors d’une mise à jour ou de l’ajout d’un nouveau handler par exemple, il est très facile d’introduire des bugs ou des régressions. Ne pas négliger l’ajout de tests unitaires et fonctionnels !

Implémenter une chaîne de responsabilité

En général pour mettre en place ce pattern, on adopte les conventions suivantes : Créer une interface (ou classe abstraite) nommée handler par exemple. L’interface définit la signature d’une méthode qui renseignera le handler suivant de la chaîne setNext() et la signature d’une méthode dédiée au traitement de la requête handle().

Créer nos handlers qui implémentent notre interface / hérite de la classe abstraite.

Exemple d'utilisation de Chain of responsibility en Go

Voici un exemple en Go pour gérer un formulaire d’inscription.

On représente ici les données d'inscriptions :


package data

type SignUpRequest struct {
	Username string
	Email    string
	Password string
}

Go

Notre interface Handler - Avec les signatures de SetNext() et Handle(). On ajoute une implémentation de base qui servira à tous nos handlers. La méthode Handle() sera directement implémenté au niveau des handlers.


package handler

import (
	"fmt"
)

type Handler interface {
	SetNext(handler Handler)
	Handle(request *data.SignUpRequest)
}


type BaseHandler struct {
	next Handler
}

func (h *BaseHandler) SetNext(handler Handler) {
	h.next = handler
}

func (h *BaseHandler) HandleNext(request *data.SignUpRequest) {
	if h.next != nil {
		h.next.Handle(request)
	} else {
		fmt.Println("Fin de la chaîne de handlers")
		// Définir ici un traitement pour la fin de la chaîne
	}
}

Go

On met en place notre handler pour gérer l'email renseigné dans le formulaire.


package handler

import (
	"fmt"
	"slices"
)

type CheckDuplicateEmailHandler struct {
	BaseHandler
	// On simule une base d'utilisateurs existants.
	existingUsers [ ]string
}

func (h *CheckDuplicateEmailHandler) Handle(request *data.SignUpRequest) {
	// Utilisateurs existants.
	h.existingUsers = [ ]string{"", "", ""}

	if slices.Contains(h.existingUsers, request.Email) {
		// Gérer le cas où l'email existe déjà
		fmt.Println("Erreur lors de l'inscripion...")
		return
	}

	h.HandleNext(request)
}

// SendEmailHandler simule l'envoi d'un email de bienvenue.
type SendEmailHandler struct {
	BaseHandler
}

func (h *SendEmailHandler) Handle(request *data.SignUpRequest) {
	// Simulation de l'envoi d'un email
	fmt.Printf("Confirmation d'inscription envoyé à %s\n", request.Email)

	h.HandleNext(request)
}

Go

Un autre handler pour gérer le mots de passe renseigné.


package handler

import (
	"fmt"
)

type PasswordValidityHandler struct {
	BaseHandler
}

// Exemple pour vérifier la validité d'un mot de passe
func (h *PasswordValidityHandler) Handle(request *data.SignUpRequest) {
	if len(request.Password) < 6 {
		fmt.Println("Mot de passe trop court !")
		return
	}

	h.HandleNext(request)
}

Go

Notre fichier principal. On initialise nos handlers et on mets en place notre chaîne de responsabilité. J'ai mis quelques exemples pour pour faire intervenir ou non nos handlers.


package main

import (
	"fmt"
	"training.go/designpatterns/chain-of-responsibility/data"
	"training.go/designpatterns/chain-of-responsibility/handler"
)

func main() {
	emailChecker := &handler.CheckDuplicateEmailHandler{}
	passwordChecker := &handler.PasswordValidityHandler{}
	emailSender := &handler.SendEmailHandler{}

	// Mise en place d'une chaîne de responsabilité
	// emailChecker -> passwordChecker -> emailSender
	emailChecker.SetNext(passwordChecker)
	passwordChecker.SetNext(emailSender)

	// Cas 1 : Inscription OK
	signUp1 := &data.SignUpRequest{
		Username: "Robert",
		Email:    "",
		Password: "tototiti",
	}

	fmt.Println("******* Cas 1 *******")
	emailChecker.Handle(signUp1)
	// ******* Cas 1 *******
	// Confirmation d'inscription envoyé à 
	// Fin de la chaîne de handlers

	// Cas 2 : Mot de passe trop court
	signUp2 := &data.SignUpRequest{
		Username: "Manon",
		Email:    "",
		Password: "123",
	}

	fmt.Println("******* Cas 2 *******")
	emailChecker.Handle(signUp2)
	// ******* Cas 2 *******
	// Mot de passe trop court !

	// Cas 3 : Mail non valide
	signUp3 := &data.SignUpRequest{
		Username: "Pierre",
		Email:    "",
		Password: "123456789",
	}

	fmt.Println("******* Cas 3 *******")
	emailChecker.Handle(signUp3)
	// ******* Cas 3 *******
	// Erreur lors de l'inscription...
}

Go

Le même exemple en PHP

Voici exactement le même exemple mais avec PHP :


namespace Practice\DesignPatterns\ChainOfResponsibility;

interface HandlerInterface
{
    public function setNext(HandlerInterface $handler): HandlerInterface;

    public function handle(SignUpRequest $request): void;
}

PHP

namespace Practice\DesignPatterns\ChainOfResponsibility;

abstract class BaseHandler implements HandlerInterface
{
    protected ?HandlerInterface $nextHandler = null;

    public function setNext(HandlerInterface $handler): HandlerInterface
    {
        $this->nextHandler = $handler;
        return $handler;
    }

    public function handle(SignUpRequest $request): void
    {
        if ($this->nextHandler !== null) {
            $this->nextHandler->handle($request);
        } else {
            echo "Processus d'inscription terminé avec succès.";
        }
    }
}

PHP

namespace Practice\DesignPatterns\ChainOfResponsibility;

class SignUpRequest
{
    public string $username;
    public string $email;
    public string $password;

    public function __construct($username, $email, $password)
    {
        $this->username = $username;
        $this->email    = $email;
        $this->password = $password;
    }
}

PHP

namespace Practice\DesignPatterns\ChainOfResponsibility;

class CheckDuplicateEmailHandler extends BaseHandler
{
    private array $emails = ["", "", ""];

    public function handle(SignUpRequest $request): void
    {
        if (in_array($request->email, $this->emails)) {
            // Gérer le cas où l'email existe déjà
            echo "Erreur lors de l'inscripion...";

            // Gérer l'erreur
            return;
        }

        parent::handle($request);
    }
}

PHP

namespace Practice\DesignPatterns\ChainOfResponsibility;

class CheckPasswordHandler extends BaseHandler
{
    public function handle(SignUpRequest $request): void
    {
        if (strlen($request->password) < 6) {
            // Gérer le cas où le mot de passe n'est pas au bon format
            echo "Mots de passe trop court ! ";

            // Gérer l'erreur
            return;
        }

        parent::handle($request);
    }
}

PHP

namespace Practice\DesignPatterns\ChainOfResponsibility;

class SendEmailHandler extends BaseHandler
{
    public function handle(SignUpRequest $request): void
    {
        echo "Confirmation d'inscription envoyé à " . $request->email . "";
        parent::handle($request);
    }
}

PHP

index.php


use Practice\DesignPatterns\ChainOfResponsibility\CheckDuplicateEmailHandler;
use Practice\DesignPatterns\ChainOfResponsibility\CheckPasswordHandler;
use Practice\DesignPatterns\ChainOfResponsibility\SendEmailHandler;
use Practice\DesignPatterns\ChainOfResponsibility\SignUpRequest;

$emailChecker = new CheckDuplicateEmailHandler();
$passwordChecker = new CheckPasswordHandler();
$emailSender = new SendEmailHandler();

$emailChecker->setNext($passwordChecker);
$passwordChecker->setNext($emailSender);

echo "CAS 1 ";
$signUpRequest = new SignUpRequest("Robert", "", "tototiti");
$emailChecker->handle($signUpRequest);

echo "CAS 2 ";
$signUpRequest2 = new SignUpRequest("Manon", "", "123");
$emailChecker->handle($signUpRequest2);

echo "CAS 3 ";
$signUpRequest3 = new SignUpRequest("Pierre", "", "123456789");
$emailChecker->handle($signUpRequest3);

PHP

Conclusion

J'espère que grâce à cet article vous êtes désormais plus à l'aise avec le design pattern Chain of responsibility. Vous pouvez retrouver mon article sur les design patterns ou les autres tutoriels sur Observer, Decorator, Iterator et Factory.