Synthèse de cours - Le pattern décorateur

Introduction

Le design pattern décorateur est un pattern structural qui va permettre d’ajouter d’ajouter de nouveaux comportements et responsabilités à un objet, dynamiquement.

Grâce à ce pattern, on va pouvoir ajouter de nouvelles fonctionnalités à une classe existante sans avoir besoin d’avoir recours à l’héritage ou au multihéritage (au niveau du composant visé).

Les responsabilités de l’objet décoré peuvent être ajoutées ou retirées dynamiquement lors de l’exécution du programme.

Ce pattern exploite différentes notions que nous avons déjà abordées avec les principes SOLID : principe ouvert/fermé, faible couplage, inversion et injection des dépendances, abstraction, composition…

De manière générale, son diagramme de classes est le suivant :

Diagramme 1

interface ComposantAbstrait {
    void operationA();
    void operationB();
}

class ComposantConcret implements ComposantAbstrait {

    @Override
    public void operationA() {
        System.out.println("Comportement operationA de base");
    }

    @Override
    public void operationB() {
        System.out.println("Comportement operationB de base");
    }

}

abstract class ComposantDecorateur implements ComposantAbstrait {

    private ComposantAbstrait delegue;

    public ComposantDecorateur(ComposantAbstrait delegue) {
        this.delegue = delegue;
    }

    @Override
    public void operationA() {
        delegue.operationA();
    }

    @Override
    public void operationB() {
        delegue.operationB();
    }

}

class DecorateurConcretA extends ComposantDecorateur {

    public DecorateurConcretA(ComposantAbstrait delegue) {
        super(delegue);
    }

    @Override
    public void operationA() {
        super.operationA();
        System.out.println("Nouveau comportement pour operationA ajouté par DecorateurConcretA");
    }

}

class DecorateurConcretB extends ComposantDecorateur {

    public DecorateurConcretB(ComposantAbstrait delegue) {
        super(delegue);
    }

    @Override
    public void operationB() {
        super.operationB();
        System.out.println("Nouveau comportement pour operationB ajouté par DecorateurConcretB");
    }

}

class DecorateurConcretC extends ComposantDecorateur {

    public DecorateurConcretC(ComposantAbstrait delegue) {
        super(delegue);
    }

    @Override
    public void operationA() {
        super.operationA();
        System.out.println("Nouveau comportement pour operationA ajouté par DecorateurConcretC");
    }

    @Override
    public void operationB() {
        super.operationB();
        System.out.println("Nouveau comportement pour operationB ajouté par DecorateurConcretC");
    }

}

class Main {
    public static void main(String[] args) {
        ComposantAbstrait obj1 = new DecorateurConcretB(new DecorateurConcretA(new ComposantConcret()));
        ComposantAbstrait obj2 = new DecorateurConcretB(new ComposantConcret());
        ComposantAbstrait obj3 = new ComposantConcret();
        ComposantAbstrait obj4 = new DecorateurConcretB(new DecorateurConcretC(new DecorateurConcretA(new ComposantConcret())));

        /**
         * Affiche :
         * Comportement operationA de base
         * Nouveau comportement pour operationA ajouté par DecorateurConcretA
         * Nouveau comportement pour operationA ajouté par DecorateurConcretC
        */ 
        obj4.operationA();

        /**
         * Affiche :
         * Comportement operationB de base
         * Nouveau comportement pour operationB ajouté par DecorateurConcretC
         * Nouveau comportement pour operationB ajouté par DecorateurConcretB
        */ 
        obj4.operationB();
    }
}

Exemples

Nous allons maintenant voir trois exemples plus concrets pour illustrer le fonctionnement de ce pattern.

Premier exemple : Salariés avec plusieurs responsabilités

Le pattern décorateur est donc idéal pour gérer ce système de responsabilités. Le composant de base est la classe Salarie et les différentes responsabilités (chef de projet, responsable de stagiaires) sont les décorateurs. On pourra ainsi facilement ajouter de nouvelles responsabilités dans le futur.

