Aller au contenu principal

Design Patterns — Solutions éprouvées

Les design patterns (patrons de conception) sont des solutions réutilisables à des problèmes récurrents de conception logicielle. Ils ne sont pas du code prêt à copier, mais des recettes adaptables à votre contexte.

Notions théoriques

Pourquoi les connaître ?

Sans patterns, on réinvente la roue et on produit du code difficile à faire évoluer. Les patterns offrent un vocabulaire commun entre développeurs : dire "c'est un Singleton" ou "c'est un Observer" communique une architecture entière en deux mots.

Nous allons voir 4 patterns essentiels, tous illustrés avec le projet RPG.


Singleton — Une seule instance

Problème : certains objets ne doivent exister qu'en un seul exemplaire (gestionnaire de jeu, connexion à la base, configuration).

Solution : le constructeur est privé, l'accès se fait via une méthode statique qui crée l'instance au premier appel et la retourne ensuite.

public class GestionnaireJeu {

private static GestionnaireJeu instance;

// Constructeur privé : impossible de faire new GestionnaireJeu()
private GestionnaireJeu() {
System.out.println("Gestionnaire initialisé");
}

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

public void demarrerPartie() {
System.out.println("Partie démarrée !");
}
}

// Utilisation
GestionnaireJeu g1 = GestionnaireJeu.getInstance();
GestionnaireJeu g2 = GestionnaireJeu.getInstance();
System.out.println(g1 == g2); // true : même instance
attention

La version ci-dessus n'est pas thread-safe. En environnement multi-thread, ajoutez synchronized à la méthode, ou utilisez l'enum Singleton (version la plus robuste en Java) : public enum GestionnaireJeu { INSTANCE; }.


Factory — Fabriquer sans connaître la classe

Problème : le code appelant ne devrait pas avoir à connaître les classes concrètes (Guerrier, Mage...) pour créer des personnages.

Solution : une Factory centralise la création et retourne le type abstrait.

public enum TypePersonnage { GUERRIER, MAGE, ARCHER }

public class PersonnageFactory {

public static Personnage creer(TypePersonnage type, String nom) {
return switch (type) {
case GUERRIER -> new Guerrier(nom, 150, 30);
case MAGE -> new Mage(nom, 100, 20, 50);
case ARCHER -> new Archer(nom, 120, 25, 80);
};
}
}

// Utilisation : l'appelant ne cite jamais Guerrier ou Mage
Personnage p1 = PersonnageFactory.creer(TypePersonnage.GUERRIER, "Aragorn");
Personnage p2 = PersonnageFactory.creer(TypePersonnage.MAGE, "Gandalf");

Strategy — Algorithme interchangeable

Problème : un personnage peut avoir différentes stratégies d'attaque (normale, critique, magique) que l'on veut changer à l'exécution.

Solution : encapsuler l'algorithme dans une interface, l'injecter dans la classe.

@FunctionalInterface
public interface StrategieAttaque {
int calculerDegats(int force);
}

public class Personnage {
private StrategieAttaque strategie;

public Personnage(String nom, int force, StrategieAttaque strategie) {
this.nom = nom;
this.force = force;
this.strategie = strategie;
}

public int attaquer() {
return strategie.calculerDegats(this.force);
}

// Changer de stratégie à runtime
public void changerStrategie(StrategieAttaque nouvelleStrategie) {
this.strategie = nouvelleStrategie;
}
}

// Strategies
StrategieAttaque normale = force -> force;
StrategieAttaque critique = force -> force * 2;
StrategieAttaque magique = force -> force + 50;

Personnage guerrier = new Personnage("Aragorn", 150, normale);
System.out.println(guerrier.attaquer()); // 150

guerrier.changerStrategie(critique);
System.out.println(guerrier.attaquer()); // 300

Observer — Notifier des abonnés

Problème : plusieurs composants (logger, statistiques, interface) doivent être informés des événements de combat sans que Combat les connaisse directement.

Solution : Combat maintient une liste d'observateurs et les notifie à chaque événement.

