Développons en Java
v 2.40   Copyright (C) 1999-2023 .   
104. Java et UML 106. Des normes de développement Imprimer Index Index avec sommaire Télécharger le PDF

 

105. Les motifs de conception (design patterns)

 

chapitre    1 0 5

 

Niveau : niveau 3 Intermédiaire 

 

Le nombre d'applications développées avec des technologies orientées objets augmentant, l'idée de réutiliser des techniques pour solutionner des problèmes courants a abouti aux recensements d'un certain nombre de modèles connus sous le nom de motifs de conception (design patterns).

Ces modèles sont définis pour pouvoir être utilisés avec un maximum de langages orientés objets.

Le nombre de ces modèles est en constante augmentation. Le but de ce chapitre n'est pas de tous les recenser mais de présenter les plus utilisés et de fournir un ou des exemples de leur mise en oeuvre avec Java.

Il est habituel de regrouper ces modèles communs dans trois grandes catégories :

Le motif de conception le plus connu est sûrement le modèle MVC (Model View Controller) mis en oeuvre en premier avec SmallTalk.

Ce chapitre contient plusieurs sections :

 

105.1. Les modèles de création

Dans cette catégorie, il existe 5 modèles principaux :

Nom Rôle
Fabrique (Factory) Créer un objet dont le type dépend du contexte
Fabrique abstraite (abstract Factory) Fournir une interface unique pour instancier des objets d'une même famille sans avoir à connaître les classes à instancier
Monteur (Builder)  
Prototype (Prototype) Création d'objet à partir d'un prototype
Singleton (Singleton) Classe qui ne pourra avoir qu'une seule instance

 

105.1.1. Fabrique (Factory)

La fabrique permet de créer un objet dont le type dépend du contexte : cet objet fait partie d'un ensemble de sous-classes. L'objet retourné par la fabrique est donc toujours du type de la classe mère mais grâce au polymorphisme les traitements exécutés sont ceux de l'instance créée.

Ce motif de conception est utilisé lorsqu'à l'exécution il est nécessaire de déterminer dynamiquement quel objet d'un ensemble de sous-classes doit être instancié.

Il est utilisable lorsque :

L'utilisation d'une fabrique permet de rendre l'instanciation d'objets plus flexible que l'utilisation de l'opérateur d'instanciation new.

Ce design pattern peut être implémenté sous plusieurs formes dont les deux principales sont :

Il est possible d'implémenter la fabrique sous la forme d'une classe abstraite et de définir des sous-classes chargées de réaliser les différentes instanciations.

La classe ProduitFactory propose la méthode getProduitA() qui se charge de retourner l'instance créée par la méthode createProduitA().

Les classes ProduitFactory1 et ProduitFactory2 sont les implémentations concrètes de la fabrique. Elles redéfinissent la méthode createProduitA() pour qu'elle renvoie l'instance du produit.

La classe ProduitA est la classe abstraite mère de tous les produits.

Les classes ProduitA1 et ProduitA2 sont des implémentations concrètes de produits.

Exemple : le code sources des différentes classes
package fr.jmdoudoux.dej.factory1;

public class Client {

  public static void main(String[] args) {
    ProduitFactory produitFactory1 = new ProduitFactory1();
    ProduitFactory produitFactory2 = new ProduitFactory2();

    ProduitA produitA = null;

    System.out.println("Utilisation de la premiere fabrique");
    produitA = produitFactory1.getProduitA();
    produitA.methodeA();

    System.out.println("Utilisation de la seconde fabrique");
    produitA = produitFactory2.getProduitA();
    produitA.methodeA();

  }
}
package fr.jmdoudoux.dej.factory1;

public abstract class ProduitFactory {

  public ProduitA getProduitA() {
    return createProduitA();
  }

  protected abstract ProduitA createProduitA();
}

package fr.jmdoudoux.dej.factory1;

public class ProduitFactory1 extends ProduitFactory {

  protected ProduitA createProduitA() {
    return new ProduitA1();
  }
}

package fr.jmdoudoux.dej.factory1;

public class ProduitFactory2 extends ProduitFactory {

  protected ProduitA createProduitA() {
    return new ProduitA2();
  }
}

package fr.jmdoudoux.dej.factory1;

public abstract class ProduitA {

  public abstract void methodeA();
}

package fr.jmdoudoux.dej.factory1;

