Parce que les développeurs ne sont pas les seuls à en baver : Les joies du sysadmin

Le design pattern Decorator expliqué

Le design pattern Decorator expliqué

Dans cet article je vais tenter de vous expliquer ce qu’est le design pattern decorator et quand l’utiliser.

Retrouvez les sources du projet ici : https://github.com/alexandrelanglais/design-pattern-decorator

La pizzeria de tonton

Imaginons que je veuille ouvrir ma propre pizzeria « Chez Tonton » et que je souhaite utiliser mon savoir-faire en programmation pour établir une liste de pizzas avec leur compositions, prix etc… pour ensuite les afficher sur un site Web.

Comme je suis pressé d’ouvrir mon business, j’imagine à la va-vite le diagramme de classes ci-dessous pour représenter les 4 pauvres pizzas que je sais faire :

Un diagramme hautement discutable

Ok j’ai donc une classe abstraite Pizza dont vont hériter les 4 types de pizzas. Les méthodes getDescription() et getCost() sont abstraites, je vais donc pouvoir les redéfinir dans chacune des classes enfant.


Besoin de changement

3 mois plus tard, la vie est belle, je découvre les joies d’être pizzaïolo mais mes clients en ont marre de bouffer tout le temps la même chose et me demandent de la diversité.

Pas de problème, j’embauche le pizzaïolo-vedette de la place romaine qui a 30 ans d’expérience derrière lui. Celui-ci me dit qu’on devrait pouvoir faire des pizzas à libre composition des clients.

« Euuh t’es sûr ? », lui demande-je en repensant à mon diagramme de classes pondu 3 mois plus tôt

« Si si siñor c’est dé la pizzeria 2.0, ça »

« Ok » lui dis-je. J’analyse les différents ingrédients que l’on possède;  je reprend mon diagramme d’avant; là je commence à sérieusement claquer du fessier. Vu la quantité d’ingrédients que l’on a, le diagramme de classe à coder se présenterait plus ou moins comme ceci :

Y a du pain sur la planche là..

J’insulte copieusement le moi d’il y a 3 mois en voyant le résultat seulement après avoir géré 3 ingrédients supplémentaire : olives, anchois et chorizo.


Puis me vient une (fausse) bonne idée : je vais ajouter des attributs dans la classe Pizza définissant s’il y a des olives, des anchois, du chorizo ou les 3 à la fois.

La classe Pizza aura une méthode de calcul qui tiendra compte de ces ingrédients et calculera un supplément. La description aura également une implémentation par défaut dans la classe Pizza pour rajouter les ingrédients supplémentaires :

C’est mieux mais quelque chose a l’air moisi tout de même …

Le code est réécrit comme suit : Classe Pizza

public abstract class Pizza {
   protected boolean olives;
   protected boolean anchois;
   protected boolean chorizo;

   public float getCost() {
      float cost = 0;
      if (olives) {
         cost += .99f;
      }
      if (anchois) {
         cost += 1.99f;
      }
      if (chorizo) {
         cost += 2.99f;
      }
      return cost;
   }

   public String getDescription() {
      String description = "";

      if (olives || anchois || chorizo) {
         description += " avec ";
         if (olives) {
            description += "olives ";
         }
         if (anchois) {
            description += "anchois ";
         }
         if (chorizo) {
            description += "chorizo ";
         }
      }
      return description;
   }

Classe QuatreFromages et autres :

public class QuatreFromages extends Pizza {

   @Override
   public float getCost() {
      return super.getCost() + 13.99f;
   }

   @Override
   public String getDescription() {
      return "Chèvre, Camembert, Mozzarella et Emmental" + super.getDescription();
   }

}

Classe main :

public class Pizzeria {

   public static void main(String[] args) {
      Pizza quatreFromagesSimple = new QuatreFromages();
      Pizza quatreFromagesAvecAnchois = new QuatreFromages();
      Pizza specialeTonton = new LaSpecialeTonton();
      Pizza reineAvecOlivesEtChorizo = new Reine();
      Pizza vegetarienneAvecOlives = new Vegetarienne();

      quatreFromagesAvecAnchois.setAnchois(true);

      reineAvecOlivesEtChorizo.setOlives(true);
      reineAvecOlivesEtChorizo.setChorizo(true);

      vegetarienneAvecOlives.setOlives(true);

      show(quatreFromagesSimple);
      show(quatreFromagesAvecAnchois);
      show(specialeTonton);
      show(reineAvecOlivesEtChorizo);
      show(vegetarienneAvecOlives);
   }

