Aller au contenu

Decorator

Design Pattern – Decorator

Ajouter un comportement à un objet sans modifier l’existant

Introduction

Decorator est un design pattern de structuration dont l'objectif est d'ajouter dynamiquement de nouveaux comportements à un objet, sans modifier son implémentation.

Contrairement à l'héritage, qui enrichit une classe par l'ajout de membres, le Decorator encapsule un objet, afin de lui ajouter un comportement avant et/ou après son exécution. Cette approche favorise la composition et contribue à l'application du principe SOLID OCP (Open/Closed).

Ce design pattern est particulièrement adapté pour implémenter des traitements transverses (logging, cache, mesure des performances, sécurité, etc.) pouvant être ajoutés ou retirés indépendamment les uns des autres.

Problématique

Au fil de l'évolution d'une application, un même service peut devoir intégrer plusieurs traitements. Prenons l'exemple d'un service chargé d'envoyer une notification :

notificationService.Send(message);

De nouveaux besoins apparaissent progressivement. Il est demandé dans un premier temps de loguer les appels d'envoi des notifications. Puis d'enregistrer les erreurs dans une table de la base de données. Puis de mesurer les temps d'exécution. Et enfin d'implémenter un cache afin d'améliorer les performances.

Une première solution consiste à modifier directement la classe existante. Celle-ci devient alors responsable de nombreuses fonctionnalités qui ne relèvent pas de son rôle. Une autre approche consiste à créer une classe dérivée pour chaque combinaison de comportements. Cependant, cette solution devient rapidement difficile à maintenir. Le design pattern Decorator résout ce problème, en isolant chaque responsabilité dans une classe indépendante qui encapsule le composant à enrichir.

Fonctionnement

Le design pattern Decorator repose sur quatre éléments :

  • Le composant : définit le contrat commun aux objets qui peuvent être décorés. Il s'implémente avec une interface ou une classe abstraite.

  • Le composant concret : fournit l’implémentation de base du comportement. C’est l’objet qui sera enrichi par un ou plusieurs décorateurs.

  • Le décorateur abstrait : implémente le même contrat que le composant et conserve une référence vers un autre composant. Il délègue généralement l’exécution à l’objet décoré.

  • Les décorateurs concrets : ajoutent un comportement avant ou après l’appel au composant décoré, sans modifier sa classe.

Design Pattern Command : conception

Dans ce diagramme de classes :

  • L'interface INotificationService est le composant.

  • La classe NotificationService est le composant concret.

  • Les classes LoggingDecorator et TimingDecorator sont les décorateurs concrets. La première ajoute des traces dans la console avant et après l'envoi d'une notification, tandis que la seconde mesure la durée d'exécution.

Exemple complet en C

Voici le composant correspondant au contrat commun :

public interface INotificationService
{
    void Send(string message);
}

Voici l'implémentation du décorateur abstrait et des décorateurs concrets :

public abstract class NotificationDecorator : INotificationService
{
    protected readonly INotificationService Service;

    protected NotificationDecorator(INotificationService service)
    {
        Service = service;
    }

    public virtual void Send(string message)
    {
        Service.Send(message);
    }
}

public class LoggingDecorator : NotificationDecorator
{
    public LoggingDecorator(INotificationService service)
        : base(service)
    {
    }

    public override void Send(string message)
    {
        Console.WriteLine("Début de l'envoi");

        base.Send(message);

        Console.WriteLine("Fin de l'envoi");
    }
}

public class TimingDecorator : NotificationDecorator
{
    public TimingDecorator(INotificationService service)
        : base(service)
    {
    }

    public override void Send(string message)
    {
        var stopwatch = Stopwatch.StartNew();

        base.Send(message);

        stopwatch.Stop();

        Console.WriteLine($"Durée : {stopwatch.ElapsedMilliseconds} ms");
    }
}

Chaque décorateur ajoute son propre comportement sans modifier le code des autres classes :

INotificationService service =
    new TimingDecorator(
        new LoggingDecorator(
            new NotificationService()));

service.Send("Bonjour");

Enregistrement avec l'injection de dépendances .NET

L'injection de dépendances permet de construire automatiquement la chaîne de décorateurs. Nous ajoutons les services suivants dans le conteneur d'injection de dépendances :

services.AddScoped<NotificationService>();

services.AddScoped<INotificationService>(provider =>
{
    INotificationService service =
        provider.GetRequiredService<NotificationService>();

    service = new LoggingDecorator(service);
    service = new TimingDecorator(service);

    return service;
});

Pour des chaînes de décorateurs plus complexes, le package NuGet Scrutor permet de les enregistrer de manière beaucoup plus concise.

Avantages

Les avantages du design pattern Decorator sont les suivants :

  • Il respecte le principe SOLID OCP (Open/Closed Principle).

  • Il favorise la composition plutôt que l'héritage et permet ainsi d'éviter la multiplication des classes dérivées.

  • Chaque responsabilité reste isolée dans une classe dédiée.

  • Les comportements proposés par les décorateurs peuvent être combinés en fonction des besoins.