14  Décortiquer les patterns GoF avec GRASP

Craig Larman a proposé les GRASP pour faciliter la compréhension des forces essentielles de la conception orientée objet. Dans ce chapitre, on examine la présence des GRASP dans les patterns GoF. C’est une excellente façon de mieux comprendre et les principes GRASP et les patterns GoF.

14.1 Exemple avec Adaptateur

Le pattern Adaptateur a été proposé par Gamma, Helm, Johnson, et Vlissides (1994), et sa structure est illustrée sur la figure 14.1. La structure de base peut être trompeuse, car elle ne montre qu’une seule classe Adaptee qui représente l’interface API (externe) que l’on ne peut modifier. En réalité, ce pattern est plus intéressant lorsqu’on considère plusieurs cas de classe Adaptee, chacune avec une interface API différente.

Qu’est-ce qu’un calculateur de taxes ?

On rappelle ici le contexte du problème auquel a été appliqué le pattern Adaptateur, soit le problème de calcul des taxes. Le logiciel NextGen POS doit calculer les taxes pour les ventes, mais la compagnie vise un marché large (par exemple, les États-unis et le Canada). Puisque le calcul des taxes peut être différent selon l’État et même la ville (par exemple les villes de la Californie), la compagnie qui développe NextGen POS ne veut pas maintenir toute cette information dans une partie du logiciel, car la maintenance ne sera pas rentable. La solution est de permettre au système d’exploiter un module externe qui est un « calculateur de taxes », car ce genre de module existe (voir TaxCloud). Puisque la clientèle du NextGen POS pourrait utiliser des calculateurs différents (selon sa région ou son budget), il faut constater que l’interface API de chaque calculateur sera différente (faute de norme sur les interfaces API de calculateurs de taxe) et immuable (on ne peut pas s’attendre à ce que le fournisseur de chaque calculateur change son interface API ou fournisse le code source). Alors, le pattern Adaptateur est proposé comme solution à ce problème. Larman (2005) propose aussi que les modules externes soient considérés pour gérer l’inventaire et pour suivre la comptabilité, car ces fonctionnalités sont complexes et difficiles à faire correctement. Encore, si NextGen POS supporte plusieurs choix (fournisseurs) de ces modules externes, il y aura ensuite le problème des interfaces API différentes et immuables. Le pattern Adaptateur sera encore utile dans ce contexte.

Le chapitre A26/F23  présente l’exemple du pattern Adaptateur pour les calculateurs de taxes (figure F23.1/A26.1 ). On remarque que cette figure dans le livre de Larman ne montre pas les classes adaptées, mais nous les avons ajoutées dans la figure 14.2. Nous faisons une hypothèse sur les noms des méthodes pour l’interface API des classes adaptées, car cette information n’est pas spécifiée. En plus, nous n’avons pas spécifié les arguments (c’est « ? »), car les détails ne sont pas connus.

Un autre point à mentionner dans le problème de calcul des taxes est que ça dépend de l’article. Selon l’État ou la province, certains articles sont sujets à des taxes et d’autres non. A priori, le calculateur de taxes est l’expert pour décider combien de taxes pour chaque article, mais le calculateur n’est pas l’expert des articles. C’est pour cela que l’Adaptateur a accès à l’objet Vente qui agrège des LigneArticles. L’Adaptateur de calculateur de taxes pourra ainsi accéder à chaque article de la vente pour déterminer les taxes. L’objet Vente a une visibilité de paramètre.

Dans le contexte du projet NextGen POS, Larman propose l’utilisation de ce pattern pour plusieurs dimensions où des services externes ont plusieurs possibilités (l’inventaire, la comptabilité, etc.).

TargetRequest()AdapterRequest()adaptee -> SpecificRequest()AdapteeSpecificRequest()Clientadaptee

Figure 14.1: Le diagramme de classes du pattern Adaptateur (Gamma et coll., 1994). (PlantUML)

