Design patterns¶
1. Objectif général¶
Dans ce TP, vous travaillez sur une petite application Java de dessin. L'application contient :
- une zone de visualisation graphique, où les formes sont dessinées ;
- une zone console, où des messages expliquent ce qui se passe ;
- une liste de scénarios de test ;
- un bouton Étape suivante pour avancer dans le scénario sélectionné.
Le principe est toujours le même :
- choisissez un scénario ;
- cliquez sur le bouton du scénario ;
- cliquez sur Étape suivante ;
- observez ce qui se passe dans la zone graphique et dans la console ;
- lisez les consignes en rouge ;
- modifiez uniquement les fichiers demandés ;
- recompilez, relancez, puis testez à nouveau.
Les consignes importantes apparaissent en rouge dans la console de l'application.
2. Compilation et lancement¶
Téléchargez l'archive, décompressez la puis placez-vous dans le dossier contenant les fichiers .java.
Pour compiler tous les fichiers :
Pour lancer l'application :
Attention : Java distingue les majuscules et les minuscules. Il faut donc écrire exactement ApplicationDessin.
3. Règles du TP¶
Vous devez modifier uniquement les fichiers indiqués dans chaque scénario. Vous ne devez pas modifier :
ApplicationDessin.java;PanneauDessin.java;- les classes
Scenario...java; ScenarioCatalogue.java;ScenarioContext.java.
Ces fichiers servent à tester votre travail. Si vous les modifiez, vous risquez de masquer un problème au lieu de le résoudre.
Le but n'est pas de recopier du code, mais de comprendre pourquoi un design pattern devient utile.
Scénario 1 — Rectangle¶
Éléments de cours : Méthodes, attributs et calculs
En programmation orientée objet, une méthode représente un comportement. Les méthodes de calcul (comme surface()) doivent retourner un type précis (souvent double ou int pour des mesures géométriques).
Pour accéder aux attributs de l'instance courante depuis l'intérieur de la classe (comme largeur ou hauteur), on utilise le mot-clé this (par exemple : this.largeur).
- Formule de la surface : \(largeur \times hauteur\).
- Formule du périmètre : \(2 \times (largeur + hauteur)\).
Problème¶
L'application crée un rectangle, mais ses informations ne sont pas encore correctes. La console doit afficher une surface et un périmètre cohérents.
Ce que vous devez observer¶
Lancez Scenario 1 - Rectangle, puis cliquez sur Étape suivante. Le rectangle apparaît, mais les valeurs affichées peuvent être incorrectes.
Plan simplifié¶
classDiagram
class Forme {
+surface()
+perimetre()
+dessiner(g)
}
class Rectangle {
-largeur
-hauteur
+surface()
+perimetre()
+dessiner(g)
}
Forme <|-- Rectangle
Fichier à modifier¶
Rectangle.java
Travail demandé¶
Complétez les méthodes qui calculent les mesures du rectangle et améliorez son affichage graphique. Ne modifiez pas le scénario de test.
Validation¶
Après recompilation, le scénario doit afficher un rectangle visible et des valeurs non nulles dans la console.
Scénario 2 — Iterator¶
Éléments de cours : Le Design Pattern Iterator
Modifier une collection (avec remove()) pendant qu'on la parcourt avec une boucle for-each classique provoque très souvent une ConcurrentModificationException en Java.
Le Design Pattern Iterator (Itérateur) fournit un moyen d'accéder séquentiellement aux éléments d'un objet agrégé (une collection) sans exposer sa représentation sous-jacente. Il permet surtout de modifier la collection en toute sécurité pendant son parcours.
Pour supprimer proprement un élément :
- Obtenir l'itérateur :
Iterator<Type> it = liste.iterator(); - Boucler tant qu'il y a un suivant :
while (it.hasNext()). - Récupérer l'élément courant :
Type t = it.next(); - Supprimer si la condition est remplie :
it.remove();
Problème¶
On possède une liste de rectangles. On veut supprimer les rectangles trop grands pendant le parcours de la liste. Supprimer des éléments directement dans une boucle classique peut provoquer des erreurs ou des comportements instables.
Besoin du pattern¶
Le pattern ou mécanisme Iterator permet de parcourir une collection tout en gardant un contrôle propre sur la suppression d'éléments.
Ce que vous devez observer¶
Lancez Scenario 2 - Iterator. Au départ, plusieurs rectangles sont visibles. À l'étape suivante, les grands rectangles devraient disparaître.
Plan simplifié¶
classDiagram
class SuppressionFormes {
+supprimerRectanglesTropGrands(formes, seuil)
}
class Iterator {
+hasNext()
+next()
+remove()
}
SuppressionFormes ..> Iterator
Fichier à modifier¶
SuppressionFormes.java
Travail demandé¶
Complétez la méthode de suppression pour parcourir la liste et retirer uniquement les rectangles dont la surface dépasse le seuil demandé.
Validation¶
Après correction, seuls les petits rectangles restent visibles.
Scénario 3 — Factory¶
Éléments de cours : Le Design Pattern Factory (Fabrique)
L'instanciation directe avec le mot-clé new crée un couplage fort entre le code appelant et la classe concrète.
Le Design Pattern Factory délègue la création des objets à une méthode dédiée (la fabrique). Cette méthode encapsule la logique conditionnelle (souvent des if / else ou un switch) et décide quelle sous-classe instancier en fonction des paramètres fournis.
Exemple : if (largeur == hauteur) return new Carre(...); else return new Rectangle(...);
Cela permet de cacher la complexité de création, de respecter le principe Ouvert/Fermé (il est plus facile d'ajouter de nouvelles formes plus tard sans modifier tout le programme) et d'isoler la logique d'instanciation hors du code métier principal.
Problème¶
Le programme reçoit des dimensions et doit créer la bonne forme automatiquement. Quand la largeur et la hauteur sont identiques, on veut obtenir un carré. Sinon, on veut obtenir un rectangle.
Besoin du pattern¶
Le pattern Factory centralise la création d'objets. Le reste du programme demande une forme, sans connaître tous les détails de construction.
Plan simplifié¶
classDiagram
class FormeFactory {
+creerForme(x, y, largeur, hauteur)
}
class Forme
class Rectangle
class Carre
Forme <|-- Rectangle
Rectangle <|-- Carre
FormeFactory ..> Forme
Fichier à modifier¶
FormeFactory.java
Travail demandé¶
Complétez la Factory pour qu'elle choisisse le type d'objet à créer selon les dimensions reçues.
Validation¶
Le scénario doit afficher une forme rectangulaire puis une forme carrée, avec des noms de classes cohérents dans la console.
Scénarios 4, 5 et 6 — Strategy¶
Éléments de cours : Le Design Pattern Strategy (Stratégie)
Définir tous les comportements possibles directement dans la classe Forme (avec de multiples instructions conditionnelles) rendrait la classe très lourde et difficile à maintenir.
Le Design Pattern Strategy permet de définir une famille d'algorithmes (ici, les différents types de déplacements), de les encapsuler chacun dans une classe distincte qui implémente une interface commune (ex: StrategieDeplacement), et de les rendre interchangeables au moment de l'exécution.
Grâce au polymorphisme, la classe Forme possède une référence vers cette interface. Vous appellerez simplement cetteStrategie.deplacer(this) : la forme n'a pas besoin de savoir si le mouvement est lent, rapide ou en zigzag. Le comportement peut même changer dynamiquement en cours d'exécution via un setStrategie().
Problème¶
Une forme peut se déplacer de plusieurs manières : rapidement, lentement ou en zigzag.
On ne veut pas coder tous les déplacements directement dans Rectangle.
Besoin du pattern¶
Le pattern Strategy permet de changer le comportement d'un objet sans modifier sa classe principale. Ici, la forme garde une référence vers une stratégie de déplacement.
Plan simplifié¶
classDiagram
class Forme {
-strategie
+setStrategie(s)
+seDeplacer()
}
class StrategieDeplacement {
<<interface>>
+deplacer(forme)
}
class DeplacementRapide
class DeplacementLent
class DeplacementZigzag
Forme --> StrategieDeplacement
StrategieDeplacement <|.. DeplacementRapide
StrategieDeplacement <|.. DeplacementLent
StrategieDeplacement <|.. DeplacementZigzag
Scénario 4 — Déplacement rapide¶
Fichier à modifier¶
DeplacementRapide.java
Travail demandé¶
Complétez la stratégie pour que la forme avance nettement vers la droite à chaque déplacement.
Validation¶
Le rectangle doit bouger fortement vers la droite après l'étape de test.
Scénario 5 — Déplacement lent¶
Fichier à modifier¶
DeplacementLent.java
Travail demandé¶
Complétez la stratégie pour que la forme avance doucement à chaque clic.
Validation¶
Le rectangle doit se déplacer progressivement, avec un petit changement de position à chaque étape.
Scénario 6 — Déplacement zigzag¶
Fichier à modifier¶
DeplacementZigzag.java
Travail demandé¶
Complétez la stratégie pour obtenir un mouvement horizontal avec une alternance verticale. La stratégie peut mémoriser son état interne pour savoir si le prochain déplacement doit monter ou descendre.
Validation¶
Le rectangle doit avancer et alterner sa position verticale.
Scénario 7 — Singleton avec initialisation immédiate¶
Éléments de cours : Le Design Pattern Singleton (Initialisation immédiate)
Il est parfois indispensable de garantir qu'une classe n'ait qu'une seule et unique instance à travers toute l'application (ex: gestionnaire de configuration, connexion à une base de données) et de fournir un point d'accès global à cette instance. C'est le rôle du Singleton.
Il repose techniquement sur deux piliers :
- Un constructeur privé :
private MaClasse() {}(empêche l'instanciation par d'autres classes avecnew). - Un attribut statique privé qui stocke l'unique instance, accessible via une méthode publique
getInstance().
Dans l'initialisation immédiate (eager initialization), l'instance est créée dès le chargement de la classe en mémoire : private static MaClasse instance = new MaClasse();. C'est simple et sûr vis-à-vis des threads, mais cela consomme de la mémoire même si l'objet n'est finalement jamais utilisé au cours du programme.
Problème¶
L'application a besoin d'une configuration unique. Si chaque appel crée une nouvelle configuration, le programme peut devenir incohérent.
Besoin du pattern¶
Le pattern Singleton garantit qu'une seule instance d'une classe existe dans l'application. Dans cette première version, l'instance est créée dès le chargement de la classe.
Plan simplifié¶
classDiagram
class ConfigurationSingletonImmediat {
-instance
-ConfigurationSingletonImmediat()
+getInstance()
+getNomApplication()
}
Fichier à modifier¶
ConfigurationSingletonImmediat.java
Travail demandé¶
Transformez la classe pour qu'elle possède une instance unique créée dès la déclaration de l'attribut statique.
La méthode getInstance() doit renvoyer cette même instance.
Validation¶
La console doit indiquer que deux appels à getInstance() renvoient la même référence.
Scénario 8 — Singleton avec initialisation paresseuse¶
Éléments de cours : Le Design Pattern Singleton (Initialisation paresseuse)
Contrairement à l'initialisation immédiate, l'initialisation paresseuse (lazy initialization) retarde la création de l'instance jusqu'au moment où elle est réellement demandée pour la toute première fois.
L'attribut statique est déclaré mais initialisé à null. C'est à l'intérieur de la méthode d'accès que l'on vérifie l'existence de l'objet :
public static MaClasse getInstance() {
if (instance == null) {
instance = new MaClasse();
}
return instance;
}
Cette approche économise des ressources mémoire, mais nécessite des précautions supplémentaires (comme l'utilisation du mot-clé synchronized) si l'application devient multi-threadée, pour éviter que deux instances soient créées simultanément par accident.
Problème¶
On veut toujours une seule configuration, mais on ne veut la créer qu'au moment où elle est réellement utilisée.
Besoin du pattern¶
Le Singleton paresseux crée l'objet lors du premier appel à getInstance().
Les appels suivants réutilisent la même instance.
Plan simplifié¶
classDiagram
class ConfigurationSingletonParesseux {
-instance
-ConfigurationSingletonParesseux()
+getInstance()
+getNomApplication()
}
Fichier à modifier¶
ConfigurationSingletonParesseux.java
Travail demandé¶
L'attribut d'instance doit commencer vide.
Dans getInstance(), créez l'objet seulement lors du premier appel, puis renvoyez toujours la même référence.
Validation¶
La console doit montrer que le premier appel initialise l'objet et que les appels suivants gardent la même instance.
Scénario 9 — Observer¶
Éléments de cours : Le Design Pattern Observer (Observateur)
Dans un logiciel, de nombreux éléments doivent souvent réagir quand une donnée change (comme une interface graphique qui se met à jour). Lier l'objet modifié directement à tous ceux qui doivent réagir crée un couplage fort et insoutenable.
Le Design Pattern Observer définit une relation de dépendance de type un-à-plusieurs. Quand l'objet Observable (le sujet) change d'état, il notifie automatiquement tous ses Observateurs (les abonnés).
Techniquement, l'Observable maintient une liste d'abonnés de type interface (List<ObservateurForme>). Lorsqu'un événement survient (ex: un déplacement), l'Observable parcourt cette liste avec une boucle et appelle une méthode de notification prédéfinie sur chaque élément. Ainsi, l'Observable ne connaît pas la classe concrète de ses observateurs, garantissant un couplage très faible.
Problème¶
Quand une forme se déplace, plusieurs parties du logiciel peuvent vouloir réagir :
- la zone graphique doit être rafraîchie ;
- la console peut afficher un message ;
- plus tard, un historique ou une sauvegarde automatique pourrait réagir.
On ne veut pas que la forme connaisse directement toute l'application.
Besoin du pattern¶
Le pattern Observer permet à un objet observable d'envoyer une notification à plusieurs observateurs. La forme annonce qu'elle a changé, mais elle ne sait pas précisément qui va réagir.
Plan simplifié¶
classDiagram
class ObservateurForme {
<<interface>>
+notifier(message)
}
class FormeObservable {
-observateurs
+ajouterObservateur(obs)
+notifierObservateurs(message)
}
class RectangleObservable {
-observable
+ajouterObservateur(obs)
+seDeplacer()
}
class ApplicationDessin {
+notifier(message)
}
ObservateurForme <|.. ApplicationDessin
RectangleObservable --> FormeObservable
FormeObservable --> ObservateurForme
Fichiers à modifier¶
ObservateurForme.javasi nécessaire ;FormeObservable.java;RectangleObservable.java.
Travail demandé¶
Complétez le mécanisme d'abonnement et de notification. Après un déplacement, le rectangle observable doit prévenir ses observateurs.
Validation¶
Dans le scénario, une ligne commençant par [Notification] doit apparaître dans la console après le déplacement du rectangle.
Scénario 10 — Adapter¶
Éléments de cours : Le Design Pattern Adapter (Adaptateur)
Il arrive souvent qu'une classe existante propose les fonctionnalités dont vous avez besoin, mais que son interface (ses signatures de méthodes) ne corresponde pas du tout à ce que votre système attend.
Le Design Pattern Adapter agit comme un pont ou un traducteur. Il convertit l'interface d'une classe en une autre interface exigée par le client.
L'approche la plus courante en Java est l'adaptateur par composition (et délégation) : la classe Adapter implémente la nouvelle interface exigée (ou hérite de la classe de base) et possède en tant qu'attribut privé une instance de l'ancienne classe (AncienTriangle).
Quand le système appelle adapter.dessiner(g), l'Adapter traduit et relaie silencieusement l'appel en interne : monAncienTriangle.afficherAncienneVersion(g). La classe d'origine n'a absolument pas besoin d'être modifiée.
Problème¶
Le logiciel manipule des objets de type Forme.
On récupère une ancienne classe AncienTriangle, mais elle ne respecte pas l'interface attendue par le logiciel.
On veut utiliser ce triangle sans modifier l'ancienne classe.
Besoin du pattern¶
Le pattern Adapter sert de traducteur entre une ancienne classe et une nouvelle architecture.
L'adapter hérite de Forme et contient un AncienTriangle.
Plan simplifié¶
classDiagram
class Forme {
+surface()
+perimetre()
+dessiner(g)
}
class AncienTriangle {
+afficherAncienneVersion(g)
}
class TriangleAdapter {
-triangle
+surface()
+perimetre()
+dessiner(g)
}
Forme <|-- TriangleAdapter
TriangleAdapter --> AncienTriangle
Fichier à modifier¶
TriangleAdapter.java
Travail demandé¶
Complétez l'adapter pour qu'il puisse être utilisé comme une Forme.
Il doit fournir des mesures cohérentes et déléguer le dessin à l'ancien triangle.
Le dessin doit aussi respecter la position x, y portée par l'adapter, car cette position sera utilisée ensuite par le scénario Composite.
Validation¶
Le triangle magenta doit apparaître dans la zone graphique alors qu'il est ajouté dans une List<Forme>.
Scénario 11 — Composite¶
Éléments de cours : Le Design Pattern Composite
Comment traiter de manière totalement uniforme un objet simple (un cercle) et un groupe d'objets (un dessin complexe composé de plusieurs cercles et carrés) ?
Le Design Pattern Composite permet d'organiser les objets en structures arborescentes. Il permet aux clients de manipuler les objets individuels et les compositions d'objets de manière stricement identique.
L'objet Composite (ici GroupeFormes) hérite de la même classe de base Forme que les objets simples. Il contient en lui-même une collection de Forme (qui peuvent être de simples formes ou d'autres sous-groupes).
Ses opérations s'appuient fortement sur la récursivité : pour calculer la surface totale, le Composite initialise un compteur à \(0\), parcourt sa liste d'enfants, et appelle la méthode surface() sur chacun d'eux. Si un enfant est lui-même un groupe, l'appel se propagera en cascade automatiquement.
Problème¶
On veut construire un pictogramme composé de plusieurs formes : un rectangle, un cercle et un triangle adapté. Le programme doit pouvoir manipuler ce pictogramme comme une seule forme.
Sans Composite, le scénario devrait connaître tous les éléments internes du pictogramme et les manipuler un par un.
Besoin du pattern¶
Le pattern Composite permet de traiter un groupe d'objets comme un objet unique.
Ici, GroupeFormes hérite de Forme et contient plusieurs objets Forme.
Plan simplifié¶
classDiagram
class Forme {
+surface()
+perimetre()
+dessiner(g)
+seDeplacer()
}
class Rectangle
class Cercle
class TriangleAdapter
class GroupeFormes {
-enfants
+ajouter(forme)
+retirer(forme)
+nombreEnfants()
+surface()
+perimetre()
+dessiner(g)
+seDeplacer()
}
Forme <|-- Rectangle
Forme <|-- Cercle
Forme <|-- TriangleAdapter
Forme <|-- GroupeFormes
GroupeFormes --> Forme
Fichier à modifier¶
GroupeFormes.java
Travail demandé¶
Complétez le groupe pour qu'il :
- mémorise les formes ajoutées ;
- sache combien d'enfants il contient ;
- calcule des mesures globales à partir des enfants ;
- dessine tous ses enfants ;
- propage le déplacement à tous ses enfants.
Validation¶
À l'étape 2, le rectangle, le cercle et le triangle doivent apparaître alors qu'un seul objet GroupeFormes est ajouté à la liste des formes.
À l'étape 3, un seul appel à groupe.seDeplacer() doit déplacer tous les éléments du pictogramme.
Le déplacement peut être différent pour chaque enfant selon sa stratégie, mais tous les éléments doivent réagir.
Bilan attendu¶
À la fin du TP, vous devez pouvoir expliquer :
- pourquoi Iterator aide à modifier une liste pendant un parcours ;
- pourquoi Factory évite de disperser le code de création ;
- pourquoi Strategy permet de changer un comportement sans modifier la classe principale ;
- pourquoi Singleton garantit une instance unique ;
- pourquoi Observer réduit les dépendances entre objets ;
- pourquoi Adapter permet d'intégrer une ancienne classe ;
- pourquoi Composite permet de manipuler un groupe comme un objet unique.
QCM de validation des acquis¶
Testez votre compréhension des Design Patterns vus dans ce TP. Sélectionnez la bonne réponse pour chaque question, puis validez pour obtenir votre score.