Aller au contenu principal
genie-logicielLes design patterns

Les design patterns

Ce que ce chapitre apporte

  • Définir ce qu'est un design pattern, et ce qu'il n'est pas.
  • Nommer les trois familles et savoir y ranger un pattern donné.
  • Lire un diagramme de classes de pattern et retrouver le rôle de chaque participant.
  • Implémenter Stratégie, Observateur, Fabrique, Singleton et Pont.
  • Justifier l'emploi d'un pattern par le couplage qu'il supprime.
  • Distinguer Stratégie de Pont, que l'on confond presque toujours.
  • Reconnaître les cas où un pattern coûte plus qu'il ne rapporte.
Où on va
Les mêmes difficultés de conception reviennent d'un projet à l'autre : faire varier un algorithme sans toucher au code qui l'appelle, prévenir plusieurs composants qu'une donnée a changé, choisir une classe à l'exécution, éviter que deux axes de variation ne multiplient les sous-classes. Des solutions éprouvées existent, elles portent un nom, et ce nom est la moitié de leur intérêt. Ce chapitre en détaille cinq, avec le code avant et après, le diagramme, et surtout les cas où il ne faut pas les employer.

Ce qu'est un design pattern

Définition

Un design pattern, ou patron de conception, est une solution générale et réutilisable à un problème de conception qui revient fréquemment. Il décrit une organisation de classes et de responsabilités, pas un code à recopier.

Le terme vient de l'architecture, où Christopher Alexander avait catalogué des situations récurrentes de construction. Quatre auteurs, connus comme le Gang of Four, ont transposé l'idée au logiciel en 1994 et décrit vingt-trois patterns. Ce catalogue reste la référence, et la plupart des bibliothèques que vous utiliserez en portent la trace jusque dans le nom de leurs classes.

Un pattern ne s'installe pas comme une dépendance. Il se reconnaît dans une situation, puis s'adapte. Deux implémentations du même pattern dans deux projets ne se ressemblent pas forcément, et c'est normal : ce qui est commun est la structure des responsabilités, pas les lignes de code.

Le vocabulaire est la moitié de l'intérêt
Dire « ici on met un observateur » transmet en trois mots une structure qu'il faudrait un quart d'heure à décrire, et l'interlocuteur sait immédiatement quoi chercher dans le code.
C'est aussi ce qui rend la relecture possible : un lecteur qui reconnaît le pattern n'a plus à comprendre le mécanisme, seulement à vérifier qu'il est correctement appliqué.

La fiche d'un pattern

Un pattern se décrit toujours par les mêmes rubriques, et c'est ce qui permet de les comparer.

RubriqueQuestion à laquelle elle répond
Intentionquel problème de conception le pattern résout-il ?
Participantsquelles classes, avec quel rôle ?
Structurecomment sont-elles reliées ?
Collaborationdans quel ordre s'appellent-elles à l'exécution ?
Conséquencesqu'est-ce que cela coûte, et qu'est-ce que cela rend possible ?
Un pattern se paie
Chacun ajoute au minimum une interface, une classe et un niveau d'indirection. La lecture du code devient moins directe : pour savoir ce qui s'exécute, il faut suivre une référence au lieu de lire la ligne suivante.
Ce coût se justifie quand le problème est réellement présent. Appliquer un pattern parce qu'on vient de l'apprendre produit une conception plus difficile à lire que le code direct qu'elle remplace. La question à se poser reste : quel couplage est-ce que je supprime, et est-ce qu'il me gêne aujourd'hui ?

Les trois familles

FamilleCe dont elle s'occupePatterns du catalogue
Créationcomment les objets sont instanciésSingleton, Fabrique, Fabrique abstraite, Monteur, Prototype
Structurecomment les objets se composentPont, Adaptateur, Décorateur, Façade, Composite, Proxy
Comportementcomment les objets communiquentStratégie, Observateur, Commande, Itérateur, État, Visiteur

