Aller au contenu

Chain of Responsibility

Faire circuler une demande parmi plusieurs traitements

Introduction

Le design pattern Chain of Responsibility est un pattern de conception comportemental, dont l'objectif est de faire circuler une requête entre plusieurs objets susceptibles de la traiter. Chaque objet de la chaîne décide s'il peut prendre en charge la requête ou s'il doit la transmettre à l'objet suivant.

Lorsqu'une application doit appliquer un ou plusieurs traitements à une requête, le code client peut rapidement devenir dépendant de l'ensemble des traitements et de leur ordre d'exécution. L'ajout, la suppression ou la réorganisation d'un traitement nécessite alors de modifier le code.

Le design pattern Chain of Responsibility permet de déplacer cette responsabilité vers une chaîne d'objets indépendants. Le code client transmet la requête au premier objet de la chaîne sans avoir besoin de savoir quels objets de la chaîne la traiteront.

Problématique

Nous développons une application permettant de gérer des demandes de support. Une demande, de type SupportRequest, est caractérisée par une description et un niveau de difficulté de type SupportLevel :

public enum SupportLevel
{
    Basic = 1,
    Technical = 2,
    Expert = 3
}

public class SupportRequest
{
    public string Description { get; }

    public SupportLevel Level { get; }

    public SupportRequest(string description, SupportLevel level)
    {
        Description = description;
        Level = level;
    }
}

L'application dispose de plusieurs niveaux de support :

  • Le support de niveau 1 traite les demandes simples.
  • Le support de niveau 2 traite les demandes techniques.
  • Le support de niveau 3 traite les demandes nécessitant l'intervention d'un expert.

Une première approche pourrait consister à déterminer directement le traitement à exécuter :

public void Handle(SupportRequest request)
{
    if (request.Level == SupportLevel.Basic)
        Console.WriteLine("Traitement par le support de niveau 1.");

    else if (request.Level == SupportLevel.Technical)
        Console.WriteLine("Traitement par le support de niveau 2.");

    else if (request.Level == SupportLevel.Expert)
        Console.WriteLine("Traitement par le support de niveau 3.");
}

Cette implémentation est correcte, mais le code chargé de distribuer les demandes doit connaître tous les traitements disponibles, ainsi que les conditions permettant de sélectionner chacun d'entre eux. L'ajout d'un nouveau niveau de support nécessite également de modifier cette logique de distribution, ce qui pénalise la maintenance et l'évolutivité.

Mise en œuvre du pattern

Le design pattern Chain of Responsibility consiste à organiser plusieurs handlers (objets) sous la forme d'une chaîne. Chaque élément de cette chaîne possède une référence vers l'élément suivant : Chain of Responsibility - Mise en oeuvre Lorsqu'un handler reçoit une demande, il peut soit la traiter, soit la transmettre au handler suivant dans la chaîne. Le code client, à l'origine de la demande, n'a donc pas besoin de savoir quel objet réalisera effectivement son traitement.

Conception

Le diagramme de classes présente la structure mise en œuvre pour appliquer le design pattern Chain of Responsibility à notre gestionnaire de demandes :

Chain of Responsibility - Conception

Le design pattern Chain of Responsibility fait généralement intervenir plusieurs éléments :

  • Le Handler définit le contrat ou le comportement commun des différents traitements, et permet généralement de référencer le handler suivant dans la chaîne. La classe SupportHandler joue ce rôle.
  • Les Concrete Handlers représentent les traitements capables de prendre en charge certaines demandes. Il s'agit des classes Level1SupportHandler, Level2SupportHandler et Level3SupportHandler. Chacune de ces classes décide si elle peut prendre en charge la SupportRequest en fonction de son SupportLevel ou si elle doit la transmettre au handler suivant.
  • Le Client construit ou utilise la chaîne et transmet une demande à son premier élément.

Implémentation en C#

Implémentation des handlers

Commençons par implémenter une classe abstraite représentant un élément de la chaîne :

public abstract class SupportHandler
{
    private SupportHandler? _nextHandler;

    public SupportHandler SetNext(SupportHandler handler)
    {
        _nextHandler = handler;
        return handler;
    }

    public virtual void Handle(SupportRequest request)
    {
        _nextHandler?.Handle(request);
    }
}