public class ProduitA1 extends ProduitA {

  public void methodeA() {
    System.out.println("ProduitA1.methodeA()");
  }
}

package fr.jmdoudoux.dej.factory1;

public class ProduitA2 extends ProduitA {

  public void methodeA() {
    System.out.println("ProduitA2.methodeA()");
  }
}

Résultat :
Utilisation de la premiere fabrique
ProduitA1.methodeA()
Utilisation de la seconde fabrique
ProduitA2.methodeA()

Il est possible d'implémenter la fabrique sous la forme d'une classe qui possède une méthode chargée de renvoyer l'instance voulue. La création de cette instance est alors réalisée en fonction de données du contexte (valeurs fournies en paramètres de la méthode, fichier de configuration, paramètres de l'application, ...).

Dans l'exemple ci-dessous, la méthode getProduitA() attend en paramètre une constante qui précise le type d'instance à créer.

Exemple : le code sources des différentes classes
package fr.jmdoudoux.dej.factory2;

public class Client {

  public static void main(String[] args) {
    ProduitFactory produitFactory = new ProduitFactory();

    ProduitA produitA = null;

    produitA = produitFactory.getProduitA(ProduitFactory.TYPE_PRODUITA1);
    produitA.methodeA();

    produitA = produitFactory.getProduitA(ProduitFactory.TYPE_PRODUITA2);
    produitA.methodeA();

    produitA = produitFactory.getProduitA(3);
    produitA.methodeA();

  }
}

package fr.jmdoudoux.dej.factory2;

public class ProduitFactory {

  public static final int TYPE_PRODUITA1 = 1;
  public static final int TYPE_PRODUITA2 = 2;

  public ProduitA getProduitA(int typeProduit) {
    ProduitA produitA = null;

    switch (typeProduit) {
      case TYPE_PRODUITA1:
        produitA = new ProduitA1();
        break;
      case TYPE_PRODUITA2:
        produitA = new ProduitA2();
        break;
      default:
        throw new IllegalArgumentException("Type de produit inconnu");
    }

    return produitA;
  }
}

package fr.jmdoudoux.dej.factory2;

public abstract class ProduitA {

  public abstract void methodeA();
}

package fr.jmdoudoux.dej.factory2;

public class ProduitA1 extends ProduitA {

  public void methodeA() {
    System.out.println("ProduitA1.methodeA()");
  }
}

package fr.jmdoudoux.dej.factory2;

public class ProduitA2 extends ProduitA {

  public void methodeA() {
    System.out.println("ProduitA2.methodeA()");
  }
}

Résultat :
ProduitA1.methodeA()
ProduitA2.methodeA()
java.lang.IllegalArgumentException: Type de produit inconnu
	at fr.jmdoudoux.dej.factory2.ProduitFactory.getProduitA(ProduitFactory.java:19)
	at fr.jmdoudoux.dej.factory2.Client.main(Client.java:16)
Exception in thread "main"

Cette implémentation est plus légère à mettre en oeuvre.

Remarque : c'est une bonne pratique de toujours respecter la même convention de nommage dans le nom des fabriques et dans le nom de la méthode qui renvoie l'instance.

 

105.1.2. Fabrique abstraite (abstract Factory)

Le motif de conception Abstract Factory (fabrique abstraite) permet de fournir une interface unique pour instancier des objets d'une même famille sans avoir à connaître les classes à instancier.

L'utilisation de ce motif est pertinente lorsque :

Le principal avantage de ce motif de conception est d'isoler la création des objets retournés par la fabrique. L'utilisation d'une fabrique abstraite permet de facilement remplacer une fabrique par une autre selon les besoins.

Le motif de conception fabrique abstraite peut être interprété et mis en oeuvre de différentes façons. Le diagramme UML ci-dessous propose une mise en oeuvre possible avec deux familles de deux produits.

Dans cet exemple, les classes suffixées par un chiffre correspondent aux classes relatives à une famille donnée.

Les classes misent en oeuvre sont :

C'est une des classes filles de la fabrique qui se charge de la création des objets d'une famille. Ainsi tous les objets créés doivent hériter d'une classe abstraite qui sert de modèle pour toutes les classes de la famille.

Le client utilise une implémentation concrète de la fabrique abstraite pour obtenir une instance d'un produit créé par la fabrique.