interface SalarieInterface {
  double getSalaire();
}


class Salarie implements SalarieInterface {

  private double salaire;

  public Salarie(double salaire) {
    this.salaire = salaire;
  }

  public double getSalaire() {
    return salaire;
  }

}

abstract class SalarieDecorateur implements SalarieInterface {
   private SalarieInterface salarie;

   public SalarieDecorateur(SalarieInterface salarie) {
      this.salarie = salarie;
   }

   public double getSalaire() {
      return salarie.getSalaire();
   }
}

class SalarieChefProjet extends SalarieDecorateur {

  private int nombreProjetsGeres;

  public SalarieChefProjet(SalarieInterface salarie, int nombreProjetsGeres) {
    super(salarie);
    this.nombreProjetsGeres = nombreProjetsGeres;
  }

  @Override
  public double getSalaire() {
    return super.getSalaire() + 100 * (nombreProjetsGeres);
  }

}

class SalarieResponsableStagiaires extends SalarieDecorateur {

  private int nombreStagiairesGeres;

  public ResponsableDeStagiaires(SalarieInterface salarie, int nombreStagiairesGeres) {
    super(salarie);
    this.nombreStagiairesGeres = nombreStagiairesGeres;
  }

  @Override
  public double getSalaire() {
    return super.getSalaire() + 50 * (nombreStagiairesGeres);
  }

}

class Main {
    public static void main(String[]args) {
        SalarieInterface s1 = new Salarie(2000);
        SalarieInterface s2 = new SalarieChefProjet(s1, 3);
        SalarieInterface s3 = new SalarieResponsableStagiaires(s1, 2);
        SalarieInterface s4 = new SalarieChefProjet(new SalarieResponsableStagiaires(s1, 2), 3);

        //Affiche 2000
        System.out.println(s1.getSalaire());

        //Affiche 2300
        System.out.println(s2.getSalaire());

        //Affiche 2100
        System.out.println(s3.getSalaire());

        //Affiche 2400
        System.out.println(s4.getSalaire());
    }
}

Open close 3

Deuxième exemple : Styles de textes

Voici le contexte du second exemple :

Ici aussi, le pattern décorateur est parfait pour cette tâche : le composant de base est le TexteBasique et les décorateurs sont les différents styles. On pourra ainsi facilement ajouter de nouveaux styles dans le futur.

interface TexteInterface {
    void afficher();
}

class TexteBasique implements TexteInterface {

    private String texte;

    public TexteBasique(String texte) {
        this.texte = texte;
    }

    @Override
    public void afficher() {
        System.out.print(texte);
    }

}

abstract class TexteDecorateur implements TexteInterface {

    private TexteInterface texteDecore;

    public TexteDecorateur(TexteInterface texteDecore) {
        this.texteDecore = texteDecore;
    }

    @Override
    public void afficher() {
        texteDecore.afficher();
    }

}

class TexteEnGras extends TexteDecorateur {

    public TexteEnGras(TexteInterface texteDecore) {
        super(texteDecore);
    }

    @Override
    public void afficher() {
        System.out.print("<b>");
        super.afficher();
        System.out.print("</b>");
    }

}

class TexteEnItalique extends TexteDecorateur {

    public TexteEnItalique(TexteInterface texteDecore) {
        super(texteDecore);
    }

    @Override
    public void afficher() {
        System.out.print("<i>");
        super.afficher();
        System.out.print("</i>");
    }

}

class TexteSouligne extends TexteDecorateur {

    public TexteSouligne(TexteInterface texteDecore) {
        super(texteDecore);
    }

    @Override
    public void afficher() {
        System.out.print("<u>");
        super.afficher();
        System.out.print("</u>");
    }

}

class Main {
    public static void main(String[] args) {
        TexteInterface t1 = new TexteBasique("Hello");
        TexteInterface t2 = new TexteEnGras(new TexteSouligne(t1));
        TexteInterface t3 = new TexteSouligne(new TexteEnGras(new TexteEnItalique(t1)));

        //Affiche "Hello"
        t1.afficher();

        //Affiche "<b><u>Hello</u></b>"
        t2.afficher();

        //Affiche "<u><b><i>Hello</i></b></u>"
        t3.afficher();
    }
}