Le classement se retrouve à partir d'une seule question : le pattern parle-t-il de la naissance des objets, de leur assemblage, ou de leurs échanges ?

Le principe commun

Presque tous les patterns appliquent la même idée : dépendre d'une abstraction plutôt que d'une implémentation. C'est le cinquième principe SOLID, l'inversion des dépendances, vu au premier chapitre.

Regardez ce que fait ce code, et surtout ce qu'il empêche.

C#
// Avant : la classe connaît toutes les variantes, et le choix est figé dans un test.
public class Facture
{
public decimal Total(Commande commande, string modeLivraison)
{
decimal frais;
if (modeLivraison == "standard")
frais = 4.90m;
else if (modeLivraison == "express")
frais = 12.50m + 0.60m * commande.Poids;
else if (modeLivraison == "point-relais")
frais = 2.90m;
else
throw new ArgumentException("Mode inconnu");
return commande.MontantHT + frais;
}
}

Trois défauts, et ils se cumulent. Ajouter un mode de livraison oblige à modifier Facture, alors qu'elle n'a rien à voir avec la question. Tester la tarification express demande de construire une facture entière. Et le jour où le calcul express dépend du jour de la semaine, la méthode se met à grossir dans une classe qui ne parle pas de tarifs.

Le pattern Stratégie règle exactement cela.

Stratégie

Intention

Définir une famille d'algorithmes, les encapsuler chacun dans une classe, et les rendre interchangeables. L'algorithme varie indépendamment du code qui l'utilise.

Diagramme de classes
«interface»
IFraisLivraison
+ Calculer(commande)
FraisStandard
+ Calculer(commande)
FraisExpress
- _tarifAuKilo : decimal
+ Calculer(commande)
FraisPointRelais
+ Calculer(commande)
Facture
- _frais : IFraisLivraison
+ Facture(frais)
+ Total(commande)

Les rôles : Facture est le contexte, il détient une référence vers l'abstraction et ne connaît rien d'autre. IFraisLivraison est la stratégie, le contrat commun. Les trois classes concrètes sont les stratégies concrètes.

C#
public interface IFraisLivraison
{
decimal Calculer(Commande commande);
}
public sealed class FraisStandard : IFraisLivraison
{
public decimal Calculer(Commande commande) => 4.90m;
}
public sealed class FraisExpress : IFraisLivraison
{
private readonly decimal _tarifAuKilo;
public FraisExpress(decimal tarifAuKilo) => _tarifAuKilo = tarifAuKilo;
public decimal Calculer(Commande commande) => 12.50m + _tarifAuKilo * commande.Poids;
}
public sealed class Facture
{
private readonly IFraisLivraison _frais;
// La stratégie est fournie de l'extérieur : la facture ne choisit pas.
public Facture(IFraisLivraison frais) => _frais = frais;
public decimal Total(Commande commande) => commande.MontantHT + _frais.Calculer(commande);
}

Ce que le changement apporte : ajouter un mode de livraison n'oblige plus à toucher Facture, on écrit une classe de plus. Tester FraisExpress demande trois lignes et aucune facture. Et le choix du mode peut désormais venir d'une configuration, d'une base de données ou d'un test, puisqu'il se fait au moment de la construction.

Le test n'a pas disparu, il a déménagé
Quelqu'un doit toujours décider quelle stratégie construire, et ce quelqu'un contient encore un test sur la chaîne "express". La différence est qu'il est seul, à la frontière du système, là où la donnée arrive, au lieu d'être disséminé dans chaque classe qui a besoin du calcul.
Ce point de décision unique est très souvent une fabrique, le pattern suivant.

À l'exécution, la collaboration est d'une simplicité qui vaut d'être vue : le contexte ne fait que déléguer.

Diagramme de séquence
ClientFactureFraisExpressTotal(commande)Calculer(commande)20.30la facture ignore laquelle des trois répond320.30

Le même pattern en Python