Cette instance est obligatoirement du type de la classe abstraite dont toutes les classes concrètes héritent. Ainsi des objets concrets sont retournés par la fabrique mais le client ne peut utiliser que leur interface abstraite.

Comme il n'y a pas de relation entre le client et la classe concrète retournée par la fabrique, celle-ci peut renvoyer n'importe quelle classe qui hérite de la classe abstraite.

Ceci permet facilement :

Pour prendre en compte une nouvelle famille de produit dans le code client, il suffit simplement d'utiliser la fabrique dédiée à cette famille. Le reste du code client ne change pas. Ceci est beaucoup plus simple que d'avoir à modifier dans le code client l'instanciation des classes concrètes concernées.

Exemple :
package fr.jmdoudoux.dej.abstractfactory;

public class Client {

  public static void main(String[] args) {
    IProduitFactory produitFactory1 = new ProduitFactory1();
    IProduitFactory produitFactory2 = new ProduitFactory2();

    ProduitA produitA = null;
    ProduitB produitB = null;

    System.out.println("Utilisation de la premiere fabrique");
    produitA = produitFactory1.getProduitA();
    produitB = produitFactory1.getProduitB();
    produitA.methodeA();
    produitB.methodeB();

    System.out.println("Utilisation de la seconde fabrique");
    produitA = produitFactory2.getProduitA();
    produitB = produitFactory2.getProduitB();
    produitA.methodeA();
    produitB.methodeB();

  }
}

package fr.jmdoudoux.dej.abstractfactory;

public interface IProduitFactory {

  public ProduitA getProduitA();
  public ProduitB getProduitB();
}


package fr.jmdoudoux.dej.abstractfactory;

public class ProduitFactory1 implements IProduitFactory {

  public ProduitA getProduitA() {
    return new ProduitA1();
  }

  public ProduitB getProduitB() {
    return new ProduitB1();
  }
}

package fr.jmdoudoux.dej.abstractfactory;

public class ProduitFactory2 implements IProduitFactory {

  public ProduitA getProduitA() {
    return new ProduitA2();
  }

  public ProduitB getProduitB() {
    return new ProduitB2();
  }
}

package fr.jmdoudoux.dej.abstractfactory;

public abstract class ProduitA {

  public abstract void methodeA();
}

package fr.jmdoudoux.dej.abstractfactory;

public class ProduitA1 extends ProduitA {

  public void methodeA() {
    System.out.println("ProduitA1.methodeA()");
  }
}

package fr.jmdoudoux.dej.abstractfactory;

public class ProduitA2 extends ProduitA {

  public void methodeA() {
    System.out.println("ProduitA2.methodeA()");
  }
}

package fr.jmdoudoux.dej.abstractfactory;

public abstract class ProduitB {

  public abstract void methodeB();
}

package fr.jmdoudoux.dej.abstractfactory;

public class ProduitB1 extends ProduitB {

  public void methodeB() {
    System.out.println("ProduitB1.methodeB()");
  }
}

package fr.jmdoudoux.dej.abstractfactory;

public class ProduitB2 extends ProduitB {

  public void methodeB() {
    System.out.println("ProduitB2.methodeB()");
  }
}

Résultat :
Utilisation de la premiere fabrique
ProduitA1.methodeA()
ProduitB1.methodeB()
Utilisation de la seconde fabrique
ProduitA2.methodeA()
ProduitB2.methodeB()

Une fabrique concrète est généralement un singleton.

 

105.1.3. Monteur (Builder)

 

 

en construction
Cette section sera développée dans une version future de ce document

 

105.1.4. Prototype (Prototype)

 

 

en construction
Cette section sera développée dans une version future de ce document

 

105.1.5. Singleton (Singleton)

Ce motif de conception propose de n'avoir qu'une seule et unique instance d'une classe dans une application.

Le Singleton est fréquemment utilisé dans les applications car il n'est pas rare de ne vouloir qu'une seule instance pour certaines fonctionnalités (pool, cache, ...). Ce modèle est aussi particulièrement utile pour le développement d'objets de type gestionnaire. En effet ce type d'objet doit être unique car il gère d'autres objets par exemple un gestionnaire de logs.

La mise en oeuvre du design pattern Singleton doit :

Un singleton peut maintenir un état (stateful) ou non (stateless).

