Patrons de conception II
Dans cette unité, nous allons étendre notre répertoire précédent de modèles de conception avec une sélection de trois nouveaux modèles : « abstract factory (usine)», «flyweight» et « décorateur ».
Patrons de fabrication
Dans le cours précédent, nous avons déjà exploré un premier modèle d’usine, le modèle du singleton.
Quel est l’intérêt principal du patron du singleton ?
L’intérêt principal du patron du singleton est d’assurer qu’une entité donnée n’existe qu’une seule fois. Il dissimule la création d’objets derrière une méthode getInstance, en empêchant en interne la création d’entités concurrentes.
Dans ce qui suit, nous allons examiner un second patron de création, le patron « usine abstraite », qui est souvent utile pour définir un dénominateur commun pour des usines alternatives.
Abstract factory
Intention
« Fournir une interface pour créer des familles d’objets liés ou dépendants sans spécifier leurs classes concrètes. »
-- GoF, Design Patterns
Motivation
Nous voulons différents styles visuels, c’est-à-dire différentes apparences pour les éléments de l’interface utilisateur :
-
« UI claire », avec une barre de défilement toujours visible :

-
« UI sombre », avec une barre de défilement visible seulement pendant le défilement :

Pouvez-vous penser à d’autres exemples ?
Pratiquement tout ce qui offre des interfaces alternatives. Mode clair ou sombre de macOS, mais aussi des choix plus drastiques comme l’UI classique d’IntelliJ (puissante, pour professionnels) versus l’UI moderne (minimaliste, pour débutants).
Où est le problème ?
- Nous avons toujours besoin des mêmes éléments (ou presque).
- Ils diffèrent par leur nature et leur comportement :
- Disposition des éléments
- Apparence des éléments
- Comportement des éléments
- Coder en dur un style visuel particulier rend les changements futurs difficiles.
Patron : créer une classe d’usine abstraite commune ; chaque style visuel fournira ensuite sa propre usine.
Diagramme UML
L’idée générale du patron d’usine (factory) abstraite est d’avoir plusieurs usines (p. ex. une pour chaque style visuel), toutes implémentant la même classe ou interface abstraite.
- Au lieu de coder en dur une usine spécifique partout dans le code, on choisit une usine une seule fois (pouvant être stockée dans une variable).
- L’usine abstraite délègue la création des produits (barres de défilement, fenêtres, …) aux implémentations concrètes.
- Changer de style visuel devient facile, car chaque usine implémente la même interface ou classe parente.
Crédit image : GoF – Design Patterns, p. 87.
Utilisation
Pour appliquer le patron, nous devons simplement nous assurer que toutes les alternatives (usines) respectent une usine abstraite commune :
- Commencer avec une interface ou une classe abstraite commune :
-
Puis on implémente les factories concrètes :
- Motif factory (Motif = X Window System GUI toolkit):
- PM factory (PM = IMB OS/2 Presentation Manager):
-
Finalement, pour utiliser l’un ou l’autre style visuel, il suffit d’assigner une seule fois l’une des usines concrètes à une variable de factory :
public static void main(String[] args) {
// Here we decide for "Motif", but we can easily toggle
// for a different look and feel.
AbstractFactory lookAndFeelFactory = new MotifWidgetFactory();
// The remainder is completely independent
// of which factory was initialized:
Window myFirstWindow = lookAndFeelFactory.createWindow();
Window mySecondWindow = lookAndFeelFactory.createWindow();
ScrollBar myFirstScrollBar = lookAndFeelFactory.createScrollBar();
// and so on ...
}
Autres patrons de fabrication
Voici une liste incomplète d’autres patrons de fabrication (certains dépassent la portée de ce cours) :
- Constructeur (Builder) : Séparer la construction d’un objet complexe de sa représentation, de sorte que le même processus de construction puisse créer différentes représentations.
- Méthode fabrique (Factory Method) : Définir une interface pour créer un objet, mais laisser les sous-classes décider quelle classe instancier. (Version légère de la factory/usine abstraite.)
- Prototype : Spécifier les types d’objets à créer en utilisant une instance prototype. Créer de nouvelles instances en clonant le prototype.
Patrons structurels
Dans le cours précédent, nous avons déjà étudié un premier patron structurel, le patron composite.
Quel est l’intérêt principal du patron composite ?
Définir des actions uniformes sur des structures imbriquées, permettant un comportement fiable et cohérent.
Flyweight
Intention
« Utiliser le partage pour supporter efficacement un grand nombre d’objets à granularité fine. »
-- GoF, Design Patterns
Motivation
De nombreux objets ayant un état intrinsèque peuvent être coûteux, tant en mémoire qu’en création d’objets.
Un exemple est un éditeur de texte. Pour rendre correctement chaque caractère sur une page, nous pourrions implémenter chaque caractère comme un objet individuel contenant intrinsèquement les informations nécessaires pour s’afficher :
- Police
- Position
- Caractère
- Gras / Italique / Souligné / Barré
Illustrations de caractères de document comme objets individuels.
Pourquoi vouloir que les objets sachent s’ils se rendent à une position donnée en pixels ?
L’impression se fait pixel par pixel, ligne par ligne. En utilisant le polymorphisme, on peut facilement déterminer la couleur d’un pixel en vérifiant simplement si un objet intersecte une position donnée (teinter le pixel en noir).
Nous avons utilisé le même principe lors de la dernière séance pour notre imprimante simple (exemple du patron composite, un * était imprimé lorsqu’un composite intersectait la position d’impression).
Cependant, une telle implémentation naïve créerait des centaines (ou milliers) d’objets pour n’importe quelle page de document :
Illustration tirée de GoF, Design Patterns
Si nous considérons toutes les informations typographiques comme un état intrinsèque, les documents plus volumineux ne pourraient pas être mis à l’échelle — la consommation de ressources serait linéaire au nombre de caractères :
Illustration tirée de GoF, Design Patterns
L’essence du patron flyweight est d’externaliser tout ce qui n’a pas besoin d’être un état intrinsèque vers un pool d’objets flyweight partagés.
- Les objets du pool flyweight sont initialisés une seule fois et peuvent être réutilisés à chaque usage.
- La consommation de ressources pour les glyphes du document est drastiquement réduite, ne gardant que l’information impossible à externaliser ou à calculer.
Illustration tirée de GoF, Design Patterns
- La figure ci-dessus montre comment des flyweights génériques ayant un état intrinsèque sont réutilisés dans différents contextes.
- Un exemple est le caractère
0, qui référence le même objet dans le pool flyweight, n’y ajoutant que l’information contextuelle.
Diagramme UML
Nous allons maintenant rendre plus concret le fonctionnement interne du patron flyweight en examinant une implémentation compatible avec ce patron pour l’exemple de l’éditeur.
Pour commencer, réexaminons comment nous pouvons réutiliser un patron de conception déjà vu pour implémenter les propriétés typographiques pouvant être attribuées aux colonnes, aux lignes ou aux caractères individuels.
??? question "Comment réutiliser un patron de conception précédent pour refléter la structure imbriquée des glyphes dans un diagramme de classes ?"
Nous pouvons imiter de près ce que nous avons vu avec le patron composite. Un composant (glyphe) peut contenir d’autres composants (glyphes), et chacun peut se voir attribuer une typographie ou l’utiliser pour le rendu :
<img src="svg/flyweight-glyph.svg" width="90%" style="display: block; margin-left: auto; margin-right: auto;"/>
Nous pouvons considérer le Contexte comme tout ce qui est nécessaire pour représenter l’état externe (non
intrinsèque), tandis que les flyweights préparés sont réduits à des informations partageables, sans contexte.
Pour un caractère, ce serait :
- Externe : Position, certaines propriétés typographiques (pouvant être calculées pour optimiser encore l’espace)
- Intrinsèque : Le caractère et sa forme graphique
Notez que les flyweights ne sont pas limités aux objets feuille dans la structure composite :
- Nous pouvons aussi imaginer une construction graduelle d’un état par une hiérarchie de flyweights, enrichissant
progressivement le contexte.
Dans l’exemple de l’éditeur de texte, on peut considérer les lignes ou les colonnes comme des flyweights « non partagés », ajoutant au contexte nécessaire pour le rendu absolu des caractères. - Le diagramme de classes ci-dessous montre comment des flyweights hiérarchiques sont partagés :
Illustration tirée de GoF, Design Patterns
La fabrique garde la trace des instances de flyweights partageables, de manière similaire au patron singleton :
- Si l’instance du flyweight n’a pas encore été initialisée (
== null), un nouveau flyweight est créé. - Sinon, le flyweight partageable est retourné.
Exemple de code pour la FlyweightFactory :
getFlyweight(String key) {
if (flyweightPool.contains(key)) {
return flyweightPool.get(key);
} else {
Flyweight flyweight = new Flyweight(glyph);
flyweightPool.put(key, flyweight);
return flyweight;
}
}
Étant donné l’implémentation ci-dessus, quel type de collection le pool utiliserait-il ?
Une map. Chaque entrée est une paire clé-valeur reliant les identifiants de flyweight à leurs objets partageables dans le pool.
Utilisation
- Ce n’est pas parce que nous pouvons implémenter le patron Flyweight que nous devrions le faire.
- Le flyweight est avant tout un patron motivé par l’optimisation des ressources.
- Pour exploiter pleinement son potentiel, plusieurs conditions doivent être réunies :
- Réplication massive d’objets similaires dans une implémentation naïve.
- État commun dans ces objets similaires, pouvant être externalisé contextuellement (beaucoup de caractères partagent une typographie semblable, stockable ailleurs).
- L’information intrinsèque, donc unique, dans les flyweights peut être minimisée (les glyphes ne doivent stocker que le caractère qu’ils représentent).
- De plus, l’état intrinsèque peut parfois suivre une structure prédictible et être calculé plutôt que stocké (le décalage selon la position dans une ligne peut être calculé).
Le flyweight est destiné aux structures d’objets massives et répétitives.
À moins de gérer des centaines ou des milliers d’objets, majoritairement similaires et suivant un schéma généralisable, ne considérez pas le patron flyweight.
Décorateur
Intention
« Ajouter dynamiquement des responsabilités à un objet. Les décorateurs offrent une alternative flexible à l’héritage pour étendre les fonctionnalités. »
-- GoF, Design Patterns
Motivation
Les décorateurs sont utiles lorsque vous souhaitez ajouter des responsabilités à un objet sans lier ces nouveautés à toute la classe (par héritage).
Vous pouvez voir le décorateur comme un proxy enveloppant un objet existant. Tous les appels de méthodes déjà existants sont transmis tels quels, mais le proxy (comme un emballage) ajoute des méthodes ou comportements supplémentaires.
Diagramme UML
- Comme le
Décorateurdoit transmettre tous les appels de méthodes à sonComposantConcretenveloppé, il possède naturellement la même interface (Composant). - De plus, il peut y avoir plusieurs
DécorateursConcretsoffrant chacun un comportement ou un état supplémentaire.
Utilisation
Tout ce qui touche à la nourriture ou aux boissons constitue un excellent exemple pour le patron décorateur — simplement parce qu’il existe souvent des variantes « améliorées » de plats ou de boissons de base.
Implémentons un patron décorateur pour une tasse de café :
/**
* The coffee interfaces corresponds to "Component" in the above UML diagram.
*/
public interface Coffee {
/**
* Returns the costs for a cup of coffee.
*/
double cost();
/**
* Returns a string description of the beverage.
*/
String description();
}
Les interfaces seules ne font pas grand-chose, créons donc une implémentation concrète, par exemple CaféFiltré :
/**
* FilterCoffee corresponds to "Concrete Component".
*/
public class FilterCoffee implements Coffee {
@Override
public int cost() {
return 150;
}
@Override
public String description() {
return "Filter coffee - the preferred beverage of any software engineer.";
}
}
Passons maintenant aux Décorateurs. Nous convenons tous qu’il existe plusieurs façons de décorer un café :
- Glaçons
- Lait
- Sucre
Chacun de ces décorateurs modifie le prix et la description, ils sont donc tous qualifiés de Décorateur, enveloppant
notre composant Café. Il est préférable de créer une classe abstraite commune enveloppant le Café :
Et bien sûr, nous avons finalement besoin d’un décorateur :
public class IceDecorator extends CoffeeDecorator {
public IceDecorator(Coffee coffee) {
super(coffee);
}
/**
* Ice cubes cost 50 cents extra.
*/
@Override
public int cost() {
return coffee.cost() + 50;
}
/**
* Ice must be mentioned as addendum in description.
*/
@Override
public String description() {
return coffee.description() + " Enhanced with ice cubes.";
}
}
Voyons comment créer un café glacé :
public static void main(String[] args) {
Coffee basicFilterCoffee = new FilterCoffee();
System.out.println(basicFilterCoffee.description() + "\nCosts: " + basicFilterCoffee.cost());
// Now decorate with ice:
Coffee icedCoffee = new IceDecorator(basicFilterCoffee);
System.out.println(icedCoffee.description() + "\nCosts: " + icedCoffee.cost());
}
L’exemple du café glacé illustre comment le patron décorateur peut être utilisé pour contourner les restrictions de l’héritage. Nous n’aurions pas pu implémenter la même chose avec un héritage simple, car l’héritage multiple n’est pas possible en Java.
Comment créer un café glacé avec du lait ?
Comme les Décorateurs héritent de Café, ils peuvent envelopper d’autres Décorateurs.
Il suffit donc d’ajouter une autre classe DécorateurLait et de l’utiliser pour envelopper l’objet caféGlacé existant : ```java
Autres patrons structurels
Voici une liste incomplète d’autres patrons structurels (au-delà de la portée de ce cours) :
- Adaptateur : Convertir des interfaces ou classes abstraites pour les adapter à d’autres interfaces, afin de créer une compatibilité qui ne peut être déduite par le système de types.
- Façade : Fournir une interface unifiée pour un ensemble d’interfaces, rendant les interfaces individuelles plus faciles à gérer/utiliser.
- Proxy : Fournir un substitut à un autre objet pour contrôler l’accès.
Patrons comportementaux
Dans cette unité, nous n’examinerons pas de nouveaux patrons comportementaux.
Autres patrons comportementaux
Voici une liste incomplète de patrons comportementaux supplémentaires (au-delà de la portée de ce cours) :
- Chaîne de responsabilité : Découpler les actions d’une responsabilité unique, laisser la chaîne décider qui est le plus apte.
- Interpréteur : Regrouper les définitions d’un langage avec une classe interpréteur capable d’interpréter les phrases.
- Itérateur : Une abstraction pour parcourir des données structurées sans nécessiter la connaissance de leur structure interne.
- Médiateur : Empêcher les objets de se référer directement les uns aux autres, en brisant le couplage via un médiateur.
- Memento : Capturer l’état d’un objet pour pouvoir le restaurer ultérieurement.
- État : Permettre à un objet de passer entre différents comportements selon son état interne.
- Stratégie : Encapsuler des algorithmes et les proposer sous une forme interchangeable.
- Méthode Template : Définir seulement les étapes macro d’un algorithme sur place, déléguer les détails des étapes aux sous-classes.