Un objet-stratégie qui n'a qu'une méthode est, dans un langage où les fonctions sont des valeurs, simplement une fonction. Le pattern ne disparaît pas, il s'écrit plus court.

main.py
Sortie
>_ Prêt à exécuter…
Quand Stratégie ne sert à rien
S'il n'y a qu'une seule variante et aucune perspective d'en ajouter, l'interface n'apporte qu'un fichier de plus.
Si les variantes ont besoin de données différentes, le contrat commun devient un sac de paramètres facultatifs, et la signature ment sur ce que chaque implémentation utilise vraiment. C'est le signe que le découpage n'est pas au bon endroit.

Observateur

Intention

Définir une dépendance de un vers plusieurs entre objets, de sorte que lorsque l'un change d'état, tous ceux qui en dépendent en soient avertis, sans qu'il ait à les connaître.

Le problème se voit bien sur un capteur. Une température est relevée, et trois choses doivent en découler : l'affichage se met à jour, le journal enregistre, une alerte part au-delà d'un seuil. Écrit naïvement, le capteur appelle les trois, donc il en dépend, donc ajouter un quatrième usage revient à modifier le capteur. Et le capteur, qui devrait ne parler que de mesure, se met à connaître l'interface graphique.

Diagramme de classes
«interface»
IObservateur
+ Notifier(mesure)
Affichage
+ Notifier(mesure)
Journal
+ Notifier(mesure)
Alerte
- _seuil : double
+ Notifier(mesure)
Capteur
- _observateurs : List
+ Abonner(o)
+ Desabonner(o)
+ Mesurer()

Le sujet est Capteur : il tient la liste des abonnés et les prévient. Les observateurs sont les trois classes concrètes. Le sens de la dépendance est l'essentiel du pattern : ce sont les observateurs qui connaissent le sujet, jamais l'inverse.

C#
public interface IObservateur
{
void Notifier(Mesure mesure);
}
public sealed class Capteur
{
private readonly List<IObservateur> _observateurs = new();
public void Abonner(IObservateur o) => _observateurs.Add(o);
public void Desabonner(IObservateur o) => _observateurs.Remove(o);
public void Mesurer()
{
var mesure = new Mesure(DateTime.UtcNow, LireLeCapteur());
// Copie de la liste : un observateur peut se désabonner en réagissant.
foreach (var o in _observateurs.ToArray())
o.Notifier(mesure);
}
}

En .NET, le pattern est intégré au langage sous la forme des événements, qui en sont exactement la structure : event déclare la liste d'abonnés, += abonne, -= désabonne.

C#
public sealed class CapteurEvenementiel
{
public event Action<Mesure>? MesurePrise;
public void Mesurer()
{
var mesure = new Mesure(DateTime.UtcNow, LireLeCapteur());
MesurePrise?.Invoke(mesure); // n'appelle rien s'il n'y a aucun abonné
}
}
// Ailleurs, sans que le capteur en sache rien :
capteur.MesurePrise += m => Console.WriteLine($"{m.Horodatage} : {m.Valeur} °C");
Diagramme de séquence
CapteurAffichageJournalAlerteNotifier(mesure)okNotifier(mesure)okNotifier(mesure)seuil dépassé, un courriel partok
Trois pièges de l'observateur, dans l'ordre de fréquence
L'abonnement jamais résilié. Le sujet garde une référence vers l'observateur, donc le ramasse-miettes ne peut pas le libérer. Une fenêtre fermée mais toujours abonnée reste en mémoire, et continue de réagir. C'est la fuite mémoire la plus courante en .NET, et le chapitre sur la mémoire y revient.
L'exception dans un observateur. Si le troisième abonné lève une exception, les suivants ne sont jamais appelés. Un sujet robuste protège chaque notification.
L'ordre supposé. Rien ne garantit l'ordre des notifications, et un code qui en dépend fonctionnera jusqu'au jour où l'on ajoutera un abonné.

