Le modèle décorateur(Decorator Pattern)

Le pattern decorator selon l'ouvrage "Design Patterns: Elements of Reusable Object-Oriented Software" du GOF.

Alias

  • Fr : décorateur, emballeur
  • Angl : decorator

 

Intention du decorator pattern

Decorator Pattern attache dynamiquement des responsabilités supplémentaires à un objet. Les décorateurs fournissent une alternative souple à la dérivation, pour étendre les fonctionnalités.

 

Motivation du decorator pattern

GOF exemple de decorator pattern

Nous souhaitons parfois ajouter des fonctionnalités à des objets individuellement. Une boîte à outils d'interface graphique utilisateur, par exemple, peut nous laisser ajouter des propriétés comme des bordures, ou des comportements comme le scroll (défilement au moyen d'un ascenseur, ou scrollbar).

Une manière d'ajouter des responsabilités est l'héritage. Hériter une bordure d'une autre classe correspond à disposer cette bordure autour de chaque instance de sous-classe. Ce n'est cependant pas très souple, car le choix des bordures est fait de manière statique. Un client ne peut contrôler le moyen et le moment d'utiliser cette bordure dans le document.

Une approche plus flexible est d'introduire le composant dans un autre objet qui ajoute cette bordure. L'objet envelloppe est appelé decorator. Le décorateur se conforme à l'interface du composant qu'il décore, de telle manière que sa présence soit transparente aux clients du composant. Decorator transfère les requêtes au composant et peut effectuer des actions ajoutées (comme le tracé de bordure) avant ou après transmission. La transparence permet d'imbriquer récursivement les décorateurs, permettant ainsi l'ajout de responsabilités en nombre illimité.

Par exemple, supposons que nous disposions d'un objet TextView qui affiche un texte dans une fenêtre. TextView ne possède pas de scrollbar par défaut, parce que nous n'en avons pas systématiquement besoin. Quand nous en avons besoin, nous pouvons ajouter des scrollbars par l'intermédiaire d'un ScrollDecorator. Supposons que nous désirions aussi ajouter une bordure noire autour de TextView, nous pouvons utiliser un BorderDecorator pour le faire. Nous pouvons simplement composer les décorateurs avec l'objet TextView afin de produire le résultat escompté.

Les classes ScrollDecorator et BorderDecorator sont des sous-classes de Decorator, une classe abstraite pour composants visuels qui décore d'autres composants visuels.

VisualComponent est la classe abstraite pour les composants visuels. Elle définit la manière de les dessiner et une interface pour leur gestion d'évènements. Notons que la classe Decorator transfère simplement les demandes de tracés à ses composants, et que les sous-classes de ScrollDecorator peuvent étendre ce comportement.

Les sous-classes de Decorator peuvent ajouter des opérations pour les fonctionnalités spécifiques. Par exemple, l'opération ScrollTo de ScrollDecorator peut déléguer à un autre objet le défilement de l'interface, dans la mesure où il sait qu'il s'y trouve un objet ScrollDecorator. Un aspect important de ce pattern est qu'il autorise décorateurs apparaitre partout où un VisualComponent peut être. De cette manière, les clients ne peuvent généralement pas faire la différence entre un composant décoré et un autre non décoré, et ne sont donc absolument pas dépendants de la décoration.

 

Utilisation du decorator pattern

Le modèle decorateur peut nous aider dans les cas suivants :

  • Nous souhaitons ajouter dynamiquement des responsabilités à des objets individuels, ceci d'une manière transparente, c'est à dire sans affecter les autres objets.
  • Nous souhaitons pouvoir retirer des responsabilités.
  • Nous ne pouvons ajouter des responsabilités par dérivation. Nous pouvons parfois trouver un grand nombre d'extensions indépendantes possibles; il en résulte alors une explosion de la production de sous-classes pour permettre chaque combinaison.

 

Structure du decorator pattern

GOF structure de decorator patternConcreteDecoratorComponentConcreteComponentDecoratorConcreteDecorator

 

