Il y a quelques jours j’ai posté un article pour mettre en garde les gens à propos de quelques pièges qui nous pendent au nez lorsqu’on met en place une architecture hexagonale. Un de ces pièges est de faire fuir une partie de la logique du domaine vers un ou plusieurs adaptateurs (côté code d’infrastructure donc). Si je suis convaincu que c’est quelque chose qu’il faut absolument éviter pour que notre code du domaine reste cohérent et détaché de tout enjeu technique, je voudrai ici m’attarder un peu et revaloriser une zone que j’ai moi-même pris l’habitude de décrier légèrement : le code à l’extérieur de l’hexagone. Aujourd’hui, le pratiquant passionné de Domain Driven Design que je suis veut affirmer que les adaptateurs de droite de l’architecture hexagonale méritent beaucoup mieux que le traitement qu’on leur réserve habituellement. Et si ce code d’infrastructure pouvait finalement être tout aussi important que notre domaine dans certains cas, voire le siège d’enjeux majeurs pour nos systèmes ?
Les adaptateurs, ces Hérauts du DDD
Le Domain Driven Design regroupe beaucoup de choses. Mais
à l’origine, il part d’une tentative d’Éric Evans de donner plus
d’autonomie aux gens et aux différentes équipes qui travaillaient -parfois avec
beaucoup trop de frictions- dans la même structure où il était lui aussi (dans
le secteur de la finance).
Pour y arriver, Éric eu l’idée de faire émerger sa notion
clé de Contextes Délimités/Bounded Contexts. Un Bounded Context est un espace
linguistique et conceptuel délimité où les mots, les gens et les modèles qui
s’appliquent sont cohérents et servent à résoudre des problèmes spécifiques.
Les problèmes spécifiques de gens du métier qui parlent la même langue et qui
ont les mêmes préoccupations. On peut donc dire que le DDD milite pour des
modèles spécifiques par usages, calqués sur des groupes de personnes.
Tout ceci est bien entendu très spécifique à une
organisation et aux processus métiers mis en place dans celle-ci. Dans la même
société, on pourra trouver par exemple un ou plusieurs Contextes lié au
« marketing », un autre a la « comptabilité », un autre
propre au « service de livraison », etc. Lorsqu’on fait du DDD, on
essaie en général d’identifier et de caractériser ces différents contextes pour
savoir dans lequel on se trouve, si celui-ci apporte vraiment de la valeur
métier ou concerne seulement des fonctions support. On essaie également
d’identifier les autres Contextes avec lesquels on va devoir collaborer. Le
tout étant en général illustré dans un diagramme qui représente les Bounded
Contexts comme des patates et que l’on nomme Context Map (voir l’exemple
ci-dessous).
Tout le volet stratégique du DDD décrit ces rapports
entre contextes (humains et techniques) et nous fournit des patterns pour gérer
telle ou telle situation. Pour connecter et faire communiquer tel ou tel
contexte étant donné les rapports de force en présence.
Voici un exemple naïf de Context Map appliqué à un groupe
hôtelier qui distribue ici ses chambres d’hôtels à travers sa propre plateforme
de distribution (web et apps).
Exemple de Context
Map (domaine de l’hôtellerie)
L’orienté Services FTW
Comme on lit beaucoup de choses sur le sujet, je voudrais
donner quelques ordres d’idées de relations entre ce concept de Bounded Context
et des artefacts logiciels plus tangibles et que vous connaissez surement
mieux, histoire de rendre mes implicites un peu plus explicites. Je vais assez
naturellement m’inscrire dans une architecture orientée service qui est un
style d’architecture que j’affectionne particulièrement et qui est
incroyablement utile quand on s’y prend bien (retirer tout ce savon de votre
tête pour les lignes qui suivent ;-)
Lorsqu’on fait du DDD, la taille d’un service (au sens
SOA moderne) importe peu, c’est son alignement avec le métier (et donc au sein
d’un Bounded Context) qui compte. En conséquence, on trouvera souvent un ou
plusieurs services (web APIs en général) au sein d’un même Bounded Context. Le pattern d’Alistair
étant très pratique pour implémenter un service qui fait vraiment du métier, je
l’utilise souvent pour implémenter ceux-ci. On va donc avoir en général 1
hexagone par processus (que l’on pourra scaler horizontalement). Dans certains
cas on pourra mettre plusieurs hexagones au sein du même processus en les
reliant entre-eux par des adaptateurs In-Proc qui ne passeront pas par le réseau
pour communiquer entre-eux. C’est quelque chose qui est très pratique pour de
nombreuses raisons et que l’on appelle dans le jargon DDD des Modulars
Monolith.
1 BC peut avoir n services/hexagones.
On peut aussi mettre 1 ou n hexagones dans un même process (Modular
Monolith)
Un ADN commun
Faire des modèles autonomes, les séparer et éviter qu’un
modèle spécifique à un contexte ne vienne parasiter un autre modèle d’un autre
contexte, cela ne vous rappelle pas quelque chose ? C’est pour cela que
le pattern d’Alistair convient si bien aux pratiquantes et aux pratiquants du
DDD. Les ports et les adaptateurs nous permettent effectivement d’éviter que le
DTO d’une APi extérieure (souvent en provenance d’un autre contexte donc) ne
vienne parasiter et pénétrer dans notre hexagone qui ne doit gérer que des
problèmes du contexte auquel il appartient.
Si on ne faisait pas ça, ce couplage serait infernal car on aurait des petits bouts d’autres contextes au sein de notre code métier. Je vous laisse imaginer l’impact sur votre code au cas où les gens de l’équipe travaillant pour l’autre contexte en venaient à changer en permanence leurs contrats d’API ou DTOs sans nous demander notre avis. Pour se protéger d’une pareille situation, le DDD nous propose notamment un pattern intéressant : l’Anti-Corruption Layer (ACL).
Si on ne faisait pas ça, ce couplage serait infernal car on aurait des petits bouts d’autres contextes au sein de notre code métier. Je vous laisse imaginer l’impact sur votre code au cas où les gens de l’équipe travaillant pour l’autre contexte en venaient à changer en permanence leurs contrats d’API ou DTOs sans nous demander notre avis. Pour se protéger d’une pareille situation, le DDD nous propose notamment un pattern intéressant : l’Anti-Corruption Layer (ACL).
Anti-Corruption quoi ?
L’Anti-Corruption Layer est un pattern popularisé par le DDD qui permet à un (Bounded) Context de ne pas se retrouver pollué par l’incohérence du modèle d’un autre contexte avec lequel il doit travailler (ou de ses changements trop fréquents). C’est comme un amortisseur entre 2 Contextes. On s’en sert pour réduire le couplage et notre dépendance vis à vis d’un autre Contexte. En procédant ainsi, on isole notre dépendance dans une petite boite bien protégée et a accès limité : l’ACL. On comprend donc alors que tout changement intempestif du Contexte extérieur n’aura que très peu d’impact sur notre code métier qui fait office de consommateur de l’ACL et non plus directement du code instable en provenance du Contexte extérieur.
Pas d’ACL ici, donc
le modèle métier de l’autre Bounded Context s’insère partout dans notre
hexagone
Ici, notre
Adaptateur/ACL empêche les modèles des autres Bounded Context de rentrer dans
notre hexagone
Une anti-corruption layer peut-être implémentée sous
différentes formes : service ou Api intermédiaire, base de données tampon (pour
les vieux systèmes), ou bien juste un
adaptateur (in-proc donc par rapport au code consommateur) d’une architecture
hexagonale.
Quelle Eurythmics choisir pour votre ACL ?
Tout bon article sur le DDD se doit d'avoir une section "Heuristiques" (c'est désormais obligatoire ;-) Voici la mienne :
Si vous avez plusieurs consommateurs en provenance de votre Bounded Context (potentiellement plusieurs APIs ou composant au sein de votre Bounded Context) qui ont besoin d’interagir avec cette partie de l’autre Bounded Context a l’extérieur, vous aurez tout intérêt à faire émerger un service (une API) façade qui ferait office d’ACL partagé.
Si vous n’avez que le code d’un seul hexagone qui a besoin de ce type d’interaction avec ces systèmes tierces en provenance d’autre Bounded Contexts, cette logique d’ACL aura tout intérêt à se trouver dans un des adaptateurs de droite de votre architecture hexagonale. Il est très fréquent de commencer par implémenter un adaptateur dans un process a un endroit, et de le sortir ensuite sous forme d’api autonome si le besoin s’en fait sentir.
Si vous avez plusieurs consommateurs en provenance de votre Bounded Context (potentiellement plusieurs APIs ou composant au sein de votre Bounded Context) qui ont besoin d’interagir avec cette partie de l’autre Bounded Context a l’extérieur, vous aurez tout intérêt à faire émerger un service (une API) façade qui ferait office d’ACL partagé.
Si vous n’avez que le code d’un seul hexagone qui a besoin de ce type d’interaction avec ces systèmes tierces en provenance d’autre Bounded Contexts, cette logique d’ACL aura tout intérêt à se trouver dans un des adaptateurs de droite de votre architecture hexagonale. Il est très fréquent de commencer par implémenter un adaptateur dans un process a un endroit, et de le sortir ensuite sous forme d’api autonome si le besoin s’en fait sentir.
Une
Anti-Corruption Layer peut être hébergée dans un process dédié (API externe)
Et c’est dans ce dernier cas -où un de vos adaptateurs de
droite ferait office d’anti-corruption layer- que vous allez souvent retrouver
des formes d’orchestrations entre plusieurs services extérieurs. Car en général
aucun d’entre eux n’a été fait spécifiquement pour vous et ne réponds
directement aux questions que vous vous posez dans le cadre de vos use cases et
de votre domaine/contexte (celui que vous implémentez dans votre hexagone).
Dans la plupart des cas, les systèmes extérieurs dont vous aurez besoin pour
faire fonctionner votre hexagone seront même des systèmes legacy tous pourris,
ou un mix entre un vieux système et une toute nouvelle API ne fonctionnant que
sur certains cas récents.
Votre adaptateur d’archi hexagonale va donc devoir très souvent appeler ces deux sources pour pouvoir offrir une vision consolidée a votre domaine. Du genre : appeler un premier service d’un autre BC pour récupérer des données incomplètes, traduire ou adapter une partie de sa réponse dans un autre modèle de donnée pour pouvoir ensuite forger une requête intelligible par un autre service extérieur (souvent appartenant au même BC que celui du 1 er appel), récupérer le résultat de ce second appel et adapter le tout pour produire une réponse qui conviendra enfin à votre hexagone qui aura déclenché cette requête via le port de droite que notre adaptateur ACL implémente.
Votre adaptateur d’archi hexagonale va donc devoir très souvent appeler ces deux sources pour pouvoir offrir une vision consolidée a votre domaine. Du genre : appeler un premier service d’un autre BC pour récupérer des données incomplètes, traduire ou adapter une partie de sa réponse dans un autre modèle de donnée pour pouvoir ensuite forger une requête intelligible par un autre service extérieur (souvent appartenant au même BC que celui du 1 er appel), récupérer le résultat de ce second appel et adapter le tout pour produire une réponse qui conviendra enfin à votre hexagone qui aura déclenché cette requête via le port de droite que notre adaptateur ACL implémente.
Mon domaine n’est pas ton domaine
Dans la plupart des cas, toute cette complexité
d’orchestration d’appels aux services ou bases de données tierces et d’adhérence
aux modèles spécifiques de l’autre BC n’a rien à faire dans le code de Domaine
de votre hexagone qui s’occupe lui d’un autre problème spécifique. Celle-ci
sera donc très souvent transparente pour lui, complètement cachée dans votre
adaptateur de droite.
Exemple d’Adaptateur-ACL
qui orchestre différents appels à différents back-ends/APIs d’un autre BC
Dans d’autres cas, vous trouverez naturel que le centre
de votre hexagone aille chercher explicitement deux choses différentes via deux
méthodes ou deux ports de droite différents avant de les assembler pour devenir
quelque chose d’utile à votre domaine.
There is no silver bullet
Ne comptez pas sur moi pour vous donner une règle
universelle et absolue pour répondre à cette question. C’est du Design, et cela
va bien entendu dépendre de votre contexte et de votre domaine. La bonne
question à se poser à chaque fois est :
est-ce légitime et utile pour mon code du domaine de connaitre ces détails ?
Si oui, votre hexagone doit expliciter ces interactions
et ces échanges. Si non (le cas du cache misère vis à vis de l’autre BC par
exemple), cette connaissance doit plutôt rester dans votre adaptateur de
droite.
Une situation paradoxale
A la lumière de ces explications, vous comprendrez
pourquoi je regrette vraiment d’avoir écrit dans mon précédent post qu’un
« bon adaptateur est un adaptateur assez bête ». C’est le cas
quand il y a une relation 1-1 (c.ad. Votre hexagone avec un autre système
extérieur). Mais dans la pratique c’est même assez souvent le contraire (et
encore plus si votre adaptateur est une ACL).
Et c’est là tout le paradoxe. Depuis des années je n’ai
fait que mettre l’accent sur le code du Domaine des hexagones. Avec la pratique
et tous les cas non triviaux rencontrés dans ma carrière, je m’aperçois
que ces adaptateurs sont beaucoup plus que de simples figurants.
A ce titre, ils méritent également beaucoup mieux que
d’être simplement bouchonnés dans nos tests d’acceptation. Mais sur cette
partie des tests, je reviendrai très vite avec un nouvel épisode de cette série
consacrée à l’architecture hexagonale.
No comments:
Post a Comment