Le mécanisme tient en peu de lignes, et l'écrire une fois suffit à ne plus jamais le confondre avec autre chose.

main.py
Sortie
>_ Prêt à exécuter…

Fabrique

Intention

Déléguer la création d'un objet à un code dédié, pour que le client n'ait pas à connaître la classe concrète qu'il obtient.

Le terme recouvre trois choses distinctes, souvent confondues, et il vaut mieux les nommer correctement.

NomCe que c'estQuand
Fabrique simpleune méthode qui, selon une donnée, retourne l'implémentation adéquatele cas courant
Fabrique (méthode)une méthode d'instance redéfinie par les sous-classes, chacune créant son produitquand la création dépend de la sous-classe
Fabrique abstraiteun objet qui crée une famille cohérente de produitsquand plusieurs produits doivent aller ensemble

La fabrique simple, la plus employée, est ce point de décision unique évoqué plus haut.

C#
public static class FabriqueFrais
{
public static IFraisLivraison Creer(string mode) => mode switch
{
"standard" => new FraisStandard(),
"express" => new FraisExpress(tarifAuKilo: 0.60m),
"point-relais" => new FraisPointRelais(),
_ => throw new ArgumentException($"Mode de livraison inconnu : {mode}"),
};
}

Le test sur la chaîne existe toujours, mais il est seul, à un endroit qu'on sait désigner, et c'est le seul fichier à modifier pour ajouter un mode. Le reste du programme ne manipule que IFraisLivraison.

Le vrai critère
Une fabrique se justifie quand le choix de la classe dépend d'une donnée connue seulement à l'exécution : une valeur de configuration, un format de fichier détecté, un choix de l'utilisateur, une ligne en base.
Si le type est connu à l'écriture du code, new suffit. Une fabrique qui n'a qu'un cas est une indirection gratuite.

La fabrique abstraite répond à un autre besoin : garantir la cohérence d'une famille. Une application qui doit produire ses documents entièrement en PDF, ou entièrement en HTML, sans jamais mélanger un en-tête PDF avec un tableau HTML, s'en remet à un objet qui crée les deux.

Diagramme de classes
«interface»
IFabriqueDocument
+ CreerEntete() : IEntete
+ CreerTableau() : ITableau
FabriquePdf
+ CreerEntete() : IEntete
+ CreerTableau() : ITableau
FabriqueHtml
+ CreerEntete() : IEntete
+ CreerTableau() : ITableau
Rapport
- _fabrique : IFabriqueDocument
+ Produire()

Singleton

Intention

Garantir qu'une classe ne possède qu'une seule instance, et fournir un point d'accès à celle-ci.

La structure repose sur trois éléments : un constructeur privé, qui interdit le new depuis l'extérieur, un champ statique qui détient l'unique instance, et une propriété statique qui la retourne.

C#
public sealed class Configuration
{
// Lazy<T> garantit l'unicité même si plusieurs threads demandent en même temps,
// et ne construit rien tant que personne ne demande.
private static readonly Lazy<Configuration> _instance = new(() => new Configuration());
public static Configuration Instance => _instance.Value;
private readonly Dictionary<string, string> _valeurs;
private Configuration() // personne d'autre ne peut construire
{
_valeurs = ChargerDepuisFichier("app.config");
}
public string Lire(string cle) => _valeurs[cle];
}
Écrire un singleton naïvement ne marche pas
La version que l'on écrit spontanément, « si l'instance est nulle, la créer », est fausse dès que deux threads y arrivent ensemble : les deux voient le champ nul, les deux construisent, et deux instances existent.
Les solutions correctes sont un champ static readonly initialisé à la déclaration, ou Lazy<T> comme ci-dessus. Le verrouillage à double vérification, longtemps recommandé, est difficile à écrire correctement et n'apporte plus rien.

C'est le pattern le plus connu et le plus critiqué, et il faut savoir pourquoi.

