Aller au contenu

Head First Design Patterns

·2015 mots·10 mins
Eric Freeman Elisabeth Robson
Sommaire

La structure-même du livre est assez inhabituelle, et force le travail, la répétition et la compréhension (ce qui est plutôt bien). Ceci dit, cette même structure va parfois trop loin dans la dérision et dans une présentation un peu surfaite des personnages. J’aimais assez bien la découpe et la chronologie, ainsi que les exemples donnés, mais le fait qu’il soit un peu fouilli me dérangeait un peu.

Tous les patrons de conception (= design patterns, mais rassurez-vous, j’utiliserai uniquement ce terme par la suite) ne sont pas représentés, mais on y trouve les principaux - dont certains que vous utilisez sans doute déjà sans savoir qu’ils portent un petit nom bien à eux. En plus de représenter des solutions reconnues à des problèmes connus, les design patterns correspondent aussi à un vocabulaire commun.

Ces design patterns peuvent être regroupés selon plusieurs catégories :

  • Creational, orientés sur le découplage et l’instanciation de composants,
  • Behavioral pour aider aux interactions entre composants,
  • Structural, pour les structures plus larges.

Pour rappel, la Programmation Orientée Objets se base sur les principes suivants :

  1. Encapsuler ce qui varie,
  2. Favoriser la composition sur l’héritage,
  3. Programmer envers des interfaces, pas des classes concrètes.

Creational
#

Les patterns ci-dessous sont orientés sur la nécessité de découpler des composants entre eux.

Factory & Abstract Factory
#

Une factory s’occupe uniquement de la création d’un type d’objet. Il s’agit d’une interface pour créer un objet ou un type d’objet, mais qui laisse le choix aux sous-classes de décider quoi (réellement) instancier.

Nous voulons ici que nos instances dépendent toutes d’abstraction, ce qui permet de rebondir sur le principe d’inversion de dépendances :

  • Aucune variable ne doit conserver de référence vers une classe concrète,
  • Aucune classe ne doit dériver d’une classe concrète,
  • Nous ne voulons pas autoriser la surcharge de méthode par une classe héritée - sans quoi ce ne serait pas un héritage d’une interface.

L’objectif final d’une Factory (ou AbstractFactory) est de s’occuper de tout ce qui concerne la création de sous-types, à partir d’un type en particulier, puisqu’elle retournera systématiquement une référence héritant d’une interface d’un certain type. Une Factory va aussi garder une certaine forme de couplage entre différentes classes, mais uniquement pour en assurer la création en fonction de certains paramètres - c’est donc voulu et totalement assumé.

Singleton
#

Le Singleton s’assure de n’avoir (tout au long de la vie d’un programme) qu’une et une seule instance d’une classe particulière, afin d’éviter ne soit globale et ne consomme plein de ressources pour rien.

Un Singleton peut être modélisé en rendant le constructeur privé, et en rendant l’accès à ce constructeur statique :

public class Singleton {
    private static Singleton uniqueInstance;

    private Singleton();

    public static Singleton getInstance() {
        if (uniqueInstance == null) {
            uniqueInstance = new Singleton();
        }
        return uniqueInstance;
    }
}

Comme le constructeur est privé, il ne peut être instancié que par la classe elle-même. La méthode de classe est par contre publique et permet de retourner la référence vers la seule instance (si elle existe) ou de la créer (dans le cas contraire).

Pour le multithreading, il convient d’utiliser des Mutex - qui correspond au mot-clé synchronized en Java - afin de s’assurer que chaque thread ne puisse appeler ces méthodes que chacun à son tour.

Behavioral
#

Les patterns liés au comportement permettent de simplifier (ou mieux structurer) les intercations entre plusieurs composants.

Command
#

L’interface Command va permettre de contrôler des objets présentant des comportements similaires, mais présentant chacun des actions spécifiques. De manière plus générale, l’interface encapsule une requête de manière à paramétrer d’autres objets (LightSwitch, GarageDoorOpener, …).

Pour chaque objet devant être gérén il conviendra d’avoir une classe implémentant cette nouvelle interface.

Pour implémenter une méthode undo(), il suffirait d’ajouter une référence vers une deuxième commande : LightOff devient la référence de LightOn et inversément. Et pour pousser le vice, l’implémentation d’une macro nécessiterait juste d’avoir une liste de ce que la commande doit activer.

Le design du pattern Command permet notamment de gérer des listes de tâches, où il suffira d’appeler une méthode execute() pour chacune d’entre elles.

Iterator
#