La compréhension de ce motif de conception est facile mais son implémentation ne l'est pas toujours, notamment, à cause de quelques subtilités de Java et d'une attention particulière à apporter dans le cas d'une utilisation multithreads.

Ce design pattern peut avoir plusieurs implémentations en Java.

1) une implémentation classique avec initialisation tardive

Exemple :
public final class MonSingleton {

  private static MonSingleton instance;

  private MonSingleton() {
    // traitement du constructeur
  }

  public static MonSingleton getInstance() {
    if (instance == null) {
      instance = new MonSingleton();
    }
    return instance;
  }

  @Override
  public Object clone() throws CloneNotSupportedException {
    throw new CloneNotSupportedException();
  }
}

Cette implémentation est simple mais malheureusement, elle n'est pas threadsafe. Dans un contexte multithreads, il est possible que les deux premiers appels concomitants puissent créer deux instances. Chaque thread reçoit alors une instance distincte ce qui ne répond pas aux contraintes du design pattern.

 

2) une implémentation thread-safe classique avec initialisation tardive

Le plus simple et le plus sûr pour éviter ce problème est de sécuriser l'accès au getter avec le mot clé synchronized.

Exemple :
public final class MonSingleton {

  private static MonSingleton instance;

  private MonSingleton() {
    // traitement du constructeur
  }

  public static synchronized MonSingleton getlnstance() {
    if (instance == null) {
      instance = new MonSingleton();
    }
    return instance;
  }

  @Override
  public Object clone() throws CloneNotSupportedException {
    throw new CloneNotSupportedException();
  }
}

Cette solution est thread-safe mais elle induit un coût en terme de performance, lié à la synchronisation de la méthode, qui peut devenir génant si la méthode est appelée fréquemment de façon concomitante.

 

3) une implémentation classique non thread-safe avec initialisation tardive

La partie qui doit vraiment être thread safe est la création de l'instance ce qui correspond uniquement à la première invocation de la méthode. Il peut être alors tentant de ne synchroniser que la création de l'instance.

Exemple :
public final class MonSingleton {

  private static MonSingleton instance;

  private MonSingleton() {
    // traitement du constructeur
  }

  public static MonSingleton getInstance() {
    if (instance == null) {
      synchronized (MonSingleton.class) {
        instance = new MonSingleton();
      }
    }
    return instance;
  }

  @Override
  public Object clone() throws CloneNotSupportedException {
    throw new CloneNotSupportedException();
  }
}

Le but est d'éviter de poser un verrou sur le moniteur de la classe à chaque invocation de la méthode. Malheureusement, cette solution n'est pas thread-safe.

Le thread 1 entre dans le bloc securisé et avant l'assignation de la reference créé par le constructeur à la variable instance, le scheduler passe la main au thread 2 qui teste si l'instance est null et c'est le cas donc il va attendre la sortie du bloc sécurisé du thread 1 pour exécuter à son tour le bloc de code sécurisé. Les deux threads obtiennent chacun une instance distincte.

 

4) une implémentation classique avec initialisation tardive non thread-safe avec double-checked

Une autre implémentation utilisée est celle nommée double-checked : elle consiste à retester si l'instance est bien null après la pose du verrou au cas ou un autre thread aurait déjà passé le premier test.

Exemple :
public final class MonSingleton {

  private static MonSingleton instance;

  private MonSingleton() {
    // traitement du constructeur
  }

  public static MonSingleton getInstance() {
    if (instance == null) {
      synchronized (MonSingleton.class) {
        if (instance == null) {
          instance = new MonSingleton();
        }
      }
    }
    return instance;
  }

  @Override
  public Object clone() throws CloneNotSupportedException {
    throw new CloneNotSupportedException();
  }
}

Cette solution, elle aussi, peut ne pas fonctionner non plus correctement si le compilateur, par optimisation, assigne la référence alors que l'objet n'est pas encore initialisé (son constructeur n'est pas encore invoqué).

Ainsi le premier thread pourrait ne pas avoir une instance entièrement initialisée.

 

5) une implémentation threadsafe avec initialisation au chargement de la classe

Cette implémentation qui exploite une spécificité de Java est simple, rapide et sûre.

Exemple :
public final class MonSingleton {

  private static MonSingleton instance = new MonSingleton();

  public static MonSingleton getlnstance() {
    return instance;
  }

  private MonSingleton() {
  }
}

