Quand utiliser le Design Pattern Decorator ?

Très souvent, lorsque l’on souhaite ajouter des fonctionnalités à une classe, on pense héritage – et c’est un bon réflexe ! Mais, très souvent également, cette solution n’est pas réellement adaptée : on peut souhaiter ajouter plusieurs fonctionnalités à la même classe de manière dynamique (runtime) et pas à l’écriture du programme (ajout de fonctionnalité statique), en choisissant les nouvelles fonctionnalités pour chaque instance et non pour la classe. Dans ce cas, on doit faire appel au design pattern Decorator (pattern du GoF de type structure).

La modélisation de glaces

Prenons un exemple simple pour illustrer ce pattern trop peu utilisé dans notre écosystème : la modélisation de glaces.
Il en existe plein de types (à l’eau, à l’Italienne, boules…) mais tous ces types de glaces ont en commun des goûts : chocolat, vanille, citron, etc.
On souhaite donc avoir un même modèle permettant d’avoir une glace à l’Italienne au chocolat et à la vanille. On va alors pouvoir utiliser l’héritage avec, par exemple, la classe Italienne (qui hérite de glace) et qui a comme propriété une liste de goûts.
Toutefois, si on ajoute des options telles que des amandes grillées sur le dessus ou toute autre chose, on complexifie le modèle et on risque surtout de ne plus respecter le principe SOLID « open close principle ».

Le Pattern Decorator

Voyons comment le pattern Decorator peut nous aider de manière élégante :
Dans ce pattern, on va identifier deux parties de l’arbre d’héritage:

    1. la partie classique où l’on va retrouver nos classes Glace (abstraite) GlaceItalienne et GlaceALeau.
    2. La partie liée aux ajouts de fonctionnalités avec :
      • une autre classe abstraite GlaceDecorator qui servira de base pour tous les decorators (ie fonctionnalités)
      • les décorateurs concrets : GoutVanille & GoutChocolat qui en héritent.

Decorateurs concrets

Pour le code, on va juste compléter la description de la glace :

 
public abstract class Glace
    {
        public virtual string Description
        {
            get
            {
                return "Glace virtuelle";
            }
        }
    }

    public class GlaceItalienne : Glace
    {
        public override string Description
        {
            get
            {
                return "glace italienne";
            }
        }
    }

    public class GlaceALeau : Glace
    {
        public override string Description
        {
            get
            {
                return "glace a l'eau";
            }
        }
    }

    public abstract class GlaceDecorator : Glace
    {
        protected Glace _glace;

        public override string Description
        {
            get
            {
                return _glace.Description;
            }
        }

    }

    public class GoutChocolat : GlaceDecorator
    {
        public GoutChocolat(Glace glace)
        {
            _glace = glace;
        }

        public override string Description
        {
            get
            {
                return _glace.Description + " avec du chocolat ";
            }
        }

    }

    public class GoutVanille : GlaceDecorator
    {

        public GoutVanille(Glace glace)
        {
            _glace = glace;
        }

        public override string Description
        {

            get
            {
                return _glace.Description + " avec de la vanille ";
            }
        }
    }

Lors de l’utilisation, il nous faut juste instancier une glace italienne et ajouter les options vanille et chocolat par exemple :

 
static void Main(string[] args) 
        { 
            Glace maglace = new GlaceItalienne(); 
            maglace = new GoutChocolat(maglace); 
            maglace = new GoutVanille(maglace); 
            Console.WriteLine(maglace.Description); 
            Console.ReadKey(); 
        }

J’ai le résultat suivant à la console :

Resultcons

De cette manière, avec un arbre d’héritage peu complexe, on a la possibilité d’avoir des compositions dynamiques très complexes tout en respectant le principe « open close ».

Un des avantages de ce pattern est l’aspect dynamique et optionnel des fonctionnalités que l’on ajoute. Si on le met en place suffisamment tôt lorsque l’on développe, la solution reste simple et élégante.
Contrairement à l’héritage, nous n’avons pas accès aux propriétés et méthodes privées de l’objet que l’on décore.