Un singleton est un état global. Tout code peut l'atteindre sans le déclarer, donc les dépendances d'une classe ne se lisent plus dans sa signature : un constructeur qui ne prend rien peut dépendre de six singletons. Il rend aussi les tests difficiles, puisqu'on ne peut pas lui substituer une version d'essai, et que l'instance survit d'un test à l'autre avec l'état laissé par le précédent.

Unicité et accès global sont deux choses différentes
Le besoin réel est presque toujours « il ne doit y avoir qu'une seule connexion à ce matériel », pas « n'importe qui doit pouvoir y accéder de partout ».
On obtient l'unicité en construisant l'objet une fois au démarrage et en le passant à ceux qui en ont besoin. C'est ce que fait un conteneur d'injection de dépendances avec un cycle de vie singleton : une instance unique, mais déclarée dans les constructeurs, donc visible et remplaçable en test.
Le singleton classique reste défendable pour un accès à une ressource matérielle unique, ou pour un objet sans état comme un journal.

Pont

Intention

Découpler une abstraction de son implémentation, afin que les deux puissent varier indépendamment.

Le problème est celui de la multiplication des sous-classes. Une application produit des rapports, qui existent en deux formes, résumé et détaillé, et doivent partir vers trois destinations, écran, PDF, courriel. Par l'héritage seul, il faut six classes : ResumeEcran, ResumePdf, ResumeCourriel, DetailEcran, et ainsi de suite. Un quatrième canal en ajoute deux, un troisième type de rapport en ajoute quatre. La formule est en n × m, et le code de rendu se retrouve dupliqué dans chaque branche.

Le pont sépare les deux axes et les relie par une référence.

Diagramme de classes
Rapport
- _sortie : ISortie
+ Rapport(sortie)
+ Produire(donnees)
RapportResume
+ Produire(donnees)
RapportDetaille
+ Produire(donnees)
«interface»
ISortie
+ Ecrire(texte)
SortieEcran
+ Ecrire(texte)
SortiePdf
+ Ecrire(texte)
SortieCourriel
+ Ecrire(texte)

On passe de n × m classes à n + m. Ajouter un canal coûte une classe, et aucun rapport ne change.

C#
public abstract class Rapport
{
protected readonly ISortie Sortie; // le pont
protected Rapport(ISortie sortie) => Sortie = sortie;
public abstract void Produire(Donnees donnees);
}
public sealed class RapportResume : Rapport
{
public RapportResume(ISortie sortie) : base(sortie) { }
public override void Produire(Donnees donnees)
{
Sortie.Ecrire($"Total : {donnees.Total:C}");
Sortie.Ecrire($"Sur {donnees.Lignes.Count} lignes");
}
}
// Les deux axes se combinent librement, au moment de la construction.
var rapport = new RapportResume(new SortiePdf("bilan.pdf"));
rapport.Produire(donnees);
Pont ou Stratégie ? Les deux se ressemblent, et se confondent
Les deux diagrammes se ressemblent : un objet qui délègue à une interface. La différence est dans l'intention, et elle est nette.
Stratégie fait varier un algorithme, souvent interchangeable en cours d'exécution, et le contexte n'a en général pas de hiérarchie propre.
Pont fait varier toute une hiérarchie d'abstraction en parallèle d'une hiérarchie d'implémentation. Il y a des sous-classes des deux côtés, et le lien est fixé à la construction.
Le test qui tranche : s'il n'y a qu'une seule classe du côté gauche, c'est une stratégie ; s'il y a deux hiérarchies qui grossiraient l'une par l'autre, c'est un pont.

Trois autres que vous croiserez

Adaptateur (structure). Faire collaborer deux interfaces incompatibles en intercalant une classe qui traduit l'une dans l'autre. C'est le pattern qu'on écrit sans le savoir chaque fois qu'on enveloppe une bibliothèque externe pour ne pas en dépendre partout. Il ne change aucun comportement, il change une signature.