Cette implémentation est thread-safe car les spécifications du langage Java impose à la JVM d'avoir initialisée une variable static avant sa première utilisation.

 

6) une implémentation threadsafe avec initialisation tardive

L'utilisation d'une classe interne statique permet une initialisation tardive garantie par les spécifications de la JVM.

Exemple :
public class MonSingleton {
  private MonSingleton() {
  }

  private static class MonSingletonWrapper {
    private final static MonSingleton instance = new MonSingleton();
  }

  public static MonSingleton getInstance() {
    return MonSingletonWrapper.instance;
  }
}

 

Il existe plusieurs précautions à prendre lors de la mise en oeuvre du Singleton. Il est tentant d'utiliser des singletons mais ceux-ci peuvent être à l'origine de certaines difficultés dans des cas biens précis :

 

105.2. Les modèles de structuration

 

 

105.2.1. Façade (Facade)

Une bonne pratique de conception est d'essayer de limiter le couplage existant entre des fonctionnalités proposées par différentes entités. Dans la pratique, il est préférable de développer un petit nombre de classes et de proposer une classe pour les utiliser. C'est ce que propose le motif de conception façade.

Le but est de proposer une interface facilitant la mise en oeuvre d'un ensemble de classes généralement regroupées dans un ou plusieurs sous-systèmes. Le motif Façade permet d'offrir un niveau d'abstraction entre l'ensemble de classes et celles qui souhaitent les utiliser en proposant une interface de plus haut niveau pour utiliser les classes du sous-système.

Exemple : un client qui utilise des classes d'un sous-système directement

Cet exemple volontairement simpliste va être modifié pour mettre en oeuvre le modèle de conception Façade.

Employer ce modèle aide à simplifier une grande partie de l'interface pour utiliser les classes du sous-système. Il facilite la mise en oeuvre de plusieurs classes en fournissant une couche d'abstraction supplémentaire entre ces dernières et les classes qui les utilisent. Le modèle Façade permet donc de faciliter la compréhension et l'utilisation d'un sous-système complexe que ce soit pour faciliter l'utilisation de tout ou partie du système ou pour forcer une utilisation particulière de celui-ci.

Les classes du sous-système encapsulent les traitements qui seront exécutés par des appels de méthodes de l'objet Façade. Ces classes ne doivent pas connaître ni, de surcroît, avoir de référence sur l'objet Façade.

La façade propose un ensemble de méthodes qui vont réaliser les appels nécessaires aux classes du sous-système pour offrir des fonctionnalités cohérentes. Elle propose une interface pour faciliter l'utilisation du sous-système en implémentant les traitements requis pour utiliser les classes de celui-ci.

La classe qui implémente le modèle Façade encapsule les appels aux différentes classes impliquées dans l'exécution d'un traitement cohérent. Elle fait donc office de point d'entrée pour utiliser le sous-système.

Ce modèle requiert plusieurs classes :

Exemple :

Le code à utiliser dans la classe client est réduit ce qui va en faciliter la maintenance. La façade masque donc les complexités du sous-système utilisé et fournit une interface simple d'accès pour les clients qui l'utilisent.

Exemple :
public class ClientTestFacade {
  public static void main(String[] argv) {
    TestFacade facade = new TestFacade();

    facade.methode1();
    facade.methode2();
  }
}

public class TestFacade {

  ClasseA classeA;
  ClasseB classeB;
  ClasseC classeC;
  ClasseD classeD;

  public TestFacade() {
    classeA = new ClasseA();
    classeB = new ClasseB();
    classeC = new ClasseC();
    classeD = new ClasseD();
  }

  public void methode1() {
    System.out.println("Methode2 : ");
    classeA.methodeA();
    classeD.methodeD();
    classeC.methodeC();
  }

  public void methode2() {
    System.out.println("Methode1 : ");
    classeB.methodeB();
    classeC.methodeC();
  }
}

public class ClasseA {
  public void methodeA() {
    System.out.println(" - MethodeA ClasseA");
  }
}

public class ClasseB {
  public void methodeB() {
    System.out.println(" - MethodeB Classe B");
  }
}

public class ClasseC {
  public void methodeC() {
    System.out.println(" - MethodeC ClasseC");
  }
}

public class ClasseD {
  public void methodeD() {
    System.out.println(" - MethodeD ClasseD");
  }
}

