Introduction aux Design Patterns Java
Introduction aux design patterns en Java — ce qu'ils sont et comment utiliser les plus courants.
Un design pattern est une solution réutilisable et nommée à un problème qui revient souvent lors du développement logiciel. Les patterns ne sont pas des bibliothèques à importer ni du code à copier — ce sont des structures pour agencer classes et objets que les développeurs expérimentés ont adoptées au fil du temps. Les apprendre vous donne un vocabulaire commun : dites « on va utiliser une Factory ici » et un autre développeur Java comprend immédiatement ce que vous voulez dire.
Cette page présente ce que sont les design patterns, les trois familles dans lesquelles ils se répartissent, et trois des plus courants — Strategy, Factory et Singleton — avec un exemple exécutable qui les combine. Elle suppose que vous maîtrisez les interfaces et le polymorphisme, car presque chaque pattern s'appuie sur eux.
Les patterns ont été popularisés par le livre de 1994 Design Patterns du « Gang of Four », qui en répertoriait 23. Vous n'avez pas besoin des 23 pour commencer. Une poignée, appliquée au bon moment, rend le code plus facile à modifier sans le casser.
Les trois familles
Le catalogue classique divise les patterns en trois groupes selon ce qu'ils facilitent :
| Famille | Préoccupation | Exemples |
|---|---|---|
| Créationnels | Comment les objets sont créés | Singleton, Factory, Builder |
| Structurels | Comment les objets sont composés | Adapter, Decorator, Facade |
| Comportementaux | Comment les objets interagissent | Strategy, Observer, Iterator |
Java lui-même est rempli de ces patterns. StringBuilder est un Builder, Iterator est le pattern Iterator, et java.util.logging utilise des Singletons. Vous utilisiez déjà des patterns sans les nommer.
Strategy : changer l'algorithme
Le pattern Strategy encapsule une famille d'algorithmes interchangeables derrière une interface commune, de sorte que le code appelant peut basculer entre eux sans changer. Définissez l'interface, écrivez chaque variante dans sa propre classe, et laissez un contexte contenir celle dont il a besoin.
interface DiscountStrategy {
double apply(double price);
}
class PercentOff implements DiscountStrategy {
private final double percent;
PercentOff(double percent) { this.percent = percent; }
public double apply(double price) { return price * (1 - percent / 100); }
}Une classe contexte contient une DiscountStrategy et lui délègue, au lieu de brancher sur une chaîne if/switch. Ajouter une nouvelle remise signifie ajouter une classe — pas modifier le code existant. (Le contexte Checkout complet, ainsi que les variantes NoDiscount et FlatOff, apparaissent dans la démo exécutable ci-dessous.)
Factory : centraliser la création
Une Factory est une méthode unique (ou une classe) chargée de décider quel type concret instancier. Les appelants demandent un objet par description et obtiennent en retour un objet qui respecte l'interface, sans connaître la classe exacte.
static DiscountStrategy forCustomer(String tier) {
return switch (tier) {
case "gold" -> new PercentOff(20);
case "silver" -> new PercentOff(10);
default -> new NoDiscount();
};
}La logique de création se trouve en un seul endroit. Si les règles changent, vous modifiez la factory — chaque appelant continue de fonctionner sans changement.
Singleton : une seule instance
Un Singleton garantit qu'une classe n'a qu'une seule instance et fournit un point d'accès global à celle-ci. En Java moderne, un enum est le moyen le plus simple et thread-safe d'en écrire un — la JVM garantit que ses constantes sont créées exactement une fois. Consultez The Singleton Pattern pour les variantes à initialisation paresseuse et à double vérification.
enum Config {
INSTANCE;
private final String env = "production";
public String env() { return env; }
}
// usage
String e = Config.INSTANCE.env();Utilisez les Singletons avec parcimonie — ils introduisent un état global, ce qui complique les tests. Souvent, un objet unique passé via un constructeur (injection de dépendances) est le meilleur choix.
Quand ne pas utiliser un pattern
Les patterns ajoutent de la structure, et la structure a un coût. Un switch avec deux cas n'a pas besoin du pattern Strategy ; une classe instanciée une seule fois n'a pas besoin d'une Factory. Recourez à un pattern quand vous ressentez la douleur qu'il résout — branchements dupliqués, appels new éparpillés, câblage d'objets enchevêtré — pas avant. Appliquer les patterns à outrance produit un code plus difficile à lire que le problème qu'il était censé simplifier.
Strategy et Factory ensemble
L'exemple exécutable ci-dessous combine deux patterns. DiscountStrategy est l'interface Strategy avec trois implémentations ; forCustomer est une Factory qui en choisit une. Le contexte Checkout délègue à la stratégie qu'il contient, de sorte que son code ne change jamais au fur et à mesure que le comportement de tarification varie.
Ce qu'il faut retenir de l'exécution :
- Chaque niveau affiche un total différent —
regularreste à 100,00 $,silverdescend à 90,00 $,goldà 80,00 $,couponà 95,00 $ — même siCheckout.totalappelle la même méthode unique. - Le contexte
Checkoutne branche jamais sur le niveau lui-même ; il délègue simplement à laDiscountStrategyqu'il contient actuellement. - La Factory
forCustomerest le seul endroit qui sait quelle classe concrète correspond à quel niveau, de sorte que la logique de sélection se trouve en un seul endroit. - Changer de comportement se fait simplement avec
setStrategy(...)au moment de l'exécution — ajouter une quatrième remise signifierait une nouvelle classe et un cas de plus dans la factory, sans modifierCheckout. - Les dernières lignes confirment la stratégie active : après avoir resélectionné
gold, la politique indique20.0% offet le total est de 80,00 $, prouvant que le contexte reflète la dernière stratégie définie.