« interface »IAdaptateurCalculTaxesgetTaxes( Vente ) : Liste de LigneTaxesAdaptateurTaxMastergetTaxes( Vente ) : Liste de LigneTaxesAdaptateurGoodAsGoldTaxProgetTaxes( Vente ) : Liste de LigneTaxesTaxMastercalculateTaxes ( ? )GoodAsGoldTaxProcomputeTaxes ( ? )

Figure 14.2: Le pattern Adaptateur appliqué pour les services externes de calculateurs de taxes. Chaque service externe a une interface API distincte et immuable. Les adaptateurs concrets permettent de normaliser l’accès à ces interfaces API à travers une interface IAdaptateurCalculTaxes. (PlantUML)

14.2 Imaginer le code sans le pattern GoF

Chaque principe GRASP est défini avec un énoncé d’un problème de conception et avec une solution pour le résoudre. Pourtant, beaucoup d’exemples dans le livre de Larman (2005) sont des patterns GoF déjà appliqués (et le problème initial n’est pas toujours expliqué en détail).

Alors, pour mieux comprendre l’application des patterns GoF, on doit imaginer la situation du logiciel avant l’application du pattern. Dans l’exemple avec l’adaptateur pour les calculateurs de taxes, imaginez le code si l’on n’avait aucun adaptateur. À la place d’une méthode getTaxes() envoyée par la classe Vente à l’adaptateur, on aurait l’obligation de faire un branchement selon le type de calculateur de taxes externe utilisé actuellement (si l’on veut supporter plusieurs calculateurs). Donc, dans la classe Vente, il y aurait du code comme ceci :

/* calculateurTaxes est le nom du calculateur utilisé actuellement */
if (calculateurTaxes == "GoodAsGoldTaxPro") {
    /* série d'instructions pour interagir avec GoodAsGoldTaxPro */
} else if (calculateurTaxes == "TaxMaster") {
    /* série d'instructions pour interagir avec TaxMaster */
} else if /* ainsi de suite pour chacun des calculateurs */
    /* ... */
}

Pour supporter un nouveau calculateur de taxes, il faudrait coder une nouvelle branche dans le bloc de if/then. Ça nuirait à la clarté du code, et la méthode qui contient tout ce code deviendrait de plus en plus longue. Même si l’on faisait une méthode pour encapsuler le code de chaque branche, ça ferait toujours augmenter les responsabilités de la classe Vente. Elle est responsable de connaître tous les détails (l’interface API distincte et immuable) de chaque calculateur de taxes externe, puisqu’elle communique directement (il y a du couplage) avec ces derniers. Lorsqu’une classe prend plus de responsabilités, ça porte atteinte à sa cohésion.

Le pattern Adaptateur comprend les principes GRASP Faible Couplage, Forte Cohésion, Polymorphisme, Indirection, Fabrication Pure et Protection des variations. La figure 14.3 démontre la relation entre ces principes dans le cas d’Adaptateur.

   AdaptateurPatternsGoFMécanisme deProtection desvariationsMécanisme deFaible CouplageMécanisme deForte CohésionExemple dePolymorphismeMécanismed'IndirectionFabricationPurePrincipesGRASPFaible Couplage est une façon d'obtenir une protection à un point de variation. Polymorphisme est une façon d'obtenir une protection à un point de variationet un faible couplage. Une Indirection est une façon d'obtenir un faible couplage. Le pattern Adaptateur est une sorte d'Indirection et de Fabrication Pure quiapplique le principe de Polymorphisme.

Figure 14.3: Adaptateur et principes GRASP (figure F23.3/A26.3 ). (PlantUML)

On peut donc voir le pattern Adaptateur comme une spécialisation de plusieurs principes GRASP :

  • Polymorphisme
  • Indirection
  • Fabrication Pure
  • Faible Couplage
  • Forte Cohésion
  • Protection des variations