Résultat :
Methode2 : 
 - MethodeA ClasseA
 - MethodeD ClasseD
 - MethodeC ClasseC
Methode1 : 
 - MethodeB Classe B
 - MethodeC ClasseC

Le modèle Façade peut être utilisé pour :

L'utilisation d'une façade permet au client de limiter le nombre d'objets à utiliser puisqu'il se contente simplement d'appeler une ou plusieurs méthodes de la façade. Ce sont ces méthodes qui vont utiliser les classes du sous-système, masquant ainsi au client toute la complexité de leur mise en oeuvre.

Il peut être pratique de définir une façade sans état (les méthodes de la façade n'utilisent pas de membres statiques de la classe) car dans ce cas, une seule et unique instance de la façade peut être définie côté client en mettant en oeuvre le modèle de conception singleton prévu à cet effet.

Il est possible de proposer des fonctionnalités supplémentaires dans la façade qui enrichissent la mise en oeuvre du sous-système.

La façade peut aussi être utilisée pour masquer le sous-système. Elle peut encapsuler les classes du sous-système et ainsi cacher au client l'existence du sous-système. Cette mise en oeuvre facilite le remplacement du sous-système par un autre : il suffit simplement de modifier la façade pour que le client continue à fonctionner.

Il est possible que toutes les fonctionnalités proposées par les classes du sous-système ne soient pas accessibles par la façade : son but est de simplifier leurs utilisations mais pas de proposer toutes les fonctionnalités.

Ce motif de conception est largement utilisé.

 

105.2.2. Décorateur (Decorator)

Le motif de conception décorateur (decorator en anglais) permet d'ajouter des fonctionnalités à un objet en mettant en oeuvre une solution plus souple que l'héritage : il permet d'ajouter des fonctionnalités à une ou plusieurs méthodes existantes d'une classe dynamiquement.

La programmation orientée objet propose l'héritage pour ajouter des fonctionnalités à une classe, cependant l'héritage présente quelques contraintes et il n'est pas toujours possible de le mettre en oeuvre (par exemple si la classe est finale). L'héritage crée une nouvelle classe qui reprend les fonctionnalités de  la classe mère et les modifie ou les enrichie. Mais il présente quelques inconvénients :

Avec l'héritage, il serait nécessaire de définir autant de classes filles que de cas ce qui peut vite devenir ingérable. Avec l'utilisation d'un décorateur, il suffit de définir un décorateur pour chaque fonctionnalité et de les utiliser par combinaison en fonction des besoins. L'héritage ajoute des fonctionnalités de façon statique (à la compilation) alors que le décorateur ajoute des fonctionnalités de façon dynamique (à l'exécution).

Le modèle de conception décorateur apporte une solution à ces trois inconvénients et propose donc une alternative à l'héritage.

Le motif de conception décorateur permet de définir un ensemble de classes possédant une base commune mais proposant chacune des variantes sans utiliser l'héritage qui est le mécanisme de base par la programmation orientée objet. Ceci permet d'enrichir une classe avec des fonctionnalités supplémentaires.

Ce motif est dédié à la création de variantes d'une classe plutôt que d'avoir une seule classe prenant en compte ces variantes. Il permet aussi de réaliser des combinaisons de plusieurs variantes.

Ce motif de conception est donc généralement utilisé lorsqu'il n'est pas possible de prédéfinir le nombre de combinaisons induites par l'ajout de nombreuses fonctionnalités ou si ce nombre est trop important. Le principe du motif de conception décorateur est d'utiliser la composition : le décorateur contient un objet décoré. L'appel d'une méthode du décorateur provoque l'exécution de la méthode correspondante du décoré et des fonctionnalités ajoutées par le décorateur.

Le motif décorateur repose sur deux entités :

Le décorateur encapsule le décoré dont l'instance est généralement fournie dans les paramètres d'un constructeur. Il est important que l'interface du décorateur reprenne celle de l'objet décoré. Pour permettre de combiner les décorations, le décoré et le décorateur doivent implémenter une interface commune.

La combinaison peut alors être répétée pour construire un objet qui va contenir les différentes fonctionnalités proposées par les décorateurs utilisés.

Le motif de conception décorateur est particulièrement utile dans plusieurs cas :

Il permet de créer un objet qui va être composé des fonctionnalités requises par ajouts successifs des différents décorateurs proposant les fonctionnalités requises.

Un des avantages de ce motif de conception est de n'avoir à créer qu'une seule classe pour proposer des fonctionnalités supplémentaires aux classes qui mettent en oeuvre ce motif. Avec l'héritage, il serait nécessaire de créer autant de classes filles que de classes concernées ou de gérer la fonctionnalité dans une classe mère en modifiant cette dernière pour prendre en compte cet ajout avec tous les risques que cela peut engendrer.

Pour mettre en oeuvre ce motif, il faut :

1) définir une interface qui va déclarer toutes les fonctionnalités des décorés.

Exemple : interface Traitement
package fr.jmdoudoux.dej.dp.decorateur;

public interface Traitement {
  public void Operation();
}

2) définir un décorateur de base qui implémente l'interface et possède une référence sur une instance de l'interface. Cette référence est le décoré qui va être enrichi des fonctionnalités du décorateur.

