Skip to content

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 :

  1. choisissez un scénario ;
  2. cliquez sur le bouton du scénario ;
  3. cliquez sur Étape suivante ;
  4. observez ce qui se passe dans la zone graphique et dans la console ;
  5. lisez les consignes en rouge ;
  6. modifiez uniquement les fichiers demandés ;
  7. 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 :

javac *.java

Pour lancer l'application :

java ApplicationDessin

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 :

  1. Obtenir l'itérateur : Iterator<Type> it = liste.iterator();
  2. Boucler tant qu'il y a un suivant : while (it.hasNext()).
  3. Récupérer l'élément courant : Type t = it.next();
  4. 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 :

  1. Un constructeur privé : private MaClasse() {} (empêche l'instanciation par d'autres classes avec new).
  2. 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.java si 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.

1. Quel design pattern permet de modifier une collection en toute sécurité pendant son parcours ?

2. Quel pattern permet de centraliser la logique d'instanciation des objets (par exemple, choisir entre créer un Carré ou un Rectangle) ?

3. Pour rendre un comportement (comme le déplacement) facilement interchangeable dynamiquement, on utilise...

4. Quelle est la condition indispensable pour coder un Singleton en Java ?

5. Dans un Singleton paresseux (lazy), quand l'unique instance est-elle créée ?

6. Comment le pattern Observer garantit-il un couplage faible ?

7. Quel est le rôle principal de l'Adapter ?

8. Dans l'implémentation de l'Adapter par composition vue en TP, comment cela fonctionne-t-il techniquement ?

9. Quel est le but du pattern Composite ?

10. Dans le Composite `GroupeFormes`, comment calcule-t-on généralement la surface totale ?