Constituants du decorator pattern

  • Component (Composant)
    Component définit l'interface des objets qui peuvent se voir ajouter dynamiquement des responsabilités supplémentaires. Dans notre exemple, le composant est VisualComponent.
  • ConcreteComponent (Composant concret)
    ConcreteComponent définit un objet auquel nous pouvons ajouter des responsabilités supplémentaires. Dans notre exemple, le composant concret est TextView.
  • Decorator (Décorateur)
    Decorator gère une référence à un objet Component et définit une interface conforme à celle du Component.
  • ConcreteDecorator (Décorateur concret)
    ConcreteDecorator ajoute des responsabilités à un composant. Dans notre exemple, les décorateurs concrets sont BorderDecorator et ScrollDecorator.

 

Collaborations du decorator pattern

Decorator transfère les requêtes à son objet Component. Il peut éventuellement effectuer des opérations supplémentaires avant ou après le transfert de la requête.

 

Conséquences du decorator pattern

Avantages du decorator pattern

Plus flexible que l'héritage statique. Decorator pattern nous fournit un moyen d'ajouter des responsabilités aux objets de manière plus flexible que nous ne pourions le faire à l'aide de l'héritage (multiple dans certains langages, pas dans Java) statique. Avec les décorateurs, les responsabilités peuvent être ajoutées ou retirées à l'exécution simplement en les attachant ou en les détachant. A l'opposé, l'héritage impose de créer de nouvelles classes pour chaque responsabilité ajoutée (par exemple : BorderedScrollableTextView, BorderedTextView). Ceci engendre une prolifération de classes et accroît la complexité d'un système. Plus encore, fournir différentes classes Decorator pour une classe Component particulière nous impose de mixer et d'accorder les responsabilités.

Les décorateurs permettent en outre de répéter l'ajout d'une même propriété. Par exemple, pour ajouter une bordure double à TextView, nous pouvons ajouter deux fois BorderDecorator.

Evite de surcharger de fonctionnalités les classes situées en haut de la hiérarchie. Decorator permet d'ajuster en permanence l'ajout des responsabilités en fonction du besoin. Au lieu de tenter de supporter tous les dispositifs dans une classe complexe adaptable, nous pouvons définir une simple classe et ajouter de manière incrémentale les fonctionnalités à l'aide des objets décorateurs. Les fonctionnalités peuvent être constituées d'éléments simples. de cette manière, une application ne doit pas se charger des fonctionnalités qu'elle n'utilisera pas. Nous pouvons aussi aisément définir de nouvelles sortes de décorateurs, indépendament des classes d'objets qu'ils enrichissent, même pour des extensions impossibles à spécifier à l'avance.

Contraintes du decorator pattern

Un décorateur et ses composants ne sont pas identiques. Un décorateur se comporte comme un conteneur transparent. Mais du point de vue de l'identité des objets, un composant décoré n'est pas identique au composant lui-même. Nous ne pouvons donc pas faire confiance à l'identité des objets quand nous utilisons les décorateurs.

Multiples petits objets. Un design (une conception) qui utilise le modèle Decorateur amène souvent un système composé d'une multitude de petits objets, tous d'apparence semblable. Ces objets diffèrent seulement par la manière dont ils sont interconnectés, et non par leur classe ou par la valeur de leurs variables. Bien que ces systèmes soient aisés à spécialiser par ceux qui les ont assimilés, ils peuvent être difficiles à apprendre et à débugger.

 

Implémentation du decorator pattern

Particularités à considérer dans l'utilisation du Decorator Pattern:

Conformité d'interface. L'interface d'un objet Decorator doit être conforme à l'interface du composant décoré.

Omission de la classe abstraite Decorator. Si nous n'avons qu'une responsabilité à ajouter, nous ne devons pas nécessairement définir une classe Decorator abstraite. C'est souvent le cas quand nous travaillons sur une hiérarchie de classes existantes, plutôt que d'en concevoir une nouvelle. Dans ce cas, nous pouvons fusionner les responsabilités de Decorator pour le transfert des requêtes vers le composant dans ConcreteDecorator.

Conserver un poid léger pour les classes Component. Pour garantir le fait de se conformer à l'interface, les componants et l'interface doivent descendre d'une classe Componant commune. Il est important de conserver à cette classe commune un poids le plus léger possible, c'est à dire faire en sorte qu'elle se consacre à la définition d'une interface, et non au stockage des données. La définition de la représentation des données devrait être déférée aux sous-classes; sinon la complexité de la classe Component risquerait de rendre les décorateurs trop lourds pour être utilisés en quantité. Implémenter beaucoup de fonctionnalités dans un composant augmente le risque que des sous-classes concrètes soient pénalisées par la présence de fonctionnalités dont elles n'ont pas besoin.

