TP 1 — Patrons de conception
Crêpes et gaufres : Décorateur, Composite, Fabrique simple, ObjetNull et Stratégie — l'énoncé, puis un corrigé pas à pas avec le code complet de chaque étape.
Objectifs et mise en place
- comprendre ce qu'est un patron et ce que ça veut dire de l'employer ;
- comprendre et retenir le principe des patrons Décorateur, Composite, Fabrique, ObjetNull et Stratégie utilisés dans ce TP.
Mise en place
Créez un projet Java tp1-patrons-conception dans un répertoire dédié à l'ensemble de vos TP
(par ex. r304). Choisissez Maven comme build system, et cochez la création d'un dépôt git
local : des commits réguliers au fil des exercices, c'est une bonne habitude. Le cours sur les
patrons contient une présentation des patrons utilisés ici — n'hésitez pas à le consulter.
Le fil rouge du TP : vendre des crêpes et gaufres sur lesquelles on rajoute des garnitures. Deux opérations sur un dessert : récupérer son prix et son nom.
| Nom | Prix | |
|---|---|---|
| Desserts | Crêpe | 3 € |
| Gaufre | 3,50 € | |
| Garnitures | Chantilly | 1 € |
| Chocolat | 1,50 € | |
| Coulis de fraise | 2 € |
Chaque partie du sujet se travaille en deux temps : d'abord un temps de réflexion (avec de l'UML), puis un temps d'implémentation — et vous serez de moins en moins guidés. Essayez par vous-même avant d'ouvrir les corrigés : c'est en butant qu'on retient.
1. Décorateur : le double supplément chantilly-chocolat
Une garniture est un dessert qui contient un dessert : le dessert joue le rôle de
Component, la Garniture celui de Decorator. Cette conception permet plusieurs garnitures
sur un même dessert, empêche de prendre une garniture seule ou de garnir une crêpe par une
gaufre :
Dessert d = new Chocolat(new Chantilly(new Gaufre()));
System.out.println(d); // Gaufre chantilly chocolat – 6€Vérifier avant d'avancer
Un fichier de test est fourni sur Moodle : intégrez-le dans src/test/java/patisserie/garniture
en adaptant noms et imports. Les tests se vérifient automatiquement après chaque modification —
lancez-les régulièrement, notamment avant chaque commit. Complétez aussi l'UML distribué
avec le patron Décorateur tel qu'appliqué, et faites valider par votre enseignant·e.
La clé du Décorateur tient en deux phrases :
Garniturehérite deDessert→ elle peut être utilisée partout où on attend un dessert (substituabilité) ;Garniturecontient le dessert qu'elle garnit → chaque appel d'accesseur se fait en deux temps : « ma propre contribution » + « je délègue au dessert emballé ».
L'empilement new Chocolat(new Chantilly(new Gaufre())) forme une poupée russe :
Chocolat.getNom() demande son nom à la Chantilly, qui le demande à la Gaufre, qui
répond « Gaufre ». Chaque niveau rajoute son mot et son prix en remontant la chaîne.
package patisserie;
public abstract class Dessert {
private final String nom;
private final double prix;
protected Dessert(String nom, double prix) {
this.nom = nom;
this.prix = prix;
}
public String getNom() {
return nom;
}
public double getPrix() {
return prix;
}
@Override
public String toString() {
return getNom() + " – " + getPrix() + " €";
}
}Deux détails qui comptent : le constructeur est protected (on ne crée jamais un « dessert
» abstrait, seulement des crêpes et des gaufres), et toString() s'appuie sur les
accesseurs, jamais sur les attributs directement — c'est ce qui permettra aux garnitures de
redéfinir les accesseurs sans toucher à toString.
package patisserie.garniture;
import patisserie.Dessert;
public abstract class Garniture extends Dessert {
private final Dessert garnit;
protected Garniture(Dessert garnit, String nom, double prix) {
super(nom, prix);
this.garnit = garnit;
}
@Override
public double getPrix() {
return super.getPrix() + garnit.getPrix();
}
@Override
public String getNom() {
return garnit.getNom() + " " + super.getNom();
}
}package patisserie;
public class Crepe extends Dessert {
public Crepe() { super("Crêpe", 3.0); }
}
public class Gaufre extends Dessert {
public Gaufre() { super("Gaufre", 3.5); }
}package patisserie.garniture;
import patisserie.Dessert;
public class Chantilly extends Garniture {
public Chantilly(Dessert garnit) { super(garnit, "chantilly", 1.0); }
}
public class Chocolat extends Garniture {
public Chocolat(Dessert garnit) { super(garnit, "chocolat", 1.5); }
}Remarques :
- pas besoin de redéfinir
toStringdansGarniture: il appellegetNom()etgetPrix(), et la recherche de méthode part de la classe réelle → ce sont les versions redéfinies qui s'exécutent ; getNom()assemble « dessert garni + ma garniture » :Gaufre chantilly chocolat, dans l'ordre d'emballage ;- les concrets sont triviaux : leur constructeur renseigne nom et prix à la couche supérieure, rien d'autre.
Vérification : new Chocolat(new Chantilly(new Gaufre())) → prix 1,50 + 1 + 3,50 = 6 €,
nom « Gaufre chantilly chocolat ».
Une fois le patron Décorateur en place, le reste du TP se travaille en deux temps : d'abord un temps de réflexion (avec de l'UML), puis un temps d'implémentation.
2. Composite : et un café gourmand !
On veut des assortiments de desserts pour les commandes de groupe : un assortiment est un dessert composé de plusieurs desserts. Son prix est la somme des prix avec un rabais de 10 % ; son nom est celui de l'assortiment suivi, entre parenthèses, de tout ce qu'il contient :
Assortiment
- Crêpe
- Gaufre chantilly chocolat
8.1 €À vous de dire, dans le patron Composite : à quoi correspond l'assortiment ? Le dessert ? La crêpe ? Bonne nouvelle annoncée par le sujet : des ajouts, a priori aucune modification des classes existantes. Les assortiments doivent pouvoir contenir eux-mêmes des assortiments — dont les desserts peuvent être garnis.
Dans le patron Composite, tout le monde hérite du même type et répond aux mêmes messages :
- Component =
Dessert; - Leafs (feuilles) =
Crepe,Gaufre(et une garniture toute seule, qu'on interdit ici) ; - Composite =
Assortiment: il hérite deDessertet possède une collection deDessert.
Le message clé : quand on demande le prix d'un assortiment, il additionne le prix de chacun de ses enfants — qui, eux, savent se calculer (une crêpe se connaît, une garniture délègue à son dessert emballé, un assortiment imbriqué refait l'addition). Personne n'a besoin de savoir ce qu'il y a sous lui : c'est ça, « traiter feuilles et nœuds de la même façon ».
package patisserie;
import java.util.ArrayList;
import java.util.List;
public class Assortiment extends Dessert {
private final List<Dessert> desserts = new ArrayList<>();
public Assortiment(Dessert... desserts) {
super("Assortiment", 0);
for (Dessert d : desserts) {
this.desserts.add(d);
}
}
@Override
public double getPrix() {
double somme = 0;
for (Dessert d : desserts) {
somme += d.getPrix();
}
return somme * 0.9; // rabais de 10 %
}
@Override
public String getNom() {
StringBuilder sb = new StringBuilder("Assortiment");
for (Dessert d : desserts) {
sb.append("\n- ").append(d.getNom());
}
return sb.toString();
}
}Vérification : new Assortiment(new Crepe(), new Chocolat(new Chantilly(new Gaufre()))) →
somme 3 + 6 = 9, rabais appliqué → 8,1 € ✓. Et un assortiment peut en contenir un autre
sans écrire une ligne de plus : son enfant répond à getNom()/getPrix() comme n'importe quel
dessert.
3. Fabrique simple : passez commande
Instancier les desserts à la main est fastidieux. On externalise la construction des objets d'un
même super-type dans une classe dédiée : la Fabrique simple. Objectif : créer un dessert à
partir d'un texte, par exemple Gaufre chantilly chocolat (sans les assortiments pour l'instant).
Pour décomposer la commande, un simple split() et l'analyse du tableau résultant suffisent.
La chaîne Gaufre chantilly chocolat doit produire un objet équivalent à :
new Chocolat(new Chantilly(new Gaufre()));L'interface FabriqueDessert porte une seule méthode :
Dessert creerDessert(String desc). L'analyse :
desc.split(" ")découpe la commande en mots ;- le premier mot désigne le dessert de base (
crêpeougaufre) — c'est le « fond » de la poupée russe ; - chaque mot suivant enrobe : il désigne une garniture qui prend le dessert courant en paramètre et devient le nouveau courant.
On construit donc du bas vers le haut, exactement comme l'imbrication des new. Une
méthode par famille de mots (creerBase, creerGarniture) garde le code lisible.
package patisserie;
public interface FabriqueDessert {
Dessert creerDessert(String desc);
}package patisserie;
import patisserie.garniture.Chantilly;
import patisserie.garniture.Chocolat;
public class FabriqueDessertNom implements FabriqueDessert {
@Override
public Dessert creerDessert(String desc) {
String[] mots = desc.toLowerCase().split(" ");
Dessert courant = creerBase(mots[0]);
for (int i = 1; i < mots.length; i++) {
courant = creerGarniture(mots[i], courant);
}
return courant;
}
private Dessert creerBase(String mot) {
return switch (mot) {
case "crepe", "crêpe" -> new Crepe();
case "gaufre" -> new Gaufre();
default -> null; // à remplacer en partie 4
};
}
private Dessert creerGarniture(String mot, Dessert dessert) {
return switch (mot) {
case "chantilly" -> new Chantilly(dessert);
case "chocolat" -> new Chocolat(dessert);
case "coulis" -> new Coulis(dessert); // si vous l'ajoutez à la carte
default -> dessert; // mot inconnu : ignoré pour l'instant
};
}
}Vérification : creerDessert("Gaufre chantilly chocolat") construit bien
new Chocolat(new Chantilly(new Gaufre())). Écrivez le test qui le prouve
(assertEquals sur le nom et le prix).
4. Objet null : évitons de nous gaufrer
Une erreur de saisie (absence d'un dessert de base, garniture inconnue) peut faire planter le programme. En cas d'erreur, la fabrique retourne un objet « dessert null » — et non la valeur nulle. Mettez à jour l'UML, implémentez, vérifiez.
Le patron Null object retourne une vraie instance qui répond « poliment » à tous les
messages : nom « dessert inconnu », prix 0 €. Le code appelant n'a plus aucun if (d != null)
à écrire — afficher un dessert inconnu ne plante plus, ça affiche juste quelque chose de
sensé.
Côté fabrique : les deux default qui renvoyaient null en partie 3 renvoient maintenant
new DessertNull(). Attention au cas particulier : si le premier mot est inconnu, il n'y
a pas de dessert à garnir → inutile d'empiler des garnitures sur un DessertNull, on
retourne directement le DessertNull.
package patisserie;
public class DessertNull extends Dessert {
public DessertNull() {
super("dessert inconnu", 0);
}
}private Dessert creerBase(String mot) {
return switch (mot) {
case "crepe", "crêpe" -> new Crepe();
case "gaufre" -> new Gaufre();
default -> new DessertNull();
};
}
private Dessert creerGarniture(String mot, Dessert dessert) {
if (dessert instanceof DessertNull) {
return dessert; // pas de garniture sur un dessert qui n'existe pas
}
return switch (mot) {
case "chantilly" -> new Chantilly(dessert);
case "chocolat" -> new Chocolat(dessert);
default -> new DessertNull();
};
}Testez : creerDessert("Tartiflette chantilly") affiche « dessert inconnu – 0.0 € » sans
lever d'exception.
5. Stratégie : passez à la caisse !
Différents niveaux de détail sur le ticket, selon qu'il s'agit d'un cadeau ou non et selon la largeur du rouleau :
- l'interface
TicketDessertavec une seule méthode :String getTicket(Dessert dessert); - trois implémentations :
- le nom et le prix du dessert ;
- uniquement le nom (pour les cadeaux) ;
- les informations condensées sur un nombre de caractères limité, passé en paramètre à la construction ;
- une classe
Bornequi lit les commandes et affiche le ticket correspondant, avec un scénario : l'utilisateur saisit une commande, choisit le mode d'édition du ticket, le ticket s'affiche.
La Stratégie, c'est « un comportement variable derrière une interface fixe » :
- ce qui change : la façon de fabriquer le ticket → une implémentation par variante ;
- ce qui ne change pas : la
Borne— elle appellegetTicket(dessert)sans jamais se demander quelle variante est derrière.
Changer de format au runtime = remplacer l'objet stratégie (un simple setter). Aucun switch
sur le format dans la Borne : si demain on ajoute un ticket « colonne alignée », la Borne ne
bouge pas d'une ligne.
package patisserie;
public interface TicketDessert {
String getTicket(Dessert dessert);
}public class TicketComplet implements TicketDessert {
@Override
public String getTicket(Dessert dessert) {
return dessert.toString();
}
}
public class TicketCadeau implements TicketDessert {
@Override
public String getTicket(Dessert dessert) {
return dessert.getNom(); // pas de prix sur un cadeau
}
}
public class TicketCondense implements TicketDessert {
private final int largeur;
public TicketCondense(int largeur) {
this.largeur = largeur;
}
@Override
public String getTicket(Dessert dessert) {
String ligne = dessert.getNom() + " – " + dessert.getPrix() + " €";
return ligne.length() <= largeur ? ligne : ligne.substring(0, largeur);
}
}package patisserie;
import java.util.Scanner;
public class Borne {
private final FabriqueDessert fabrique = new FabriqueDessertNom();
private TicketDessert strategie = new TicketComplet();
public void setStrategie(TicketDessert strategie) {
this.strategie = strategie;
}
public void commander(String desc) {
System.out.println(strategie.getTicket(fabrique.creerDessert(desc)));
}
public static void main(String[] args) {
Borne borne = new Borne();
Scanner in = new Scanner(System.in);
System.out.print("Votre commande : ");
String commande = in.nextLine();
System.out.print("Ticket 1=complet 2=cadeau 3=condensé : ");
String mode = in.nextLine();
switch (mode) {
case "2" -> borne.setStrategie(new TicketCadeau());
case "3" -> borne.setStrategie(new TicketCondense(20));
default -> borne.setStrategie(new TicketComplet());
}
borne.commander(commande);
}
}Scénario : commande Gaufre chantilly chocolat, mode 1 → « Gaufre chantilly chocolat – 6.0 € »
; mode 3 → les 20 premiers caractères du ticket.
6. La cerise sur le gâteau
- le rabais d'un assortiment à plusieurs niveaux ne doit être effectué qu'une seule fois ;
- s'il est possible d'ajouter une garniture sur un assortiment, revoyez la conception pour l'empêcher ;
- la fabrique doit aussi gérer les assortiments (une commande contenant plusieurs desserts est un assortiment) ;
- adaptez modèle et fabrique pour gérer aussi les bons de réduction ;
- et n'oubliez pas les tests !
Le problème : Assortiment.getPrix() multiplie par 0,9 la somme des enfants. Si un enfant est
lui-même un assortiment, son prix a déjà été raboté — le rabais se cumule à chaque niveau.
La parade : distinguer « le prix affiché » et « le prix avant rabais ». Un dessert ordinaire ne connaît pas de rabais ; seul l'assortiment en applique un, et il calcule sa somme sur les prix avant rabais de ses enfants :
// dans Dessert :
public double getPrixAvantRabais() {
return getPrix();
}
// dans Assortiment :
@Override
public double getPrixAvantRabais() {
double somme = 0;
for (Dessert d : desserts) {
somme += d.getPrixAvantRabais(); // brut, jamais raboté
}
return somme;
}
@Override
public double getPrix() {
return getPrixAvantRabais() * 0.9; // un seul rabais, au niveau de ce assortiment
}Un assortiment dans un assortiment : le parent somme les prix bruts des enfants (l'enfant
expose son brut via getPrixAvantRabais()) et applique son propre rabais une fois. Chaque
niveau ne rabote que ce qu'il contient directement.
Avec le code actuel, new Chocolat(new Assortiment(…)) compile : la garniture attend un
Dessert, et l'assortiment est un dessert. C'est le prix de l'uniformité du Composite.
Deux sorties honnêtes :
-
La validation au constructeur (pragmatique, c'est celle du corrigé) :
protected Garniture(Dessert garnit, String nom, double prix) { super(nom, prix); if (garnit instanceof Assortiment) { throw new IllegalArgumentException("On ne garnit pas un assortiment."); } this.garnit = garnit; }Le cours met en garde contre les
instanceofen série — ici c'est une garde isolée, dans une règle métier explicite (« une garniture se met sur un dessert, pas sur une commande »), pas un dispatch de comportement. -
La hiérarchie parallèle (plus propre, plus lourde) : n'exposer que des « garnissables » à la garniture — au prix de dupliquer le type commun et de perdre la simplicité du Composite. À ce stade du BUT, la garde au constructeur est un choix défendable, documenté par un test qui vérifie l'exception.
Règle du sujet : une commande contenant plusieurs desserts de base est un assortiment. L'analyse change de forme : on parcourt les mots, un mot « base » ouvre un dessert, un mot garniture garnit le dessert courant, et un nouveau mot base clôt le courant et en commence un autre :
@Override
public Dessert creerDessert(String desc) {
String[] mots = desc.toLowerCase().split(" ");
List<Dessert> trouves = new ArrayList<>();
Dessert courant = null;
for (String mot : mots) {
if (estBase(mot)) {
if (courant != null) trouves.add(courant);
courant = creerBase(mot);
} else if (courant != null) {
courant = creerGarniture(mot, courant);
}
}
if (courant != null) trouves.add(courant);
if (trouves.isEmpty() || trouves.get(0) instanceof DessertNull) {
return new DessertNull();
}
return trouves.size() == 1 ? trouves.get(0) : new Assortiment(trouves.toArray(new Dessert[0]));
}creerGarniture garde sa garde instanceof DessertNull : une garniture sur un dessert
inconnu ne crée pas de fantôme. Test : creerDessert("Crepe Gaufre chantilly") → un
assortiment de 8,1 € ; creerDessert("Crepe") → une simple crêpe.
Pour les bons de réduction, le plus simple reste le patron Décorateur déjà en main : une garniture un peu spéciale qui retranche au lieu d'ajouter —
public class BonReduction extends Dessert {
private final Dessert dessert;
private final double euros;
public BonReduction(Dessert dessert, double euros) {
super("bon de réduction", -euros);
this.dessert = dessert;
this.euros = euros;
}
@Override
public double getPrix() {
return Math.max(0, dessert.getPrix() - euros);
}
@Override
public String getNom() {
return dessert.getNom();
}
}Variante demandée par le sujet : un bon en pourcentage se modélise alors très bien en Stratégie (le calcul de remise est un algorithme interchangeable) — à vous de choisir selon ce qu'on vous demande.
tp1-patrons-de-conception.pdf
R3.04 · TP 1 — Fabien Delecroix · 7 pages
Chargement du document…
Dernière mise à jour le
Ch. 5 — Principes : modularité
Composer un logiciel en briques : paquetages, les cinq principes de modularité, les cinq règles de Bertrand Meyer et les quatre niveaux de visibilité en Java.
TP 2 — Observable, observateurs et stratégie
Une roulette de casino : faire réagir des joueurs aux tirages avec Observable-Observateurs, puis varier les paris avec Stratégie — corrigé pas à pas depuis le fork du dépôt.