   private static void show(Pizza pizza) {
      DecimalFormat df = new DecimalFormat("0.00");
      System.out.println(String.format("%s : Prix = %s€ - Description = %s", pizza.getClass().getSimpleName(),
            df.format(pizza.getCost()), pizza.getDescription()));
   }

}

Résultat en sortie :

QuatreFromages : Prix = 13,99€ – Description = Chèvre, Camembert, Mozzarella et Emmental
QuatreFromages : Prix = 15,98€ – Description = Chèvre, Camembert, Mozzarella et Emmental avec anchois
LaSpecialeTonton : Prix = 19,99€ – Description = Tomates, Crème fraiche, Chorizo, Oignons, Champignons, Chèvre, Camembert, Mozzarella et Emmental
Reine : Prix = 19,97€ – Description = Jambon et Fromage avec olives chorizo
Vegetarienne : Prix = 10,98€ – Description = Tomates et Salade avec olives

C’est pas mal mais …

Je ne sais pas si vous voyez déjà les problèmes venir mais voici quelques observations relatives à cette mise en place :

  • Si le prix des ingrédients change, on va être obligé de modifier du code existant
  • Si on veut rajouter de nouveaux ingrédients par la suite, on va être obligé de modifier le code de la classe parente (c’est rarement une bonne idée)
  • Et si un client veut une double-ration d’olives ?

L’un des principes les plus important en programmation : Les classes doivent être ouvertes à l’expansion, mais fermées à la modification.

C’est le principe Ouvert-Fermé. Autrement dit, on ne veut (dans l’idéal) JAMAIS modifier une classe que l’on a testé et re-testé pour s’assurer qu’elle n’avait aucun bug. A la place, on doit ajouter de nouvelles fonctionnalités à cette classe en l’étendant.

Cela peut sembler contradictoire au premier abord mais il existe des techniques de la POO avancées qui permettent d’arriver à cette fin.


Le pattern decorator

C’est là qu’intervient le pattern Decorator. Avec ce pattern, plutôt que d’ajouter des attributs à notre classe de base, nous allons « décorer » celle-ci de plusieurs « décorateurs ». Si un client veut une pizza reine avec des olives et des anchois, nous allons :

  • Créer un objet pizza Reine de base
  • La décorer avec un objet Olives
  • La décorer avec un objet Anchois
  • S’appuyer sur la délégation de méthodes pour calculer le prix et générer la description adéquate

Commençons par créer une classe abstraite qu’étendrons nos décorateurs. Appelons-la IngredientDecorator :

package fr.demandeatonton.pizzeria.ingredients;

import fr.demandeatonton.pizzeria.pizzas.Pizza;

public abstract class IngredientDecorator extends Pizza {
   public abstract String getDescription();
}

La seule méthode abstraite que nous implémenterons est getDescription() afin de compléter la description de la pizza avec la présence d’olives, d’anchois ou de chorizo.

Vous notez que la classe étend Pizza alors que ce n’est pas réellement un genre de Pizza. Cependant, nous avons besoin que ces 2 composants puissent se mélanger. L’héritage de classe sert ici au Type Matching, mais pas au Comportement.

Ensuite nous implémentons nos décorateurs : Olives.java

package fr.demandeatonton.pizzeria.ingredients;

import fr.demandeatonton.pizzeria.pizzas.Pizza;

public class Olives extends IngredientDecorator {
   Pizza pizza;

   public Olives(Pizza pizza) {
      this.pizza = pizza;
   }

   @Override
   public String getDescription() {
      return pizza.getDescription() + " olives";
   }

   @Override
   public float getCost() {
      return 0.99f + pizza.getCost();
   }

}

Anchois.java

package fr.demandeatonton.pizzeria.ingredients;

import fr.demandeatonton.pizzeria.pizzas.Pizza;

public class Anchois extends IngredientDecorator {
   Pizza pizza;