Modifier l'apparence d'un objet ou modifier ses entrailles. Nous pouvons nous représenter un décorateur comme une enveloppe qui recouvre un objet, en modifie le comportement. Une solution alternative pour modifier l'objet consisterait à en modifier les entrailles (la structure interne). Le Strategy Pattern est un bon exemple de modèle pour modifier les entrailles.

L'utilisation du Strategy Pattern est le meilleur choix quand la classe Component est intrinsèquement lourde, ce qui rend la mise en œuvre du Decorator Pattern trop coûteuse. Dans le Strategy Pattern, le composant transfère certains de ses comportements à un objet Strategy séparé. Le Strategy Pattern permet d'accroitre ou de réduire les fonctionnalités du composant par remplacement de l'objet Strategy.

Par exemple, nous pouvons supporter différents styles de bordures par le fait que le composant délègue le tracé de bordure à un objet Border séparé. L'objet Border est un objet Strategy qui encapsule une stratégie de tracé de bordure. En étendant le nombre de stratégies d'une seule vers une liste non-limitée, nous obtenons le même effet qu'en enchaînant récursivement les décorateurs.

 

Exemples de codes du decorator pattern

  1. using System;
  2. // Component
  3. abstract class Component{
  4. // Methods
  5. abstract public void Operation();
  6. }
  7. // ConcreteComponent
  8. class ConcreteComponent : Component{
  9. // Methods
  10. override public void Operation(){
  11. Console.WriteLine("ConcreteComponent.Operation()");
  12. }
  13. }
  14. // Decorator
  15. abstract class Decorator : Component{
  16. // Fields
  17. protected Component component;
  18. // Methods
  19. public void SetComponent(Component component){
  20. this.component = component;
  21. }
  22. override public void Operation(){
  23. if(component != null){
  24. component.Operation();
  25. }
  26. }
  27. }
  28. // ConcreteDecoratorA
  29. class ConcreteDecoratorA : Decorator{
  30. // Fields
  31. private string addedState;
  32. // Methods
  33. override public void Operation()
  34. {
  35. base.Operation();
  36. addedState = "new state";
  37. Console.WriteLine("ConcreteDecoratorA.Operation()");
  38. }
  39. }
  40. // ConcreteDecoratorB
  41. class ConcreteDecoratorB : Decorator{
  42. // Methods
  43. override public void Operation(){
  44. base.Operation();
  45. AddedBehavior();
  46. Console.WriteLine("ConcreteDecoratorB.Operation()");
  47. }
  48. void AddedBehavior(){
  49. }
  50. }
  51. /// Client test
  52. public class Client{
  53. public static void Main(string[] args){
  54. // Create ConcreteComponent and two Decorators
  55. ConcreteComponent c = new ConcreteComponent();
  56. ConcreteDecoratorA d1 = new ConcreteDecoratorA();
  57. ConcreteDecoratorB d2 = new ConcreteDecoratorB();
  58. // Link decorators
  59. d1.SetComponent( c );
  60. d2.SetComponent( d1 );
  61. d2.Operation();
  62. }
  63. }

Ce qui produit en sortie :

ConcreteComponent.Operation()
ConcreteDecoratorA.Operation()
ConcreteDecoratorB.Operation()

 

  • Adapter : Decorator est différent d'un Adapter dans le sens où il ne fait que changer les responsabilités d'un objet, et non son interface. Un adapteur donnera à un objet une interface complètement nouvelle.
  • Composite : Decorator peut être vu comme un composite dégénéré ne possédant qu'un seul composant. Cependant, un décorateur ajoute des responsabilités supplémentaires - il ne favorise pas l'aggrégation d'objets.
  • Strategy : Decorator permet de changer l'enveloppe (apparence) d'un objet. Un Strategy permet de changer la structure interne. Ce sont les deux solutions alternatives pour changer un objet.

 

Document créé le 11/10/05 21:11, dernière modification le 23/03/18 09:27
Source du document imprimé : https://www.gaudry.be/pattern-decorator.html

L'infobrol est un site personnel dont le contenu n'engage que moi. Le texte est mis à disposition sous licence CreativeCommons(BY-NC-SA). Plus d'info sur les conditions d'utilisation et sur l'auteur.