Les Design Patterns structurels, en programmation, se concentrent sur la manière dont les classes et les objets sont structurés pour former des architectures logicielles plus flexibles et plus faciles à maint enir. Ils facilitent la composition d'objets pour créer des structures plus complexes tout en minimisant les dépendances entre les différents éléments du système.
Les design patterns comportementaux les plus utilisés sont les suivants :
- Adaptateur
- Composite
- Décorateur
- Façade
- Pont
- Proxy
Le design pattern Décorateur
Définition
Le design pattern Décorateur, aussi connu sous le nom de Wrapper, permet d’ajouter dynamiquement des comportements ou des responsabilités à un objet sans modifier son code.
Il favorise l'utilisation de la composition plutôt que de l'héritage pour l'extension des fonctionnalités.
Le décorateur enveloppe l'objet d'origine et ajoute de nouvelles fonctionnalités tout en déléguant les appels à l'objet encapsulé.
⚖️ Avantages et inconvénients
|
|
➕Avantages
- Flexibilité accrue : Contrairement à l'héritage, le décorateur permet d'ajouter des fonctionnalités de manière dynamique et flexible à un objet.
- Combinaison de comportements : Les décorateurs peuvent être empilés les uns sur les autres, permettant ainsi de combiner différentes fonctionnalités de manière modulaire.
- Responsabilité unique : Chaque décorateur a une seule responsabilité, ce qui rend le code plus facile à maintenir et plus lisible.
- Réutilisabilité : Les composants peuvent être réutilisés indépendamment dans différents contextes sans avoir besoin de dupliquer le code.
➖Inconvénients
- Complexité accrue : L'utilisation de plusieurs décorateurs peut rendre la compréhension du code plus difficile, surtout si les décorateurs sont imbriqués.
- Difficile à débug : La multiplication des couches de décoration peut rendre le débogage et le suivi des appels plus complexes.
- Performance : Chaque appel de méthode passe par plusieurs objets, ce qui peut légèrement dégrader les performances.
Exemple d'implémentation
Comme sans doute la plupart des développeurs je consomme régulièrement une boisson noire et amère : le café.
Il y'a quelques années de nombreux établissements spécialisés dans cette boisson ont ouvert un peu partout proposant bien sur du café, et d'autre dérivés de ce dernier, nous allons donc utilisé cet exemple dans la suite de cet article.
Dans notre exemple, nous allons aussi voir le principe de substitution de Liskov :
Si S est un sous-type de T alors tout objet de type T peut être remplacé par un objet de type S sans altérer les propriétés désirables du programme concerné
Coffee et ses implémentations :
Nous allons commencer par déclarer une interface Coffee
Coffee
est l'interface de base pour tous les types de café. Elle déclare les méthodes getCost
et getDescription
que toutes les classes de café doivent implémenter.
Nous allons ensuite créer notre première implémentation de cette interface, SimpleCoffee
.
public class SimpleCoffee implements Coffee{
@Override
public double getCost() {
return 1;
}
@Override
public String getDescription() {
return "Un café classique";
}
}
SimpleCoffee
représente un café de base sans décoration. Il implémente les méthodes getCost
et getDescription
de l'interface Coffee
.
Nous allons ensuite créer une classe abstraite CoffeeDecorator
qui implémente l'interface Coffee
et contient une référence à un objet Coffee
.
public abstract class CoffeeDecorator implements Coffee {
protected Coffee decoratedCoffee;
public CoffeeDecorator(Coffee coffee) {
this.decoratedCoffee = coffee;
}
@Override
public double getCost() {
return decoratedCoffee.getCost();
}
@Override
public String getDescription() {
return decoratedCoffee.getDescription();
}
}
CoffeeDecorator
sert de classe de base pour tous les décorateurs de café. Elle implémente les méthodes de l'interface Coffee
en déléguant les appels à l'objet Coffee
encapsulé.
Création des décorateurs :
Dans notre exemple, nous aurons 2 extensions à notre décorateur de base :
MilkDecorator
SugarDecorator
public class MilkDecorator extends CoffeeDecorator{
public MilkDecorator(Coffee coffee) {
super(coffee);
}
public double getCost() {
return super.getCost() + 1.5;
}
@Override
public String getDescription() {
return super.getDescription() + ", avec du lait";
}
}
public class SugarDecorator extends CoffeeDecorator{
public SugarDecorator(Coffee coffee) {
super(coffee);
}
public double getCost() {
return super.getCost() + 0.5;
}
@Override
public String getDescription() {
return super.getDescription() + ", avec du sucre";
}
}
Ces décorateurs modifient le cout du café en ajoutant le cout de l'ingrédient ajouté, et modifient également la description pour y ajouter le dit ingrédient.
Exemple d'utilisation
public static void main(String[] args) {
Coffee coffee = new SimpleCoffee();
System.out.println(coffee.getDescription() + " €" + coffee.getCost());
coffee = new MilkDecorator(coffee);
System.out.println(coffee.getDescription() + " €" + coffee.getCost());
coffee = new SugarDecorator(coffee);
System.out.println(coffee.getDescription() + " €" + coffee.getCost());
}
voyons un peu plus en profondeur ce que nous faisons ici :
Création d'un café simple : Nous commençons par créer un objet SimpleCoffee
.
Coffee coffee = new SimpleCoffee();
System.out.println(coffee.getDescription() + " €" + coffee.getCost());
Ajout de lait : Nous enveloppons l'objet coffee
avec MilkDecorator
. Bien que l'objet original soit de type SimpleCoffee
, il est maintenant traité comme un objet de type Coffee
avec du lait ajouté.
coffee = new MilkDecorator(coffee);
System.out.println(coffee.getDescription() + " €" + coffee.getCost());
Ajout de sucre : De même, nous enveloppons le café (qui a déjà du lait) avec SugarDecorator
, en continuant à traiter l'objet comme un Coffee
.
coffee = new SugarDecorator(coffee);
System.out.println(coffee.getDescription() + " €" + coffee.getCost());
Plus haut je vous avait parlé de la substitution de Liskov, c'est ici qu'elle entre en jeu. En effet, nous pouvons substituer des instances de Coffee
par des instances de SimpleCoffee
, MilkDecorator
, et SugarDecorator
sans modifier le code client.
Le code suivant donnera le résultat suivant en sortie console :
Un café classique €1.0
Un café classique, avec du lait €2.5
Un café classique, avec du lait, avec du sucre €3.0
En conclusion
Le design pattern Décorateur permet d'ajouter des fonctionnalités à des objets de manière dynamique. Bien qu'il puisse introduire une certaine complexité dans le code, ses avantages en termes de modularité et de réutilisabilité en font un choix précieux dans de nombreux scénarios.
Si vous souhaitez approfondir le sujet, tout le code utilisé dans cet article est disponible juste ici !