Points clés
1. Acceptez le changement : encapsulez ce qui varie et programmez sur des interfaces.
Identifiez les aspects de votre application qui varient et séparez-les de ce qui reste constant.
La constante du changement. Le développement logiciel est par nature dynamique : les exigences évoluent, et les systèmes doivent s’adapter sous peine de devenir obsolètes. Une erreur fréquente consiste à concevoir des systèmes rigides où une modification dans une zone entraîne des répercussions imprévues ailleurs dans le code. Le principe fondamental pour éviter cela est d’identifier les parties instables de votre application et de les isoler. En encapsulant ces éléments « variables », vous créez des frontières qui empêchent les modifications de provoquer des effets secondaires indésirables.
Programmez sur des abstractions. Cette encapsulation s’obtient idéalement en programmant sur une interface (ou un supertype), et non sur une implémentation concrète. Lorsque votre code interagit avec une interface, il reste flexible et peut fonctionner avec n’importe quelle classe qui implémente cette interface. Ainsi, vous pouvez introduire de nouvelles implémentations concrètes sans modifier le code client, respectant ainsi le principe « ouvert à l’extension, fermé à la modification ». Le patron Strategy illustre parfaitement cela en encapsulant des algorithmes interchangeables derrière une interface commune, permettant aux clients de varier le comportement indépendamment.
2. Privilégiez la composition à l’héritage pour des designs flexibles.
Favorisez la composition plutôt que l’héritage.
Les écueils de l’héritage. Bien que l’héritage permette la réutilisation du code, il conduit souvent à des designs rigides, surtout lorsque les comportements varient entre sous-classes. Ajouter un comportement dans une superclasse peut impacter involontairement toutes les sous-classes, même celles pour lesquelles ce comportement est inapproprié (par exemple, un canard en caoutchouc qui vole). Surcharger les méthodes dans chaque sous-classe pour désactiver un comportement non désiré engendre un cauchemar de maintenance.
La puissance du « HAS-A ». La composition, ou relation « a un », offre une alternative plus souple. Plutôt que d’hériter d’un comportement, les objets l’acquièrent en étant composés d’autres objets qui implémentent des comportements spécifiques. Cela permet de modifier dynamiquement le comportement d’un objet à l’exécution en remplaçant l’objet comportemental composé. Cette approche encapsule non seulement des familles d’algorithmes dans leurs propres classes, mais minimise aussi les interdépendances, conduisant à des systèmes plus adaptables et maintenables.
3. Détachez la création d’objets grâce aux usines.
Principe d’inversion des dépendances : dépendez des abstractions, pas des classes concrètes.
Le problème du new. Instancier directement des classes concrètes avec l’opérateur new lie étroitement votre code à des implémentations spécifiques, le rendant fragile face aux changements. Lorsqu’on introduit de nouveaux types concrets ou que ceux existants évoluent, le code truffé de new doit être modifié, violant ainsi le principe Ouvert-Fermé. Cela alourdit la maintenance et augmente le risque d’erreurs.
Les usines à la rescousse. Les patrons de type Factory encapsulent la création d’objets, déléguant la responsabilité d’instanciation à des objets ou méthodes dédiés. La Simple Factory est une forme basique, tandis que le Factory Method permet aux sous-classes de décider quelle classe concrète instancier, reportant la création sur elles. L’Abstract Factory va plus loin en fournissant une interface pour créer des familles d’objets liés sans spécifier leurs classes concrètes, garantissant la cohérence des ensembles (par exemple, des ingrédients régionaux pour une pizza). Cette approche respecte le principe d’inversion des dépendances, où composants de haut et bas niveau dépendent d’abstractions.
4. Étendez les fonctionnalités dynamiquement avec les décorateurs.
Le patron Décorateur ajoute dynamiquement des responsabilités à un objet. Il offre une alternative flexible à l’héritage pour étendre les fonctionnalités.
Éviter l’explosion des classes. L’héritage traditionnel peut engendrer une « explosion » de classes lorsqu’on cherche à ajouter plusieurs responsabilités optionnelles à des objets (par exemple, un café avec divers condiments). Cela crée un nombre ingérable de sous-classes. Le patron Décorateur propose une solution élégante en permettant d’envelopper les objets avec des objets « décorateurs » qui ajoutent de nouveaux comportements.
Flexibilité à l’exécution. Les décorateurs partagent le même supertype que les objets qu’ils décorent, ce qui permet un enveloppement transparent. Ils ajoutent leur propre comportement avant ou après avoir délégué à l’objet enveloppé. Ainsi, la fonctionnalité peut être étendue dynamiquement à l’exécution sans modifier le code existant, incarnant parfaitement le principe Ouvert-Fermé. Un exemple classique est le système d’E/S en Java, où les flux sont enveloppés par des décorateurs comme BufferedInputStream pour ajouter la mise en tampon.
5. Adaptez des interfaces incompatibles avec les adaptateurs.
Le patron Adaptateur convertit l’interface d’une classe en une autre interface attendue par les clients. Il permet à des classes incompatibles de collaborer.
Combler le fossé. Lorsqu’on intègre des classes ou bibliothèques existantes aux interfaces incompatibles, une interaction directe est impossible sans modifier l’une des parties. Le patron Adaptateur agit comme un traducteur, convertissant une interface en une autre attendue par le client. Cela permet à des classes autrement incompatibles de fonctionner ensemble harmonieusement.
Désaccoupler les clients. Un adaptateur se place entre le client et l’adapté, recevant les requêtes du client (via l’interface cible) et les traduisant en appels sur l’adapté (via son interface existante). Cela désaccouple le client de l’implémentation spécifique de l’adapté, rendant le système plus résilient aux évolutions des interfaces. Par exemple, un TurkeyAdapter peut faire apparaître un objet Turkey comme un Duck pour un client qui attend une interface Duck.
6. Informez les objets avec le patron Observateur.
Le patron Observateur définit une dépendance un-à-plusieurs entre objets, de sorte que lorsqu’un objet change d’état, tous ses dépendants sont automatiquement notifiés et mis à jour.
Couplage lâche pour les notifications. Lorsqu’un objet change d’état et que plusieurs autres doivent réagir, un design fortement couplé obligerait l’objet modifié à connaître et mettre à jour directement tous ses dépendants, rendant le système fragile. Le patron Observateur résout cela en établissant une relation un-à-plusieurs où un « Sujet » (l’objet modifié) notifie ses « Observateurs » (les dépendants) sans connaître leurs types concrets.
Flexible et extensible. Les observateurs s’enregistrent auprès du Sujet, qui, lors d’un changement d’état, parcourt la liste des observateurs enregistrés et appelle leur méthode commune update(). Ce couplage lâche permet d’ajouter ou de retirer des observateurs à tout moment sans modifier le Sujet. C’est un pilier des systèmes réactifs, visible dans les ActionListener de Java Swing ou les architectures événementielles, favorisant des designs flexibles et extensibles.
7. Encapsulez les requêtes pour une invocation flexible : le patron Commande.
Le patron Commande encapsule une requête sous forme d’objet, permettant de paramétrer d’autres objets avec différentes requêtes, de les mettre en file d’attente ou de les enregistrer, et de supporter des opérations annulables.
Désaccoupler l’action de l’invocateur. Dans des systèmes comme une télécommande, l’invocateur (le bouton) ne doit pas être étroitement lié aux actions spécifiques qu’il déclenche (par exemple, allumer une lumière). Le patron Commande transforme une requête en un objet autonome. Cet objet « Commande » encapsule à la fois l’action à réaliser et l’objet récepteur sur lequel elle s’applique.
Capacités puissantes. L’invocateur appelle simplement la méthode execute() sur l’objet Commande, sans connaître les détails de l’action ou du récepteur. Ce découplage permet des fonctionnalités avancées :
- Paramétrage : un invocateur peut être configuré avec différentes commandes à l’exécution.
- Mise en file / journalisation : les commandes peuvent être stockées ou enregistrées pour exécution ou récupération ultérieure.
- Annulation / rétablissement : les commandes peuvent implémenter une méthode
undo()pour inverser leurs effets, souvent en sauvegardant l’état précédent. - Macros : plusieurs commandes peuvent être regroupées en une seule
MacroCommande.
8. Simplifiez des sous-systèmes complexes avec des façades.
Le patron Façade fournit une interface unifiée à un ensemble d’interfaces dans un sous-système. La façade définit une interface de haut niveau qui facilite l’utilisation du sous-système.
Masquer la complexité. Les logiciels modernes comportent souvent des sous-systèmes complexes avec de nombreuses classes et interactions sophistiquées (par exemple, un système home cinéma). Les clients qui interagissent directement avec ces systèmes doivent surmonter une courbe d’apprentissage élevée et sont fortement couplés à de nombreux composants bas niveau. Le patron Façade offre une interface simplifiée et unifiée face à cette complexité.
Principe du moindre savoir. Une classe Façade agit comme point d’entrée unique, déléguant les requêtes clients aux objets appropriés du sous-système. Elle n’encapsule pas le sous-système (les clients peuvent toujours accéder aux composants individuels si besoin), mais fournit une API plus accessible et de haut niveau. Cela respecte le principe du moindre savoir (« ne parlez qu’à vos amis immédiats »), réduisant le nombre de classes avec lesquelles un client interagit directement. Le résultat est un système plus maintenable et compréhensible, les clients étant désaccouplés des détails internes du sous-système.
9. Définissez des squelettes d’algorithmes avec la méthode template.
Le patron Méthode Template définit le squelette d’un algorithme dans une méthode, en déléguant certaines étapes aux sous-classes. Il permet aux sous-classes de redéfinir certaines étapes sans modifier la structure globale.
Contrôler le déroulement de l’algorithme. Lorsque plusieurs classes partagent un algorithme commun mais diffèrent sur certaines étapes, le patron Méthode Template offre une solution puissante. Il définit la structure générale de l’algorithme dans une « méthode template » d’une classe abstraite, laissant certaines étapes comme méthodes abstraites ou « hooks » à implémenter par les sous-classes concrètes.
Principe d’Hollywood. Ce patron incarne le « principe d’Hollywood » (« Ne nous appelez pas, nous vous appellerons »), où la classe abstraite de haut niveau contrôle le flux de l’algorithme et fait appel à ses sous-classes pour les implémentations spécifiques. Cela garantit que la structure de l’algorithme reste cohérente et protégée (souvent en rendant la méthode template final), tout en permettant aux sous-classes de personnaliser les étapes individuelles. Des exemples incluent java.util.Arrays.sort() (où compareTo() est le hook) et JFrame.paint(), favorisant la réutilisation et la création de frameworks.
10. Gérez le comportement des objets via des objets d’état.
Le patron État permet à un objet de modifier son comportement lorsque son état interne change. L’objet semble alors changer de classe.
Éliminer le spaghetti conditionnel. Les objets dont le comportement varie fortement selon leur état interne conduisent souvent à des logiques conditionnelles complexes (par exemple, des if-else ou switch) dans leurs méthodes. Cela rend le code difficile à maintenir, étendre et comprendre. Le patron État propose une alternative propre en encapsulant le comportement de chaque état dans sa propre classe.
Délégation et changement apparent de classe. L’objet « Contexte » (celui dont l’état change) conserve une référence vers un objet « État », auquel il délègue tout comportement dépendant de l’état. Lorsque l’état interne du Contexte change, il remplace simplement son objet État courant par un autre. Du point de vue du client, le Contexte semble changer de classe, car son comportement est totalement différent. Bien que structurellement proche du patron Strategy, l’intention de l’État est de gérer les transitions internes d’état, le Contexte contrôlant souvent quel état est actif.
11. Contrôlez l’accès aux objets avec des proxys.
Le patron Proxy fournit un substitut ou un représentant pour un autre objet afin de contrôler l’accès à celui-ci.
La puissance d’un remplaçant. Lorsque l’accès direct à un objet est indésirable ou impraticable, le patron Proxy fournit un objet « substitut » ou « représentant » qui contrôle l’accès à l’objet réel. Le proxy implémente la même interface que le sujet réel, permettant aux clients d’interagir avec lui comme s’il s’agissait de l’objet réel, tandis que le proxy gère les complexités ou restrictions sous-jacentes.
Applications variées. Les proxys prennent plusieurs formes, chacune contrôlant l’accès différemment :
- Proxy distant : agit comme représentant local d’un objet dans une JVM distante (ex. Java RMI).
- Proxy virtuel : retarde la création d’un objet coûteux jusqu’à ce qu’il soit réellement nécessaire (ex. chargement d’images volumineuses).
- Proxy de protection : contrôle l’accès aux méthodes d’un objet selon les permissions de l’appelant (ex. rôles utilisateurs).
- Proxy de cache : fournit un stockage temporaire pour les résultats d’opérations coûteuses.
Ce patron désaccouple les clients de l’interaction directe avec des objets potentiellement problématiques, renforçant la robustesse et la performance du système.
12. Combinez les patrons pour des solutions puissantes : MVC.
Un patron composé combine deux ou plusieurs patrons pour résoudre un problème récurrent ou général.
Au-delà des patrons individuels. Si les patrons individuels résolvent des problèmes de conception spécifiques, leur véritable puissance se révèle souvent lorsqu’ils sont combinés en solutions plus larges et complètes. Ces « patrons composés » répondent à des défis architecturaux plus vastes en tirant parti des forces de plusieurs patrons agissant de concert.
Modèle-Vue-Contrôleur (MVC). Un exemple emblématique est MVC, un patron composé fondamental pour les interfaces utilisateur :
- Modèle : gère les données et la logique applicative, utilisant le patron Observateur pour notifier vues et contrôleurs des changements d’état.
- Vue : présente les données du modèle à l’utilisateur, souvent structurée avec le patron Composite pour les composants UI, et délègue la saisie utilisateur au contrôleur via le patron Strategy.
- Contrôleur : interprète la saisie utilisateur et la traduit en actions sur le modèle, agissant comme la stratégie de la vue.
MVC découple ces préoccupations, favorisant réutilisabilité, flexibilité et maintenabilité, et a été largement adopté dans divers types d’applications, notamment les frameworks web.
Résumé des avis
Head First Design Patterns reçoit des critiques extrêmement positives grâce à son approche à la fois ludique et accessible pour enseigner les design patterns. Les lecteurs apprécient particulièrement son humour, ses exemples concrets tirés du réel, ainsi que ses techniques d’apprentissage interactives. Beaucoup le jugent supérieur aux manuels traditionnels, notamment pour les débutants. Ce livre est salué pour ses explications claires, ses exemples pratiques et son insistance sur le moment opportun pour appliquer chaque pattern. Certains lecteurs relèvent que tous les patterns ne sont pas abordés en profondeur, mais dans l’ensemble, il est vivement recommandé comme introduction aux design patterns pour les programmeurs de tous niveaux d’expérience.
Les lecteurs ont aussi lu