Il existe énormément de type de “listes” possibles : ArrayList, Array, … mais tous autorisent à être appelés par une boucle for.... Le pattern Itérateur fournit une manière d’accéder à des éléments d’une agrégation d’objets, de manière séquentielle et sans exposer la structure interne.

En passant par un itérateur :

  • On code sur une interface plutôt que sur une implémentation,
  • On a le comportement qui décrit le fonctionnement (hasNext, …),
  • On utilise un composant reconnu, qui accepte du polymorphisme.

Template
#

Celui-ci, c’est un peu mon chouchou. Il consiste à définir les grandes étapes du comportement d’un modèle, pouvant être réutilisé entre plusieurs autres définitions.

Ainsi, les deux classes ci-dessous présentent les mêmes étapes d’un algorithme - elles sont juste nommées différemment :

class Coffee:
    def boil_water(self):
        ...

    def brew_coffee_grinds(self):
        ...

    def pour_in_cup(self):
        ...

    def add_sugar_and_milk(self):
        ...


class Tea:
    def boil_water(self):
        ...

    def steep_tea_bag(self):
        ...

    def pour_in_cup(self):
        ...

    def add_lemon(self):
        ...

On peut voir que la finalité est la même ("servir une boisson chaude"), tandis que les étapes varient légèrement, mais restent fortement similaires, et consistent à :

  1. Faire bouillir de l’eau,
  2. Préparer l’infusion,
  3. Servir dans une tasse,
  4. Ajouter des condiments.

Ces quatre étapes constituent les quatre méthodes principales d’une classe CaffeineBeverage, que l’on peut traduire de la manière suivante :

class CaffeineBeverage:
    def boil_water(self):
        ...

    def brew(self):
        raise NotImplementedError()

    def pour_in_cup(self):
        ...

    def add_condiments(self):
        ...

Nous pouvons à présent modéliser nos deux (sous-)classes Coffee & Tea simplement en surchargeant les méthodes qui nous intéressent, et en laissant le template faire le reste de son travail :

class Coffee(CaffeineBeverage):
    [snip]

    def brew(self):
        self.brew_coffee_grinds()


class Tea(CaffeineBeverage)
    [snip]

    def brew(self):
        self.steep_tea_bag()

Le template définit donc le squelette d’un algorithme dans une méthode, en autorisant aux sous-classes de surcharger certaines parties de cet algorithme, sans en changer la nature intrinsèque. Il est tout aussi facile de définir des hooks, qui permettent à la sous-classe de définir un comportement sur base de flags.

Les tableaux utilisent ce pattern pour les tris en utilisant les méthodes .CompareTo() (ou équivalentes) ou la surcharge d’opérateurs (__lte__, __gte__, …).

State
#

Le State pattern s’appuye sur la même modélisation que le Strategy, à la différence qu’il est surtout utilisé comme une simplification d’un ensemble de conditions, là où Strategy est surtout utilisé comme une alternative à l’héritage classique. La différence entre les deux se situe principalement au niveau de l’intention.

L’algorithme garde en mémoire l’état courant d’un élément et interagit avec celui-ci pour qu’il définisse (lui-même) l’état d’après, en fonction d’une action spécifique.

Chaque état implémente une interface commune, et chaque méthode de cette interface exprime une transition.

Observer
#

L’idée de ce pattern est de partir d’un ensemble d’observateurs, qui veulent pouvoir s’abonner ou se désabonner d’informations qu’un sujet pourrait leur communiquer. La modélisation en exemple reprend une station de météo, qui envoie régulièrement des données actualisées. Les observateurs reçoivent ces données pour les afficher différemment (= Display), suivant le type d’observation (Conditions météorologiques actuelles, statistiques, prévisions, …).

Comme améliorations, des propriétés du sujet peuvent être rendues publiques, afin que les observateurs puissent “choisir” spécifiquement les informations qu’ils pourraient vouloir afficher.

Strategy
#

Plutôt que d’implémenter une forme d’héritage dès qu’on en a l’occasion, l’idée est de différencier l’élément de son comportement.

L’exemple abordé est sans doute le plus “drôle”, puisque l’auteur propose de représenter certaines classes de canard (héritant tous d’une même classe Duck), jusqu’à arriver à modéliser un canard en plastique, qui ne pourra donc pas voler. On en arrive à comprendre la nécessité de dissocier l’implémentation du comportement par l’absurde1.

On en arrive ainsi à représenter une classe dont le comportement (FlyingBehaviour et QuackBehaviour sont totalement déléguées à d’autres classes). Les avantages sont nombreux, puisque cette modélisation respecte le Single Responsibility Principle tout en permettant une réutilisabilité très élevée. De cette manière, il suffit de déclarer une nouvelle classe, puis de lui injecter le comportement que nous attendons d’elle.