Diagramme 2

Bien sûr, comme ici le comportement est assez simple et identique pour chaque balise, on aurait pu éventuellement regrouper ces fonctionnalités dans un seul décorateur :

class TexteAvecBalise extends TexteDecorateur {

    private String balise;

    public TexteAvecBalise(TexteInterface texteDecore, String balise) {
        super(texteDecore);
    }

    @Override
    public void afficher() {
        System.out.printf("<%s>", balise);
        super.afficher();
        System.out.printf("</%s>", balise);
    }

}

class Main {
    public static void main(String[] args) {
        TexteInterface t1 = new TexteBasique("Hello");
        TexteInterface t2 = new TexteAvecBalise(new TexteAvecBalise(t1, "u"), "b");
        TexteInterface t3 = new TexteAvecBalise(new TexteAvecBalise(new TexteAvecBalise(t1, "i"), "b"), "u");

        //Affiche "Hello"
        t1.afficher();

        //Affiche "<b><u>Hello</u></b>"
        t2.afficher();

        //Affiche "<u><b><i>Hello</i></b></u>"
        t3.afficher();
    }
}

Troisième exemple : Robots

Pour ce dernier exemple plus complet, nous allons nous placer dans le contexte suivant :

C’est un peu plus complexe que les exemples précédents ! Mais on garde toujours la structure de base du décorateur, avec quelques ajouts.

interface RobotInterface {
    void decrire();
    int calculerPrix();
    int calculerDureeRoutine();
    void effectuerRoutine();
}

interface RobotPremiumInterface extends RobotInterface {
    //Rien, c'est normal (on n'ajoute pas de nouvelles opérations...)
}  

abstract class Robot implements RobotInterface {

    private int prixDeBase;

    public Robot(int prixDeBase) {
        this.prixDeBase = prixDeBase;
    }

    @Override
    public int calculerPrix() {
        return prixDeBase;
    }

    @Override
    public int calculerDureeRoutine() {
        return 0;
    }

    @Override
    public void effectuerRoutine() {
        System.out.println("Début de la routine...");
    }

}

class RobotEntreeGamme extends Robot {

    public RobotEntreeGamme() {
        super(1000);
    }

    @Override
    public void decrire() {
        System.out.println("Robot entrée de gamme");
    }

}

class RobotMilieuGamme extends Robot implements RobotPremiumInterface {

    public RobotMilieuGamme() {
        super(5000);
    }

    @Override
    public void decrire() {
        System.out.println("Robot moyenne gamme");
    }

}

class RobotHautGamme extends Robot implements RobotPremiumInterface {

    public RobotHautGamme() {
        super(10000);
    }

    @Override
    public void decrire() {
        System.out.println("Robot haut gamme");
    }

}

abstract class RobotDecorateur implements RobotInterface {

    private RobotInterface robotDecore;

    public RobotDecorateur(RobotInterface robotDecore) {
        this.robotDecore = robotDecore;
    }

    @Override
    public void decrire() {
        robotDecore.decrire();
    }
    
    @Override
    public int calculerPrix() {
        return robotDecore.calculerPrix();
    }

    @Override
    public int calculerDureeRoutine() {
        return robotDecore.calculerDureeRoutine();           
    }

    @Override
    public void effectuerRoutine() {
        robotDecore.effectuerRoutine();
    }
}

abstract class RobotDecorateurPremium extends RobotDecorateur implements RobotPremiumInterface {

    public RobotDecorateurPremium(RobotPremiumInterface robotDecore) {
        super(robotDecore);
    }

}

class RobotAvecFonctionCafe extends RobotDecorateur {

    public RobotAvecFonctionCafe(RobotInterface robotDecore) {
        super(robotDecore);
    }