Avec cette identification de principes GRASP dans le pattern Adaptateur, êtes-vous en mesure d’expliquer en détail avec ce contexte concret comment Adaptateur est relié à ces principes ?

14.3 Identifier les GRASP dans les GoF

Pour identifier les principes GRASP dans un pattern GoF comme Adaptateur, on rappelle la définition de chaque principe GRASP et l’on essaie d’imaginer le problème qui pourrait exister éventuellement. Ensuite, on explique comment le principe (et le pattern GoF) résout le problème.

Consultez la figure 14.2 du pattern Adaptateur pour les sections suivantes.

14.3.1 Polymorphisme

Selon Larman (2005) :

Problème : Qui est responsable quand le comportement varie selon le type ?
Solution : Lorsqu’un comportement varie selon le type (classe), affectez la responsabilité de ce comportement – avec des opérations polymorphes – aux types selon lesquels le comportement varie.

Le « comportement qui varie » est la manière d’adapter les méthodes utilisées par le calculateur de taxes choisi à la méthode getTaxes(). Alors, cette « responsabilité » est affectée au type interface IAdaptateurCalculTaxes (et à ses implémentations) dans l’opération polymorphe getTaxes(). Cet aspect du pattern est illustré sur la figure 14.4.

Figure 14.4: Le mécanisme de Polymorphisme dans le pattern Adaptateur appliqué au problème de calculateurs de taxes. getTaxes(...) est un appel polymorphe. Selon la configuration du système (l’objet auquel IAdaptateurCalculTaxes fait référence), le comportement sera différent.

14.3.2 Fabrication Pure

Selon Larman (2005) :

Problème : En cas de situation désespérée, que faire quand vous ne voulez pas transgresser les principes de faible couplage et de forte cohésion ?
Solution : Affectez un ensemble très cohésif de responsabilités à une classe « comportementale » artificielle qui ne représente pas un concept du domaine – une entité fabriquée pour augmenter la cohésion, diminuer le couplage et faciliter la réutilisation.

La Fabrication Pure est la classe « comportementale et artificielle » qui est la hiérarchie IAdaptateurCalculTaxes (comprenant chaque adaptateur concret). Elle est comportementale puisqu’elle ne fait qu’adapter des appels. Elle est artificielle puisqu’elle ne représente pas un élément dans le modèle du domaine.

L’ensemble des adaptateurs concrets ont des « responsabilités cohésives » qui sont la manière d’adapter la méthode getTaxes() aux méthodes (immuables) des calculateurs de taxes externes. Elles ne font que ça. La cohésion est augmentée aussi dans la classe Vente, qui n’a plus la responsabilité de s’adapter aux calculateurs de taxes externes. C’est le travail qui a été donné aux adaptateurs concrets.

Le couplage est diminué, car la classe Vente n’est plus couplée directement aux calculateurs de taxes externes. La réutilisation des calculateurs est facilitée, car la classe Vente ne doit plus être modifiée si l’on veut utiliser un autre calculateur externe. Il suffit de créer un adaptateur pour ce dernier.

14.3.3 Indirection

Selon Larman (2005) :

Problème : Comment affecter les responsabilités pour éviter le couplage direct ?
Solution : Pour éviter le couplage direct, affectez la responsabilité à un objet qui sert d’intermédiaire avec les autres composants ou services.

Le « couplage direct » qui est évité est le couplage entre la classe Vente et les calculateurs de taxes externes. Le pattern Adaptateur (général) cherche à découpler le Client des classes nommées Adaptee, car chaque Adaptee a une interface API différente pour le même genre de « service ». Alors, la responsabilité de s’adapter aux services différents est affectée à la hiérarchie de « classes intermédiaires », soit l’interface type IAdaptateurCalculTaxes et ses implémentations.

14.3.4 Protection des variations

Selon Larman (2005) :