Une autre conclusion est qu’il est parfois mieux d’implémenter un has-a (= composition, délégation, …) qu’un is-a (= héritage). De la même manière, nous pouvons implémenter des personnes d’un jeu de rôles et “réutiliser” leurs armes en définissant un WeaponBehaviour :

Structural
#

Les patterns de type structure sont orientés sur la modélisation de larges structures (arborescences, simplification de l’accès à certaines ressources, simplification d’interface, limitation de paramètres pour les intercations avec certains composants, …).

Composite
#

L’intérêt des composite est de représenter des éléments contenant d’autres éléments d’un même type. Le modèle analysé se base sur des “menus” (d’un restaurant), qui peuvent contenir d’autres sous-menus. Il s’agit donc d’un pattern pouvant servir à représenter des arborescences - où nous trouverons des noeuds et des feuilles d’un arbre.

Attention qu’en termes de performances, ce pattern doit généralement être couplé à d’autres mécanismes (caching, dénormalisation, MPTT, …).

Decorator
#

Le décorateur va fonctionner avec un mécanisme de délégations : chaque décoration pouvant être complétée par de nouvelles données provenant d’un nouveau décorateur.

Le décorateur ajoute donc son propre comportement avant / après avoir délégué au comportement de l’objet qu’il décore.

Un des (gros) désavantages de ce pattern se situe au niveau du nombre de (petites) classes qu’il va nécessiter de créer, ce qui peut être déconcertant.

Adapter
#

L’adapter convertit une interface en une autre, afin d’assurer une forme de continuité entre un client et un champ mouvant. Plus concrètement, l’adapter intercepte une requête et la modifie pour la faire correspondre avec une interface en particulier (qui n’était initialement pas prévue à traiter les données qu’elle aurait reçue sans cet adapter) :

Plus concrètement, c’est de cette manière que sont utilisés les EAI, avec des adapteurs entre des applications qui émettent des données ou des requêtes, et des applications qui attendent ces mêmes requêtes, mais qui ne parlent pas toutes les deux (exactement) le même langage.

Pour faire le lien avec le décorateur et la façade :

  • L’adapteur convertit une interface en une autre,
  • Le décorateur ne modifie pas une interface, mais y ajoute des responsabilités,
  • La façade rend une interface plus simple.

Façade
#

Une façade consiste à encapsuler un ensemble de composants, en les référençant depuis un autre, afin d’en simplifier la chronologie d’appels.

Il s’agit d’une application du principe de Least Knowledge - aussi lié au principe de Ségrégation des interfaces) : si une instance doit appeler une deuxième instance, afin de récupérer à partir d’une troisième, nous aurons tout aussi bien fait d’encapsuler cette troisième dans la deuxième, afin que la première n’ait connaisse pas l’existance et n’ait pas à s’en soucier :

Proxy
#

Un “proxy” contrôle et gère les accès à une ressource. Il faut voir ce pattern pour une représentation de plusieurs objets encapsulés, afin d’en contrôler les différents accès - locaux ou à distance -, coûteux à instancier, à créer ou présentant des besoins spécifiques en sécurité.

Nous avons une interface qui définit un sujet, tandis que notre “vrai” sujet, ainsi que son proxy, implémentent tous les deux cette même interface. Ceci permet d’interagir avec le sujet en gardant les mêmes méthodes, mais en contrôlant les ressources et/ou accès qui y sont liés, au travers du proxy.

Si vous recherchez de la sécurité ou du lazy loading, c’est ceci qu’il vous faut 😉

Compound
#

Les patterns sont souvent utilisés en composition les uns avec les autres, afin de résoudre des problèmes connus. L’exemple repris dans le livre travaille sur la modélisation d’une “Quack Meeting” - la conférence des canards à bec et en plastique :

  • Nous proposons d’abord une interface Quackable, qui propose une méthode Quack(),
  • Si nous devons modéliser une oie (ça cancanne aussi, une oie), nous lui mettrons un adapter afin qu’elle puisse se fondre dans le cancan des canards,
  • Pour compter le nombre de quacks, nous utiliserons un décorateur,
  • Pour créer nos différents canards, nous utiliserons une factory,
  • Pour parcourir les différents canards, un itérateur,
  • … et nous placerons un observer pour intercepter les coins-coins (l’interface Quackable devant alors s’étendre à une autre interface, QuackObservable).

  1. en mode, “Si vous faites de l’héritage sans réfléchir, vous allez vous planter”. ↩︎