Pattern Decorator

1. Problème initial

Comment ajouter dynamiquement des fonctionnalités à des objets dont les instances ont déjà été créés ?

2. Solution associée

Créer une classe abstraite qui va hériter de la même classe que celle dont hérite l’objet qu’on souhaite modifier. On redéfinit les méthodes qui nous intéressent et on créer autant de classes filles que d’amélioration possible.

3. Définition et exemple

Le pattern Décorateur (Decorator) attache dynamiquement des responsabilités supplémentaires à un objet. Il fournit une alternative souple à l’héritage, pour étendre des fonctionnalités.

Dans la programmation orientée objet, la façon la plus classique d’ajouter des fonctionnalités à une classe est d’utiliser l’héritage. Pourtant il arrive parfois de vouloir ajouter des fonctionnalités à une classe sans utiliser l’héritage. En effet, si l’on hérite d’une classe la redéfinition d’une méthode peut entraîner l’ajout de nouveaux bugs. On peut aussi être réticent à l’idée que des méthodes de la classe mère soient appelées directement depuis notre nouvelle classe.

De plus, l’héritage doit être utilisé avec parcimonie. Car si on abuse de ce principe de la programmation orientée objet, on aboutit rapidement à un modèle complexe contenant un grand nombre de classes.

Un autre souci de l’héritage est l’ajout de fonctionnalités de façon statique. En effet, l’héritage de classe se définit lors de l’écriture du programme et ne peut être modifié après la compilation. Or, dans certains cas, on peut vouloir rajouter des fonctionnalités de façon dynamique.

D’une manière générale on constate que l’ajout de fonctionnalités dans un programme s’avère parfois délicat et complexe. Ce problème peut être résolu si le développeur a identifié, dès la conception, qu’une partie de l’application serait sujette à de fortes évolutions. Il peut alors faciliter ces modifications en utilisant le pattern Décorateur. La puissance de ce pattern qui permet d’ajouter (ou modifier) des fonctionnalités facilement provient de la combinaison de l’héritage et de la composition. Ainsi les problèmes cités ci-dessus ne se posent plus lors de l’utilisation de ce pattern.

Afin de mettre en pratique le pattern Décorateur, nous allons concevoir une application qui permet de gérer la vente de desserts. Celle-ci doit permettre d’afficher dans la console le nom complet du dessert choisi et son prix. Les clients ont le choix entre deux desserts : crêpe ou gaufre. Sur chaque dessert ils peuvent ajouter un nombre quelconque d’ingrédients afin de faire leurs propres assortiments. Pour simplifier notre exemple, nous choisirons uniquement deux ingrédients (le chocolat et la chantilly) mais il faut garder à l’esprit que l’ajout de nouveaux ingrédients doit être simplifié. Le système de tarification est simple. Une crêpe (nature) coûte 1.50€ et une gaufre 1.80€. L’ajout de chocolat est facturé 0.20€ et de chantilly 0.50€.

Voyons comment concevoir cette application. Une première idée est de mettre en place une classe abstraite Dessert ayant deux attributs (libelle et prix) et les accesseurs en lecture/écriture correspondants. Puis, créer pour chaque combinaison de desserts et d’ingrédients une classe (CrepeChocolat, CrepeChantilly, GaufreChocolat, GaufreChantilly). Bien sûr cette solution n’est pas évolutive. Si l’on souhaite modifier le prix de l’ingrédient Chocolat on doit le modifier dans deux classes. De plus, si on ajoute une dizaine d’ingrédients nous allons obtenir une centaine de classes.

Une deuxième solution consiste à garder la classe Dessert en la modifiant légèrement. On peut rajouter un booléen pour savoir si l’ingrédient chocolat est ajouté à ce dessert de même pour Chantilly. Puis, on crée des classes Crepe et Gaufre qui héritent de Dessert. Le calcul du prix des ingrédients est effectué dans la classe Dessert auquel on rajoute le prix spécifique du dessert suivant le type de l’objet (Gaufre ou Crepe). Cette solution semble plus satisfaisante mais pose toujours certains problèmes. Si l’on souhaite rajouter un ingrédient, il faut ajouter un attribut dans la classe Dessert et modifier la méthode qui calcule le prix afin de le prendre en considération. De plus, toutes les classes héritant de Dessert posséderont ces attributs qui n’auront pas toujours de sens. Si on crée une classe SaladeDeFruit héritant de Dessert on aura un attribut (hérité de la classe mère) nommé chocolat (bizarre pour une salade de fruit).

Voyons ce qu’apporte le pattern décorateur à notre problématique.

La solution consiste à utiliser à la fois l’héritage et la composition.
Tout d’abord une classe abstraite Dessert regroupe les attributs et les méthodes communes liées aux desserts.



Le code associé est le suivant :



Ensuite on a des desserts concrets tel que Gaufre et Crepe qui héritent de cette classe. Dans le constructeur de ces classes on met à jour les attributs définis dans Dessert à l’aide des accesseurs.



Le code associé est le suivant :



Ensuite, afin de gérer les ingrédients, il faut une classe abstraite nommée DecorateurIngredient. Celle-ci possède un Dessert en attribut et oblige la redéfinition de deux méthodes getLibelle() et getPrix().
Cette classe va permettre de faire le lien entre un dessert et son ingrédient.
On utilise une classe abstraite pour pouvoir hériter du type Dessert et pouvoir décider de quelles méthodes de Dessert vont être transmises aux ingrédients.





Enfin, chaque ingrédient (Chantilly, Chocolat...) doit hériter de la classe DecorateurIngredient.
Le constructeur de ces classes permet d’initialiser l’attribut dessert présent dans la classe mère. De plus, la redéfinition des méthodes getLibelle() et getPrix() va permettre d’ajouter des fonctionnalités.





Le pattern Décorateur basé sur l’héritage et la composition est facile à mettre place. Il permet de répondre parfaitement à la problématique rencontrée lors de la conception de cette application de gestion des desserts. C’est à dire comment concevoir une application évolutive ou l’ajout et la modification de fonctionnalités ne posera pas de problème (explosion du nombre de classe...).

4. A retenir

Pour appliquer ce pattern il faut :
  • Définir une classe abstraite qui représente la composant par défaut
  • Créer des composant concret qui héritent du composant
  • Pour modifier ces composants on crée une classe décorateur abstraite qui hérite de la classe Composant et qui possède une variable du type Composant.
  • Les décorateurs concrets héritent de la classe décorateur et possèdent dans leur constructeur une référence vers le composant qu’ils vont améliorer.