public interface CombatListener {
void onAttaque(String attaquant, String cible, int degats);
}

public class Combat {
private List<CombatListener> listeners = new ArrayList<>();

public void ajouterListener(CombatListener l) {
listeners.add(l);
}

private void notifier(String attaquant, String cible, int degats) {
for (CombatListener l : listeners) {
l.onAttaque(attaquant, cible, degats);
}
}

public void attaquer(Personnage attaquant, Personnage cible) {
int degats = attaquant.attaquer();
// ... appliquer les dégâts ...
notifier(attaquant.getNom(), cible.getNom(), degats);
}
}

// Utilisation
Combat combat = new Combat();
combat.ajouterListener((att, cib, deg) ->
System.out.println("[LOG] " + att + " frappe " + cib + " pour " + deg + " dégâts"));
combat.ajouterListener((att, cib, deg) ->
stats.enregistrer(att, deg));

Exemple pratique

public class Main {

public static void main(String[] args) {
// Singleton
GestionnaireJeu jeu = GestionnaireJeu.getInstance();
jeu.demarrerPartie();

// Factory
Personnage aragorn = PersonnageFactory.creer(TypePersonnage.GUERRIER, "Aragorn");
Personnage gandalf = PersonnageFactory.creer(TypePersonnage.MAGE, "Gandalf");

// Strategy : commencer en attaque normale, passer en critique
StrategieAttaque normale = force -> force;
StrategieAttaque critique = force -> force * 2;

aragorn.changerStrategie(normale);
System.out.println("Attaque normale : " + aragorn.attaquer());

aragorn.changerStrategie(critique);
System.out.println("Attaque critique : " + aragorn.attaquer());

// Observer
Combat combat = new Combat();
combat.ajouterListener((att, cib, deg) ->
System.out.println("[LOG] " + att + " attaque " + cib + " (" + deg + " dégâts)"));

combat.attaquer(aragorn, gandalf);
}
}

Test de mémorisation/compréhension


Quel pattern garantit qu'une classe n'a qu'une seule instance ?


Pourquoi le constructeur est-il privé dans un Singleton ?


Quel pattern permet de changer l'algorithme d'une classe à l'exécution ?


Dans le pattern Factory, quel type retourne la méthode creer() ?


Dans le pattern Observer, qui maintient la liste des observateurs ?


Quel pattern correspond à l'interface CombatListener avec onAttaque() ?


TP pour réfléchir et résoudre des problèmes

Vous allez implémenter le pattern Factory et le pattern Strategy dans le projet RPG.

Étape 1 — Créer l'enum TypePersonnage et la Factory

Définissez l'enum TypePersonnage puis complétez la méthode creer() de PersonnageFactory.


Bonne pratique - La Factory centralise les valeurs par défaut

En regroupant la création dans la Factory, vous définissez les valeurs par défaut (force=150, défense=30) en un seul endroit. Modifier ces valeurs pour tous les guerriers du jeu ne demande qu'un changement dans la Factory.

Étape 2 — Implémenter l'interface StrategieAttaque

Créez l'interface fonctionnelle StrategieAttaque et deux stratégies : normale et critique.


Bonne pratique - Strategy + @FunctionalInterface = lambda directement

Quand une Strategy n'a qu'une méthode, l'annoter @FunctionalInterface permet aux appelants de passer une lambda au lieu d'instancier une classe anonyme. C'est l'usage moderne et idiomatique en Java 8+.

Étape 3 — Intégrer Strategy dans Personnage et tester le changement à runtime

Ajoutez l'attribut StrategieAttaque dans Personnage, une méthode attaquer() et une méthode changerStrategie().


Bonne pratique - Injecter la stratégie via le constructeur

Injecter la stratégie dans le constructeur (et non la coder en dur) rend le personnage testable : les tests unitaires peuvent passer n'importe quelle stratégie. C'est le principe d'injection de dépendance, que Spring Boot automatise avec @Autowired.

📌 Une solution