Design pattern: Décorateur

Objectif:

Rajouter dynamiquement une ou plusieurs compétences à une classe sans en modifier l’implémentation.

Justification

Problèmes

On souhaite ajouter un ou plusieurs comportements à une classe déjà implémentée. La méthode la plus simple est d’intervenir dans cette classe et de rajouter les comportements voulus directement. Cependant plusieurs raisons peuvent motiver le choix de ne pas toucher à l’implémentation de la classe:
Responsabilité unique: on ne veut pas compléxifier la classe en lui rajoutant trop de comportements. On évite ainsi de casser le principe de la responsabilité unique.
Comportements communs à plusieurs classes: le comportement à rajouter n’est pas forcément spécifique à la classe à modifier. Si ce comportement peut éventuellement être ajoutée à d’autres classes, on peut vouloir l’implémenter dans un objet et faire en sorte d’apporter la compétence aux autres classes sans dupliquer les implémentations.
Comportements configurables: pour éviter de rendre l’ajout de la compétence irréversible, on peut vouloir le rendre configurable ou dépendant d’un contexte.
Tests: des tests existent déjà pour cette classe, modifier le comportement de la classe peut conduire à devoir modifier les tests.

Solution

"Décorateur" rajoute le comportement à la classe à faire évoluer en l’encapsulant et en proposant les mêmes fonctions.

Par exemple, on considère la classe PersonStore permettant d’ajouter, de supprimer ou de mettre à jour des personnes. Cette classe satisfait l’interface IPersonStore:

public interface IPersonStore
{
   int AddNewPerson(string firstName, string lastName);
   void RemovePerson(string personId);
   void UpdatePerson(int personId, string firstName, string lastName);
}

public class PersonStore : IPersonStore
{
   public int AddNewPerson(string firstName, string lastName)
   { ... }

   public void RemovePerson(string personId)
   { ... }

   public void UpdatePerson(int personId, string firstName, string lastName)
   { ... }
}

On souhaite rajouter la capacité de "logging" à la classe. Le pattern "Décorateur" rajoute ce comportement en:
– implémentant le nouveau comportement dans une classe séparée appelée "Décorateur".
– le "décorateur" satisfait aussi IPersonStore pour que les classes consommatrices ne soient pas modifiées.
– le "décorateur" encapsule la classe PersonStore.

public class PersonStoreLoggingDecorator : IPersonStore
{
   private PersonStore _personStore;
   private ILog _logger;

   public PersonStoreLoggingDecorator(IPersonStore personStore, ILog logger)
   {
      this._personStore = personStore;
      this._logger = logger;
   }	

   public int AddNewPerson(string firstName, string lastName)
   {
      this._logger.Info("Adding new person");
      this._personStore.AddNewPerson(firstName, lastName);
   }

   public void RemovePerson(string personId)
   {
      this._logger.Info("Removing person");
      this._personStore.RemovePerson(personId);
   }

   public void UpdatePerson(int personId, string firstName, string lastName)
   {
      this._logger.Info("Updating person");
      this._personStore.UpdatePerson(personId, firstName, lastName);
   }
}

Le "décorateur" PersonStoreLoggingDecorator est utilisée de la même que PersonStore puisqu’elle possède la même interface:

IPersonStore personStoreWithLogger = new PersonStoreLoggingDecorator(new PersonStore());
personStoreWithLogger.AddNewPerson("Robert", "Mitchoum");

Ajouter plusieurs compétences
L’intérêt du pattern est de pouvoir ajouter plusieurs compétences en multipliant les "décorateurs". Par exemple, si on veut rajouter la sauvegarde des modifications sur un fichier, on peut implémenter un "décorateur" PersonStoreBackUpDecorator:

public class PersonStoreBackUpDecorator : IPersonStore
{
   private PersonStore _personStore;
   private PersonStoreFileWriter _fileWriter;
   
   public PersonStoreLoggingDecorator(IPersonStore personStore, 
      PersonStoreFileWriter fileWriter)
   {
      this._personStore = personStore;
      this._fileWriter = fileWriter;
   }	
   
   public int AddNewPerson(string firstName, string lastName)
   {
      this._personStore.AddNewPerson(firstName, lastName);
      this._fileWriter.Write(this._personStore);	
   }
   
   public void RemovePerson(string personId)
   {
      this._personStore.RemovePerson(personId);
      this._fileWriter.Write(this._personStore);
   }
   
   public void UpdatePerson(int personId, string firstName, string lastName)
   {
       this._personStore.UpdatePerson(personId, firstName, lastName);
       this._fileWriter.Write(this._personStore);
   }
}

On peut alors utiliser les 2 "décorateurs":

var personStore = new PersonStore();
IPersonStore personStoreWithLogger = new PersonStoreLoggingDecorator(personStore);
IPersonStore decoratedPersonStore = new PersonStoreBackUpDecorator(personStoreWithLogger);
decoratedPersonStore.AddNewPerson("Robert", "Mitchoum");

Pour aller plus loin

Version plus générique

L’exemple précédent utilisait des interfaces pour définir les décorateurs et la classe à faire évoluer. Une version plus générique de l’implémentation du pattern peut être privilégiée en utilisant:
– Une classe abstraite pour définir une version générique de la classe à faire évoluer: "Component".
– Plusieurs classes dérivant de "component" correspondant aux classes à décorer: "ConcreteComponent".
– Le "décorateur" abstrait: "Decorator".
– Tous les décorateurs spécialisés dérivant du "décorateur" abstrait: "ConcreteDecorator".

ATTENTION aux utilisations trop rigoureuses des patterns

Il est complètement inutile de vouloir implémenter un design pattern rigoureusement suivant sa définition. Chaque contexte est plus ou moins unique, il est pratiquement toujours d’adapter un pattern à se contexte.
Le pattern définit une solution qu’il convient d’adapter.

Interception

Une implémentation plus industrielle de ce pattern serait de privilégier l’interception pour découpler davantage les classes. Cette approche est plus configurable et des outils d’injection de dépendances aideraient à rendre l’implémentation plus évolutive.

Leave a Reply