Exemple : classe abstraite TraitementDecorateur
package fr.jmdoudoux.dej.dp.decorateur;

public abstract class TraitementDecorateur implements Traitement {

  protected Traitement traitement;
  
  public TraitementDecorateur() 
  {
  }
  
  public TraitementDecorateur(Traitement traitement) 
  {
    this.traitement = traitement;
  }

  public void Operation() {
    if (traitement != null)
    {
      traitement.Operation();
    }
  }
}

3) définir les décorateurs qui héritent du décorateur de base et implémentent les fonctionnalités supplémentaires qu'ils sont chargés de proposer.

Exemple : TraitementDecorateur1
package fr.jmdoudoux.dej.dp.decorateur;

public class TraitementDecorateur1 extends TraitementDecorateur {

  public TraitementDecorateur1() {
    super();
  }

  public TraitementDecorateur1(Traitement traitement) {
    super(traitement);
  }

  @Override
  public void Operation() {
    if (traitement != null)
    {
      traitement.Operation();
    }
    System.out.println("TraitementDecorateur1.Operation()");
  }
}

Exemple : TraitementDecorateur2
package fr.jmdoudoux.dej.dp.decorateur;

public class TraitementDecorateur2 extends TraitementDecorateur {

  public TraitementDecorateur2() {
    super();
  }

  public TraitementDecorateur2(Traitement traitement) {
    super(traitement);
  }

  @Override
  public void Operation() {
    if (traitement != null) {
      traitement.Operation();
    }

    System.out.println("TraitementDecorateur2.Operation()");
  }
}

Exemple : TraitementDecorateur3
package fr.jmdoudoux.dej.dp.decorateur;

public class TraitementDecorateur3 extends TraitementDecorateur {

  public TraitementDecorateur3() {
    super();
  }

  public TraitementDecorateur3(Traitement traitement) {
    super(traitement);
  }

  @Override
  public void Operation() {
    if (traitement != null)
    {
      traitement.Operation();
    }
    System.out.println("TraitementDecorateur3.Operation()");
  }
}

Il est possible de fournir une classe d'implémentation par défaut.

Il est pratique d'utiliser le motif de conception fabrique pour construire l'objet décoré finale. Dans ce cas, une implémentation par défaut de l'interface peut être utile.

Exemple : TraitementTest.java
package fr.jmdoudoux.dej.dp.decorateur;

public class TraitementTest {

  public static void main(String[] args) {
    System.out.println("traitement 1 2 3");
    Traitement traitement123 = new TraitementDecorateur3(
      new TraitementDecorateur2(new TraitementDecorateur1()));
    traitement123.Operation();
    
    System.out.println("traitement 1 3");
    Traitement traitement13 = new TraitementDecorateur3(new TraitementDecorateur1());
    traitement13.Operation();
  }
}

Résultat d'exécution :
traitement 1 2 3
TraitementDecorateur1.Operation()
TraitementDecorateur2.Operation()
TraitementDecorateur3.Operation()
traitement 1 3
TraitementDecorateur1.Operation()
TraitementDecorateur3.Operation()

L'API de base de Java utilise le motif de conception décorateur notamment dans l'API IO

 

105.3. Les modèles de comportement

 

 

en construction
La suite de ce chapitre sera développée dans une version future de ce document

 


104. Java et UML 106. Des normes de développement Imprimer Index Index avec sommaire Télécharger le PDF    
Développons en Java
v 2.40   Copyright (C) 1999-2023 .