Décorateur (structure). Ajouter une responsabilité à un objet en l'enveloppant dans un autre qui implémente la même interface et délègue. Les flux de .NET en sont l'exemple canonique : un GZipStream enveloppe un FileStream, un BufferedStream enveloppe le tout, et chacun ignore les autres. À la différence de l'héritage, les décorations se composent à l'exécution et dans n'importe quel ordre.

Commande (comportement). Transformer une demande en objet, avec ses paramètres. Ce qui semble une complication gratuite devient évident dès qu'on veut une file d'attente, une exécution différée, un journal des actions, ou une annulation : un objet se stocke, se rejoue et sait s'inverser, ce qu'un appel de méthode ne sait pas faire.

Reconnaître le pattern qui manque

Un pattern se choisit à partir d'un symptôme dans le code, jamais à partir d'une envie.

Symptôme dans le codePattern à envisager
Un switch sur un type qui grossit à chaque nouveau casStratégie, ou Fabrique
Une classe qui appelle explicitement trois autres pour les prévenirObservateur
Des new de classes concrètes disséminés dans tout le programmeFabrique
Des sous-classes dont le nom combine deux axes (ResumePdf)Pont
Une bibliothèque externe dont le nom apparaît dans vingt fichiersAdaptateur
Des options empilées par des booléens dans un constructeurDécorateur
Un besoin d'annuler, de rejouer ou de mettre en fileCommande
Le pattern appliqué à l'envers
Le défaut le plus fréquent n'est pas d'ignorer les patterns, c'est de les appliquer sans le problème correspondant : une interface à une seule implémentation, une fabrique à un seul cas, un observateur pour un unique abonné connu à l'avance.
Le résultat est un code où chaque appel demande d'ouvrir trois fichiers pour savoir ce qui s'exécute, sans qu'aucune souplesse ait jamais servi. Le remède est de laisser le besoin arriver : la deuxième variante justifie l'abstraction, pas la première.

Exercices type

Quelle différence entre un design pattern et une bibliothèque ?

Une bibliothèque est du code que l'on installe et que l'on appelle. Elle est déjà écrite, et elle impose sa forme.

Un pattern est une structure de responsabilités que l'on écrit soi-même, adaptée au contexte. Deux applications du même pattern peuvent avoir des noms de classes et des signatures différents.

Conséquence pratique : on n'ajoute pas un pattern comme une dépendance, on le reconnaît dans un problème. Et si le problème n'est pas là, appliquer le pattern n'apporte que de l'indirection.

Un switch sur le mode de livraison apparaît dans quatre classes. Que faire ?

C'est le symptôme du pattern Stratégie. Chaque branche du test devient une classe implémentant une interface commune, et les quatre classes reçoivent l'interface au lieu de la chaîne de caractères.

Le test lui-même ne disparaît pas : il subsiste une fois, dans une fabrique, à l'endroit où la donnée entre dans le système.

Le gain se mesure au moment d'ajouter un mode : une classe nouvelle et une ligne dans la fabrique, au lieu de quatre switch à retrouver et à modifier sans en oublier.

Pourquoi la dépendance va-t-elle de l'observateur vers le sujet, et non l'inverse ?

Parce que c'est ce qui permet au sujet d'ignorer qui l'écoute.

Si le capteur appelait l'affichage, le journal et l'alerte, il en dépendrait toutes les trois : ajouter un usage voudrait dire modifier le capteur, et un capteur ne devrait pas connaître une interface graphique.

En inversant, le sujet ne connaît qu'une interface IObservateur et une liste. Les abonnés viennent à lui. Ajouter un usage n'oblige à modifier ni le capteur ni les observateurs existants, ce qui est exactement le principe ouvert-fermé.

Un singleton complique les tests. Pourquoi, et par quoi le remplacer ?