Problème : Comment affecter les responsabilités aux objets, aux sous-systèmes et aux systèmes de sorte que les variations ou l’instabilité de ces éléments n’aient pas d’impact négatif sur les autres ?
Solution : Identifiez les points de variation ou d’instabilité prévisibles et affectez les responsabilités afin de créer une « interface » stable autour d’eux.

Les « variations » ou « l’instabilité » sont les calculateurs de taxes qui ne sont pas sous le contrôle des développeurs et développeuses du projet (ce sont des modules externes ayant chacun une interface API différente). Quant à l’« impact négatif sur les autres », il s’agit des modifications qu’auraient à faire les développeurs et développeuses sur la classe Vente chaque fois que l’on décide de supporter un autre calculateur de taxes (ou si l’interface API de ce dernier évolue).

Quant aux « responsabilités » à affecter, c’est la fonctionnalité commune de tous les calculateurs de taxes, soit le calcul de taxes. Pour ce qui est de l’« interface stable », il s’agit de la méthode getTaxes(), qui ne changera (probablement) jamais. Elle est définie dans l’interface type IAdaptateurCalculTaxes. Cette définition isole (protège) la classe Vente des modifications (ajout de nouveaux calculateurs ou changements de leur interface API).

14.4 GRASP et réusinage

Il y a des liens entre les GRASP et les activités de Réusinage (Refactorisation). Alors, un IDE qui automatise les réusinages peut vous aider à appliquer certains GRASP.

14.5 Exercices

Pour ces exercices, suivez le modèle pour décortiquer le pattern Adaptateur, illustré à la figure 14.3.

Une bonne ressource pour les patterns GoF est la suivante :

https://fuhrmanator.github.io/oodp-horstmann/htm/index_fr_en.html

Astuce

Il se peut que certains principes GRASP ne s’appliquent pas à un pattern GoF !

Exercice 14.1 (Itérateur) Identifiez les 4 principes GRASP dans le pattern Itérateur, selon les directives présentées à la Section 14.3.

Exercice 14.2 (Observateur) Identifiez les 4 principes GRASP dans le pattern Observateur, selon les directives présentées à la Section 14.3.

Exercice 14.3 (Stratégie) Identifiez les 4 principes GRASP dans le pattern Stratégie, selon les directives présentées à la Section 14.3.

Exercice 14.4 (Composite) Identifiez les 4 principes GRASP dans le pattern Composite, selon les directives présentées à la Section 14.3.

Exercice 14.5 (Décorateur) Identifiez les 4 principes GRASP dans le pattern Décorateur, selon les directives présentées à la Section 14.3.

Exercice 14.6 (Méthode Template) Identifiez les 4 principes GRASP dans le pattern Méthode Template, selon les directives présentées à la Section 14.3.

Exercice 14.7 (Commande) Identifiez les 4 principes GRASP dans le pattern Commande, selon les directives présentées à la Section 14.3.

Exercice 14.8 (Méthode Fabrique) Identifiez les 4 principes GRASP dans le pattern Méthode de fabrique, selon les directives présentées à la Section 14.3.

Exercice 14.9 (Proxy) Identifiez les 4 principes GRASP dans le pattern Proxy, selon les directives présentées à la Section 14.3.

Exercice 14.10 (Façade) Identifiez les 4 principes GRASP dans le pattern Façade, selon les directives présentées à la Section 14.3.

Exercice 14.11 (Adaptateur pour Maps) Proposez une mise en œuvre du pattern GoF Adaptateur pour un système de livraison qui peut être configuré avec trois variantes du service de calcul d’itinéraires :

Le système veut obtenir une liste d’étapes (des directions) pour se rendre à une destination à partir d’un point de départ. L’utilisateur ou l’utilisatrice du système pourra décider lequel des services lui convient dans les préférences.

Le but de l’exercice est de déterminer l’interface stable (GRASP Protection des variations) étant donné les variantes des services de calcul d’itinéraires. Cela peut être un diagramme de classes réalisé avec PlantUML.