    @Override
    public void decrire() {
        super.decrire();
        System.out.println("Fonction café activée");
    }
    
    @Override
    public int calculerPrix() {
        return super.calculerPrix() + 250;
    }

    @Override
    public int calculerDureeRoutine() {
        return super.calculerDureeRoutine() + 5;           
    }

    @Override
    public void effectuerRoutine() {
        super.effectuerRoutine();
        System.out.println("Le robot fait le café...");
    }

}

class RobotAvecFonctionMenage extends RobotDecorateur {

    public RobotAvecFonctionMenage(RobotInterface robotDecore) {
        super(robotDecore);
    }

    @Override
    public void decrire() {
        super.decrire();
        System.out.println("Fonction ménage activée");
    }
    
    @Override
    public int calculerPrix() {
        return super.calculerPrix() + 500;
    }

    @Override
    public int calculerDureeRoutine() {
        return super.calculerDureeRoutine() + 120;           
    }

    @Override
    public void effectuerRoutine() {
        super.effectuerRoutine();
        System.out.println("Le robot nettoie la maison...");
    }
}

class RobotAvecFonctionLessive extends RobotDecorateurPremium {

    public RobotAvecFonctionLessive(RobotPremiumInterface robotDecore) {
        super(robotDecore);
    }

    @Override
    public void decrire() {
        super.decrire();
        System.out.println("Fonction lessive activée");
    }
    
    @Override
    public int calculerPrix() {
        return super.calculerPrix() + 400;
    }

    @Override
    public int calculerDureeRoutine() {
        return super.calculerDureeRoutine() + 30;           
    }

    @Override
    public void effectuerRoutine() {
        super.effectuerRoutine();
        System.out.println("Le robot fait la lessive..");
    }
}

class RobotAvecMiseAJourAutomatique extends RobotDecorateurPremium {

    public RobotAvecMiseAJourAutomatique(RobotPremiumInterface robotDecore) {
        super(robotDecore);
    }

    @Override
    public void decrire() {
        super.decrire();
        System.out.println("Mise à jour automatique activée");
    }
    
    @Override
    public int calculerDureeRoutine() {
        return super.calculerDureeRoutine() + 5;           
    }

    @Override
    public void effectuerRoutine() {
        System.out.println("Mise à jour...");
        super.effectuerRoutine();
    }  
}

class RobotAvecAssurance extends RobotDecorateurPremium {

    public RobotAvecAssurance(RobotPremiumInterface robotDecore) {
        super(robotDecore);
    }

    @Override
    public void decrire() {
        super.decrire();
        System.out.println("Robot assuré");
    }
    
    @Override
    public int calculerPrix() {
        return super.calculerPrix() + 1000;
    }
}

class Main {

    public static void main(String[] args) {
        RobotInterface robot1 = new RobotAvecFonctionCafe(new RobotAvecMiseAJourAutomatique(new RobotAvecAssurance(new RobotMilieuGamme())));
        
        //Affiche 6250
        System.out.println(robot1.calculerPrix());

        /**
         * Affiche :
         * Mise à jour...
         * Début de la routine...
         * Le robot fait le café...
         */
        robot1.effectuerRoutine();

        /**
         * Affiche :
         * Robot moyenne gamme
         * Robot assuré
         * Mise à jour automatique activée
         * Fonction café activée
         */
        robot1.decrire();


        //Impossible : l'assurance est un service premium !
        RobotInterface robot2 = new RobotAvecFonctionMenage(new RobotAvecAssurance(new RobotEntreeGamme()));

        /**
         * Impossible : Malgré le fait que RobotHautGamme soit un RobotPremiumInterface, 
         * RobotAvecFonctionMenage est un RobotInterface simple!
         * Et RobotAvecAssurance attend un RobotPremiumInterface...
         * Il faut donc faire attention à l'ordre ici!
        */
        RobotInterface robot3 = new RobotAvecAssurance(new RobotAvecFonctionMenage(new RobotHautGamme()));
    }

}

Diagramme 3