Pour deux raisons. On ne peut pas lui substituer une version d'essai, puisque le point d'accès est statique et le constructeur privé. Et l'instance survit d'un test à l'autre, donc l'état laissé par le premier influence le second, ce qui produit des échecs qui dépendent de l'ordre d'exécution.

Le remplacement consiste à séparer les deux besoins que le pattern confond : l'unicité et l'accès global. On construit l'objet une seule fois au démarrage, et on le passe par constructeur à ceux qui en ont besoin.

La dépendance redevient visible dans la signature, et un test fournit ce qu'il veut. C'est ce que fait un conteneur d'injection avec un cycle de vie singleton.

Comment distinguer Pont de Stratégie ?

Par le nombre de hiérarchies.

Stratégie fait varier un algorithme : une seule famille de classes interchangeables, souvent remplaçables en cours d'exécution, et le client n'a pas de sous-classes.

Pont fait varier deux dimensions en parallèle : il y a des sous-classes des deux côtés, et le lien est établi à la construction pour la durée de vie de l'objet.

Le test décisif : demandez-vous si, sans le pattern, vous auriez n × m classes dont le nom combine deux axes. Si oui, c'est un pont.

Un collègue propose une interface, une fabrique et un observateur pour une fonctionnalité qui n'a qu'un seul cas. Que répondre ?

Que le coût est immédiat et le bénéfice hypothétique.

Chaque indirection rend le code plus difficile à suivre : pour savoir ce qui s'exécute, il faut ouvrir plusieurs fichiers et remonter une référence. Une interface à implémentation unique n'apporte aucune souplesse, seulement un fichier.

L'argument « on en aura besoin plus tard » se retourne : le jour où la deuxième implémentation arrive, extraire l'interface est une opération mécanique que l'éditeur fait en une commande. Anticiper coûte tout de suite ; extraire plus tard coûte une minute.

Où se trouve la fuite mémoire classique de l'observateur ?

Dans l'abonnement jamais résilié.

Le sujet conserve une référence vers chaque abonné. Tant que le sujet vit, aucun abonné ne peut être libéré, même si plus rien d'autre ne le référence. Une fenêtre fermée mais toujours abonnée reste en mémoire et continue de réagir aux notifications.

La règle est que celui qui s'abonne se désabonne, au moment où il cesse d'être utile : Dispose, fermeture de la fenêtre, fin du traitement. En .NET, cela veut dire un -= pour chaque +=.

La méthode

  1. Pars du symptôme, pas du catalogue : quel couplage te gêne aujourd'hui ?
  2. Nomme les participants avant d'écrire : qui est le contexte, qui est l'abstraction ?
  3. Vérifie qu'il y aura une deuxième implémentation. S'il n'y en a qu'une, attends.
  4. Fais entrer la dépendance par le constructeur, jamais par un accès statique.
  5. Regroupe les décisions de création en un point unique plutôt que de les disséminer.
  6. Désabonne ce que tu as abonné, dans le même objet.
  7. Relis en te demandant si un lecteur qui ne connaît pas le pattern comprendra le code.

En résumé

  • Un design pattern est une structure de responsabilités, pas du code à recopier.
  • Le vocabulaire commun est la moitié de l'intérêt : il rend la conception discutable.
  • Trois familles : création, structure, comportement.
  • Presque tous appliquent la même idée : dépendre d'une abstraction.
  • Stratégie encapsule chaque algorithme dans une classe interchangeable.
  • Observateur inverse la dépendance : le sujet ignore qui l'écoute.
  • Fabrique rassemble en un point unique le choix de la classe concrète.
  • Singleton confond unicité et accès global ; l'injection sépare les deux.
  • Pont transforme n × m sous-classes en n + m.
  • Un pattern appliqué sans son problème ne laisse que le coût.

Et ensuite ? Ces patterns organisent des classes entre elles. À l'échelle d'une application entière, la même question se pose et reçoit des réponses du même genre : le chapitre suivant traite des architectures MVC et MVVM.

Les design patterns | Plateforme ETS