Avez-vous déjà rencontré des problèmes lors de la mise à niveau la stack de votre logiciel ? Êtes-vous en mesure de distinguer vos tests fonctionnels de vos tests d’intégration ? Migrer votre legacy signifie tout réécrire de zéro ? Découvrez comment l’Architecture Hexagonale, un modèle de clean architecture, également connu sous le nom de ports et adapters, peut vous aider!
Logique métier à la mer !
Dans une expérience précédente, mon équipe a dû porter une ancienne application sur une toute nouvelle stack. Le logiciel passait d’une application EAR/SQL à un JAR autonome utilisant une base de donnée NoSQL. En l’étudiant, nous nous sommes vite rendu compte que nous devions refaire toute l’infrastructure. En fait, la seule chose qui n’avait pas à changer était la logique métier. Donc, il est logique de la réutiliser, non?
Après une étude plus approfondie, le module maven nommé modèle était composés de POJOs avec seulement des getters et setters, totalement anémique… Bien qu’il existe également un module service, la logique métier était répartie sur toutes les couches. Elle a été noyée dans beaucoup de code technique tels que la création de DAO, la serialization, etc. Et il n’y avait aucun moyen d’extraire cette logique métier ! D’autant plus que certaines parties de celle-ci s’appuyaient sur le comportement technique de l’ancien framework que nous devions supprimer. Et pourquoi ? Parce qu’il n’y avait pas de séparation nette entre la logique métier et le code technique.
Ségrégation des responsabilités: isoler la logique métier
L’architecture hexagonale créée par Alistair Cockburn assure la réutilisation de la logique métier en la rendant agnostique de la technique. Ainsi, la modification de la stack n’aura aucun impact sur le code du domaine.
Un concept clé de cette architecture est de mettre toute la logique métier en un seul endroit nommé le domaine. Si on met à jour le schéma précédent :
Contrainte importante : le domaine ne doit dépendre que de lui-même ; c’est la seule façon de s’assurer que la logique métier est découplée des couches techniques. Comment pouvons-nous y parvenir sur le schéma précédent ? Le domaine dépend clairement de la couche de persistance ! Eh bien, en utilisant un modèle que vous connaissez peut-être: l’inversion de contrôle. En appelant le domaine l’hexagone, l’architecture ressemblera à ceci:
L’inversion du contrôle était assez magique, voyons plus tard comment elle fonctionne. Maintenant, vous avez un aperçu de ce qu’est l’architecture hexagonale:
- Seulement deux mondes, à l’intérieur et à l’extérieur de l’hexagone. A l’intérieur: toute la logique métier, à l’extérieur: l’infrastructure – ce qui signifie tout votre code technique.
- Les dépendances vont toujours de l’extérieur vers l’intérieur de l’hexagone. Ceci assure l’isolement du domaine métier de la partie technique.
- Un corollaire est que l’hexagone ne dépend que de lui-même. Et pas seulement en ce qui concerne vos propres couches: Il ne doit pas dépendre d’un cadre technique. Cela inclut les annotations externes venant de Jackson ou JPA.
Aucun framework dans le domaine
Permettez-moi de vous expliquer un peu plus ce dernier point qui est vraiment important. Dans une autre expérience, mon équipe a dû porter une application du framework « classique » Spring vers Spring Boot. Le principal (et douloureux) problème que nous avions était de tirer trop parti des tests d’intégration de Spring pour valider nos fonctionnalités. En outre, le code de production de ces fonctionnalités était également trop couplé avec le Spring.
Au premier essai de notre migration Spring Boot, tous les tests fonctionnels échouaient. Nous n’avons pas été en mesure de déterminer si la logique métier était cassée quelque part ou si la raison était purement technique. Mais nous avons finalement compris qu’il s’agissait d’un problème d’intégration au niveau des tests. Nous avons donc fixé tous les tests un par un en croisant les doigts… En espérant que le domaine était toujours correct.
Aucun framework dans l’hexagone permet de réutiliser le domaine métier indépendamment du changement de la stack technique. Il augmentera également la testabilité de votre domaine puisque vous ne le mélangez plus avec des problèmes d’intégration. Et à la fin, vous ferez de vrais tests fonctionnels grâce à cette contrainte de l’architecture hexagonale. De cette façon, les tests fonctionnels interagiront directement avec l’hexagone et seulement avec lui.
NOTE: Dans Maven, vous pouvez assurer cette contrainte à l’aide de l’enforcer plugin.
L’inversion de contrôle de l’architecture hexagonale
Vous vous rappelez de l’inversion du contrôle ? Pour assurer l’isolement de l’hexagone, les dépendances des couches en aval ont été inversées. L’astuce est en fait assez simple que vous pouvez le voir:
L’extérieur de l’hexagone (l’infrastructure) est divisé en deux parties virtuelles, le côté gauche et le côté droit. Sur la gauche, vous avez tout ce qui interrogera le domaine (le contrôleur, la couche REST, etc.). Et à droite, tout ce qui fournira des informations/services au domaine (couche de persistance, services tiers, etc.).
Protéger le domaine avec un anti-corruption layer
Pour permettre à l’extérieur d’interagir avec le domaine, l’hexagone fournit des interfaces métier divisées en deux catégories :
- L’API rassemble toutes les interfaces pour tout ce qui a besoin d’interroger le domaine. Ces interfaces sont implémentées par l’hexagone.
- La SPI (Service Provider Interface) rassemble toutes les interfaces requises par le domaine pour récupérer des informations auprès de tiers. Ces interfaces sont définies dans l’hexagone et implémentée par le côté droit de l’infrastructure. Nous verrons que dans certaines circonstances, l’hexagone peut également implémenter la SPI.
Il y a deux faits importants ici :
- L’API et la SPI font partie de l’hexagone.
- L’API et la SPI ne manipulent que les objets de domaine de l’hexagone. Ceci assure en effet l’isolement de celui-ci.
Dans une architecture en couches, les objets métier ou les services créent généralement les DAO. Dans l’architecture hexagonale, le domaine ne gère que les objets du domaine. Par conséquent, la persistance est en charge de traduire les objets du domaine en tous les « DAOs » à persister. C’est ce que nous appelons une adaptation.
La force de modularité de l’architecture hexagonale
Comme mentionné ci-dessus, l’architecture ports et adapters est un autre nom de l’architecture hexagonale. Il vient de la puissance de la modularité de cette architecture. Parce que tout est découplé, vous pouvez avoir une couche REST et JMS en face de votre domaine en même temps sans avoir d’impact sur celui-ci.
Du côté SPI, vous pouvez passer d’une implémentation MongoDB à Cassandra si nécessaire. Comme la SPI ne changera pas parce que vous modifiez le module de persistance, le reste de votre logiciel ne sera pas affecté. L’API et le SPI sont les ports et les modules d’infrastructure qui les utilisent ou les implémentent sont les adaptateurs.
Comment implémenter l’architecture hexagonale ?
Une règle de plus ici: toujours commencer par l’intérieur de l’hexagone. Cela vous apportera beaucoup d’avantages:
- Un focus sur la fonctionnalité au lieu des détails techniques. Parce que seule la fonctionnalité apporte de la valeur à votre utilisateur. Un développeur travaillant sur un autre domaine metier est en mesure de mettre en place un contrôleur Spring. Mais la méthode d’amortissement dégressif à taux double ressemblera à du Wookiee pour lui à moins qu’il ne travaille pour une société de comptabilité.
- Retarder des choix sur l’implémentation technique. Parfois, il est vraiment difficile de savoir de quelle solution technique vous avez réellement besoin dès le départ. Par conséquent, retarder ce choix vous aide à vous concentrer sur ce qui apporte directement de la valeurs – la fonctionnalité. En outre, après l’implémentation de la logique métier, certains nouveaux éléments peuvent vous aider à faire le meilleur choix en ce qui concerne votre infrastructure. Vous pouvez découvrir que le domaine est plus relationnel que prévu, et par conséquent SQL serait un bon choix pour votre base de données.
- Un corollaire est qu’il assure que l’hexagone est un stand-alone. Puisque vous ne devriez jamais écrire du code sans tests, cela signifie aussi que l’Hexagone est auto-testé. En outre, nous avons obtenu ici de vrais tests fonctionnels se concentrant sur la logique métier seulement.
Une implémentation pilotée par les tests
Avec l’architecture hexagonale, vous mettez vos tests fonctionnels dans votre domaine. Ces tests appelleront directement l’API du domaine tout en évitant toute perturbation de la partie technique. D’une certaine manière, vous créez un adapter simulant le contrôleur pour tester les fonctionnalités du domaine.
En commençant par les tests fonctionnels du domaine
Mon conseil est d’écrire votre scénario fonctionnel d’abord en utilisant le Behavior-Driven Development pour décrire votre fonctionnalité.
La première étape de la « double boucle » produira votre test fonctionnel à l’aide d’ATDD. Et écrivez ensuite l’interface de votre API qui sera le point d’entrée de votre fonctionnalité. Ensuite, implémentez vos tests avec du TDD et enfin implémentez votre logique métier. Lors de l’écriture, vous devrez peut-être récupérer certaines données de la base de données par exemple, alors créez une SPI. Puisque le côté droit n’est pas encore implémenté, créez une implémentation stubbée de cette SPI à l’intérieur de votre hexagone. Cela peut être réalisé à l’aide d’une base de données en mémoire implémentée avec une Map.
Vous pouvez choisir de conserver les stubs dans les test de votre application. Mais vous pouvez aussi bien les livrer en production temporairement si nécessaire. Par exemple, une fois que nous avons fait la première fonctionnalité dans l’hexagone, nous avons stubbé un service externe et la base de données. Parce que notre client avait besoin que nous lui fournissions un contrat d’interface, nous avons ensuite exposé le domaine par l’intermédiaire d’un contrôleur REST. Nous avons donc livré une première version avec des données stubbées sur le côté droit de l’infrastructure. De cette façon, le client a pu voir la structure de nos données et le comportement attendu de la fonctionnalité. Et cela était beaucoup plus fiable que de créer à la main quelques échantillons de JSON de nos requêtes et réponses parce que le domaine implémente les contraintes métier réelles.
Finir par les adapters
L’étape suivante est généralement l’ouverture du côté gauche en premier. De cette façon, vous pouvez mettre en place des tests d’intégration sur la fonctionnalité. À ce moment là, vous pouvez fournir une documentation vivante et assurer un contrat d’interface avec vos clients.
Enfin, ouvrez sur la droite en implémentant le SPI de votre fonctionnalité en profitant des tests d’intégration. Je recommande fortement que vos tests soient autonomes pour éviter toute instabilité pendant le temps de build. Vous devriez toujours vous mocker de vos dépendances en utilisant quelque chose comme Wiremock pour les services externes ou Fongo pour simuler un MongoDB.
Puis faites de la même manière pour vos autres fonctionnalités.
Pour plus d’informations, une stratégie de test en architecture hexagonale est disponible sur GitLab qui est également décrite par le talk ci-dessus 🇫🇷.
L’architecture hexagonale en bref
Maintenant nous avons vu ce qu’est l’architecture hexagonale! Il y a un réel avantage à découpler la logique métier du code technique. Cela garantit que votre domaine métier est durable et robuste face à l’évolution continuelle des stacks techniques.
L’architecture hexagonale vous offre un réel moyen d’y parvenir en :
- Mettant toute la logique métier en un seul endroit.
- Le domaine est isolé et agnostique vis-à-vis de la partie technique car il ne dépend que de lui-même. C’est pourquoi les dépendances vont toujours de l’extérieur à l’intérieur de l’hexagone.
- L’hexagone est un module autonome. Ainsi, cela augmente la testabilité de votre domaine en écrivant de vrais tests fonctionnels qui ne doivent pas être impactés par des problèmes techniques.
- Cette architecture offre une modularité puissante. Elle vous aide à écrire autant d’adapters que nécessaire avec un faible impact sur le reste du logiciel. Et puisque le domaine est agnostique de la technique, la stack peut être modifiée sans aucun impact sur le domaine.
- En commençant toujours par le domaine, vous vous assurez d’apporter de la valeur à votre client en vous concentrant sur le développement des fonctionnalités. De cette façon, vous pouvez retarder les choix sur les solutions technique pour faire le meilleur choix au bon moment.
Quelques commentaires
L’architecture hexagonale ne convient pas à toutes les situations. Comme le Domain-Driven Design, cela est vraiment applicable si vous avez un vrai domaine métier. Pour une application qui transforme une donnée en un autre format, cela peut être contre-productif et inapproprié.
Pour finir, soyez toujours pragmatique lorsque vous adoptez une nouvelle technologie. Comme indiqué précédemment, l’hexagone ne doit pas dépendre d’un framework technique, mais exceptionnellement vous pouvez. Par exemple, dans notre cas, l’hexagone avait trois exceptions : Apache Commons Lang3 (StringUtils), SLF4J et le JSR305 de Findbugs. Parce que nous ne voulions pas réinventer la roue et puisque ces frameworks avaient des impacts très faibles sur le domaine. Un bon effet secondaire de l’architecture hexagonale, est que vous n’arrêtez pas de vous challenger avant d’intégrer un nouveau framework. En utilisant cette architecture, nous avons réduit le nombre de dépendances de cinquante à seulement trois ou quatre pour le domaine. Et c’est très bien d’un point de vue sécurité.
Pour aller plus loin
Vous voulez en savoir plus ? Jetez un œil sur Domain-Driven Design and Hexagonal Architecture tips&tricks series
Vous pouvez également trouver ici un bon article sur l’architecture hexagonale: https://softwarecampament.wordpress.com/portsadapters
Et si vous voulez voir du code, une application hexagonale faite en Kotlin & Spring Boot est disponible sur GitLab.
English version: Hexagonal Architecture: the practical guide for a clean architecture