   public Anchois(Pizza pizza) {
      this.pizza = pizza;
   }

   @Override
   public String getDescription() {
      return pizza.getDescription() + " anchois";
   }

   @Override
   public float getCost() {
      return 1.99f + pizza.getCost();
   }

}

Chorizo.java

package fr.demandeatonton.pizzeria.ingredients;

import fr.demandeatonton.pizzeria.pizzas.Pizza;

public class Chorizo extends IngredientDecorator {
   Pizza pizza;

   public Chorizo(Pizza pizza) {
      this.pizza = pizza;
   }

   @Override
   public String getDescription() {
      return pizza.getDescription() + " chorizo";
   }

   @Override
   public float getCost() {
      return 2.99f + pizza.getCost();
   }

}

Notez comme ces décorateurs prennent un constructeur une interface de type Pizza, qui peut de fait être une Pizza ou un Ingredient. Ensuite et par pseudo-récurrence, on s’appuie sur la délégation de méthodes pour générer la coût et la description de la pizza finale.

Ce procédé nous permet d’englober (de wrapper) plusieurs fois une pizza avec les décorateurs qui nous intéressent !

Ainsi notre classe main devient :

public class Pizzeria {

   public static void main(String[] args) {
      Pizza quatreFromages = new QuatreFromages();
      Pizza quatreFromagesOlivesAnchois = new Olives(new Anchois(new QuatreFromages()));
      Pizza specialeTonton = new LaSpecialeTonton();
      Pizza specialeTontonOlives = new Olives(new LaSpecialeTonton());
      Pizza specialeTontonDoubleOlives = new Olives(new Olives(new LaSpecialeTonton()));
      Pizza reine = new Reine();
      Pizza reineAnchoisOliveChorizo = new Olives(new Anchois(new Chorizo(new Reine())));
      Pizza vegetarienne = new Vegetarienne();

      show(quatreFromages);
      show(quatreFromagesOlivesAnchois);
      show(specialeTonton);
      show(specialeTontonOlives);
      show(specialeTontonDoubleOlives);
      show(reine);
      show(reineAnchoisOliveChorizo);
      show(vegetarienne);
   }

   private static void show(Pizza pizza) {
      DecimalFormat df = new DecimalFormat("0.00");
      System.out.println(
            String.format("Description = %s - Prix = %s€", pizza.getDescription(), df.format(pizza.getCost())));
   }

}

Avec comme résultat après exécution :

Description = La Quatre Fromages : Chèvre, Camembert, Mozzarella et Emmental – Prix = 13,99€
Description = La Quatre Fromages : Chèvre, Camembert, Mozzarella et Emmental anchois olives – Prix = 16,97€
Description = La Spéciale Tonton : Tomates, Crème fraiche, Chorizo, Oignons, Champignons, Chèvre, Camembert, Mozzarella et Emmental – Prix = 19,99€
Description = La Spéciale Tonton : Tomates, Crème fraiche, Chorizo, Oignons, Champignons, Chèvre, Camembert, Mozzarella et Emmental olives – Prix = 20,98€
Description = La Spéciale Tonton : Tomates, Crème fraiche, Chorizo, Oignons, Champignons, Chèvre, Camembert, Mozzarella et Emmental olives olives – Prix = 21,97€
Description = La Reine : Jambon et Fromage – Prix = 15,99€
Description = La Reine : Jambon et Fromage chorizo anchois olives – Prix = 21,96€
Description = La Végétarienne : Tomates et Salade – Prix = 9,99€

Notez qu’avec ce système, on a même pu créer une spéciale tonton avec double ration d’olives en supplément !

Si vous avez l’oeil aguerri vous aurez surement fait le lien avec une classe intégrée de base à Java : la classe java.io.Reader. Vous en connaissez sûrement d’autres 😉

Merci Tonton

Voilà pour cet article, j’espère que le Decorator n’a plus de secret pour vous désormais 😉

Retrouvez les sources du projet ici : https://github.com/alexandrelanglais/design-pattern-decorator

Pour en apprendre davantage sur les Design Patterns, je vous conseille l’excellent O’Reilly Head First Design Patterns

Laisser un commentaire