Les design patterns les plus utiles : Singleton, Factory, Strategy, Observer, Builder, Command, Bridge
Un tour d'horizon pratique des patrons de conception qui reviennent le plus souvent, avec pour chacun le problème résolu, un exemple de code et les pièges.
Les design patterns (patrons de conception) sont des solutions éprouvées à des problèmes récurrents, popularisées par le livre du « Gang of Four » (1994). Ce ne sont pas des recettes à copier-coller mais un vocabulaire commun : dire « ici on utilise une Strategy » communique une intention en deux mots. Voici les plus utiles au quotidien, chacun avec le problème qu'il résout et un exemple.
1. Singleton — une seule instance
Problème : garantir qu'une classe n'a qu'une instance, accessible globalement (un pool de connexions, une configuration).
public enum Config {
INSTANCE;
private final String url = "jdbc:...";
public String url() { return url; }
}
// Usage : Config.INSTANCE.url();
En Java, l'enum est la façon la plus sûre (thread-safe, résistant à la sérialisation). Piège : le Singleton est souvent un état global déguisé qui rend les tests difficiles. Dans une appli Spring, préférez un bean singleton géré par le conteneur.
2. Factory Method — déléguer la création
Problème : créer un objet sans coder en dur sa classe concrète, pour pouvoir en changer facilement.
interface Notification { void envoyer(String msg); }
class NotificationFactory {
static Notification creer(String canal) {
return switch (canal) {
case "email" -> new EmailNotification();
case "sms" -> new SmsNotification();
default -> throw new IllegalArgumentException(canal);
};
}
}
La factory centralise la logique de création. Sa cousine, l'Abstract Factory, produit des familles d'objets cohérents (ex. un thème d'interface entier).
3. Strategy — interchanger un algorithme
Problème : choisir un comportement à l'exécution parmi plusieurs variantes (tri, calcul de frais, compression). C'est l'alternative propre au gros switch.
interface Compression { byte[] compresser(byte[] data); }
class Service {
void archiver(byte[] d, Compression strategie) {
byte[] out = strategie.compresser(d);
}
}
Strategy incarne le principe Open/Closed : ajouter un algorithme = ajouter une classe. C'est sans doute le pattern le plus utile au quotidien.
4. Observer — notifier des abonnés
Problème : quand un objet change d'état, prévenir automatiquement plusieurs autres, sans couplage fort. C'est le cœur des systèmes événementiels et de la programmation réactive (RxJS, les signals).
interface Observateur { void surEvenement(String e); }
class Sujet {
private final List<Observateur> obs = new ArrayList<>();
void abonner(Observateur o) { obs.add(o); }
void publier(String e) { obs.forEach(o -> o.surEvenement(e)); }
}
5. Builder — construire pas à pas
Problème : créer un objet complexe avec beaucoup de paramètres optionnels, sans constructeur télescopique illisible. Lombok génère ce pattern avec @Builder.
Article a = Article.builder()
.titre("SOLID")
.langue("fr")
.minutes(14)
.build();
6. Command — encapsuler une action
Problème : transformer une requête en objet, pour la mettre en file, la journaliser, ou l'annuler (undo/redo). Un éditeur de texte représente chaque action (« insérer », « supprimer ») comme une Command avec executer() et annuler().
interface Commande { void executer(); void annuler(); }
class Historique {
private final Deque<Commande> pile = new ArrayDeque<>();
void faire(Commande c) { c.executer(); pile.push(c); }
void defaire() { if (!pile.isEmpty()) pile.pop().annuler(); }
}
7. Bridge — séparer abstraction et implémentation
Problème : éviter l'explosion combinatoire de classes quand deux dimensions varient indépendamment. Exemple : des formes (cercle, carré) × des rendus (SVG, Canvas). Sans Bridge, on aurait CercleSvg, CercleCanvas, CarreSvg… Le Bridge relie les deux hiérarchies par composition : une Forme possède un Rendu, et chacun évolue de son côté.
interface Rendu { void dessinerCercle(double r); }
abstract class Forme {
protected final Rendu rendu; // le "pont"
Forme(Rendu r) { this.rendu = r; }
abstract void dessiner();
}
Quand (ne pas) utiliser un pattern
Le plus grand risque est le « patternite » : plaquer des patterns partout par réflexe, ce qui alourdit un code qui n'en avait pas besoin. Un pattern se justifie quand il répond à une variation ou une douleur réelle. Apprenez-les pour reconnaître les situations — pas pour les caser à tout prix. Bien utilisés, ils rendent le code lisible par quiconque connaît le vocabulaire ; mal utilisés, ils l'obscurcissent.