Le champ _nextHandler permet à chaque handler de référencer le handler suivant dans la chaîne. La méthode SetNext permet de construire la chaîne et retourne le handler qui vient d’être ajouté afin de permettre le chaînage des appels. Par défaut, la méthode Handle transmet la demande au handler suivant, s’il existe. Nous pouvons maintenant implémenter le premier niveau de support :

public class Level1SupportHandler : SupportHandler
{
    public override void Handle(SupportRequest request)
    {
        if (request.Level == SupportLevel.Basic)
        {
            Console.WriteLine("Traitement par le support de niveau 1.");
            return;
        }

        base.Handle(request);
    }
}

Le traitement vérifie s'il peut prendre en charge la demande. Si ce n'est pas le cas, l'instruction base.Handle permet de la transmettre au prochain handler de la chaîne. Le support de deuxième niveau fonctionne selon le même principe :

public class Level2SupportHandler : SupportHandler
{
    public override void Handle(SupportRequest request)
    {
        if (request.Level == SupportLevel.Technical)
        {
            Console.WriteLine("Traitement par le support de niveau 2.");
            return;
        }

        base.Handle(request);
    }
}

Enfin, le troisième niveau prend en charge les demandes nécessitant l'intervention d'un expert :

public class Level3SupportHandler : SupportHandler
{
    public override void Handle(SupportRequest request)
    {
        if (request.Level == SupportLevel.Expert)
        {
            Console.WriteLine("Traitement par le support de niveau 3.");
            return;
        }

        base.Handle(request);
    }
}

Si aucun handler ne peut prendre en charge la demande, celle-ci atteint la fin de la chaîne sans être traitée. Selon les besoins de l’application, le dernier handler pourrait journaliser cette information, retourner un objet représentant le résultat du traitement (traitée avec succès, traitée avec erreur, non traitée) ou lever une exception.

Construction de la chaîne

Les différents traitements peuvent maintenant être assemblés :

var level1 = new Level1SupportHandler();
var level2 = new Level2SupportHandler();
var level3 = new Level3SupportHandler();

level1
    .SetNext(level2)
    .SetNext(level3);

Le code client transmet systématiquement les demandes au premier élément :

var request = new SupportRequest("Impossible de démarrer l'application", SupportLevel.Technical);

level1.Handle(request);

Le handler Level1SupportHandler reçoit en premier la demande. Comme celle-ci ne correspond pas à son niveau de responsabilité, il la transmet au handler Level2SupportHandler. Ce dernier peut la traiter et interrompt donc sa propagation dans la chaîne. Le code client n'a pas eu besoin de déterminer lui-même quel niveau de support devait recevoir la demande.

Avantages

La conception et l’implémentation présentées ci-dessus montrent que le principal avantage du design pattern Chain of Responsibility est de réduire le couplage entre l’objet qui émet une demande et celui qui la traite. La chaîne est généralement construite au démarrage de l’application, éventuellement à l’aide du conteneur d’injection de dépendances. Le code qui émet ensuite les demandes ne connaît que le premier handler et n’a pas besoin de connaître les autres éléments de la chaîne.

Le pattern facilite également l'évolution du processus de traitement. Il est facile d'ajouter un nouveau niveau de support :

var level4 = new Level4SupportHandler();

level1
    .SetNext(level2)
    .SetNext(level3)
    .SetNext(level4);

Cette organisation favorise le respect du principe SOLID OCP (Open/Closed Principle) : un nouveau traitement peut être introduit sous la forme d’un nouveau handler, sans modifier l’implémentation des handlers existants. Seule la composition de la chaîne doit être adaptée.

Arrêt ou poursuite de la chaîne

Une caractéristique importante du pattern Chain of Responsibility est la manière dont une demande circule dans la chaîne. Dans notre exemple, dès qu'un élément traite la demande, sa propagation s'arrête.

Il existe cependant une autre variante dans laquelle plusieurs éléments peuvent intervenir successivement. Cette variante est particulièrement adaptée aux pipelines dans lesquelles plusieurs traitements doivent être appliqués successivement à une même demande : validation, journalisation, authentification, gestion des erreurs, etc. Ce sujet est traité dans cet article : L'utilisation des middlewares dans les applications ASP.NET Core.