Architecture hexagonale : fondamentaux
Par Christian Sperandio
- 5 minutes de lecture - 999 motsOn lit ou on entend par-ci par-là que pour suivre une architecture hexagonale, il faut appliquer tels principes de développement, que cela peut s’appliquer à tous les projets ou au contraire seulement à un nombre restreint, que le projet doit être organisé ainsi ou pas, etc. Cela finit en mantra sans aucune réflexion et sans explication. Dans cet article, il n’y aura ni injonction ni recette. Je pense qu’une fois les raisons et les fondamentaux connus, vous serez à même de suivre l’architecture hexagonale (ou non) de la manière la plus adaptée à votre projet et votre écosystème de développement.
L’architecture hexagonale (également appelée Port/Adapter) a été publiée en 2005 par Alistair Cockburn. Elle est représentée ainsi :
Vous avez l’un des points principaux de cette architecture : vous avez un noyau applicatif et le monde extérieur.
Cette histoire de noyau entraîne certaines discussions. Il arrive vite lors d’échanges que le terme « cœur métier » (ou core domain en VO) apparaisse, faisant bondir des personnes qui déclarent que l’architecture hexagonale n’est pas liée au DDD. Et c’est vrai : nous avons d’un côté une méthodologie de développement et de l’autre une architecture logicielle. Les deux marchent bien ensemble, mais l’une n’implique pas l’autre. Néanmoins, il est bon de rappeler que « cœur métier » n’est pas exclusif au DDD. On ne réalise pas une application pour le plaisir de développer : toute application a un but métier.
Le principe de l’architecture hexagonale est de découpler le cœur de l’application de toute la partie infrastructure, en d’autres termes, les appels depuis ou vers des systèmes externes. C’est là que la dénomination « port /adapter » prend tout son sens. Le cœur communique exclusivement vers l’extérieur via des contrats, correspondant aux ports. Les « adapters » sont les implémentations de ces contrats.
Cela fait très théorique 😄 Prenons l’exemple d’une application dont le but métier est de donner la liste des lieux proposant des figurines de pingouin à proximité d’un collectionneur de figurines.
La partie métier de l’application doit avoir les coordonnées GPS du collectionneur et son contrat avec le repository est d’obtenir une liste des points de ventes à partir de ces coordonnées. En langage Java, cela se traduit par la définition de cette interface :
public interface AukVendorRepository {
List<AukVendor> vendorsCloseTo(Location location);
}
Avec Location définit par un tuple de coordonnées :
// Juste à titre d'exemple
public record AukVendor (
String name,
String address,
Location location
) {}
Le contrat est défini, le cœur de l’application n’a aucun lien sur l’implémentation du repository. Le repository va peut-être réaliser une requête JDBC sur une base, passer par un ORM, appeler un web service externe en http ou en gRPC, taper sur un REDIS, etc. Peu importe l’implémentation, qui est l’adapter, du moment qu’elle respecte le contrat.
Il y a un point très important à garder à l’esprit lors de la définition du contrat. Il est défini par le cœur métier de l’application. Imaginons que l’application considère que les coordonnées sont exprimées en degrés décimaux, alors ce format fait partie du contrat. Si le service utilisé comme repository utilise le format Degrés Minutes Secondes (DMS), la conversion est de la responsabilité de l’adapter. Cette contrainte s’applique également à la réponse de l’adapter : peu importe le format de la réponse du service sous-jacent, l’adapter doit réaliser la transformation nécessaire pour donner le résultat dans le format attendu. Les opérations peuvent être représentées ainsi :
L’avantage de ce découplage entre la partie métier et l’infrastructure de l’application est de gérer différents scénarios :
- Changement de type d’infrastructure (passage d’une base de données interne au SI à un web service externe)
- Changement de fournisseur des données
- Appels à de multiples fournisseurs, puis consolidation
Peu importe le scénario, la partie métier de l’application est stable, mais néanmoins évolutive.
Jusqu’à présent nous nous sommes concentrés sur la communication du cœur de l’application vers des composants extérieurs. Le découplage entre le cœur de l’application doit également se faire depuis les composants externes. C’est sur ce point que le développement peut déraper.
Quittons le monde des figurines et prenons un exemple d’intégration des notifications envoyées par Google Cloud Storage (GCS) dès qu’une action se produit sur un objet. L’objectif est d’utiliser certaines informations de la notification, les enrichir et d’envoyer le résultat comme résultat vers un pub/sub.
Le premier reflexe des développeurs a été de déclarer une interface pour le service :
C’est l’une des incompréhensions de la notion de contrat : ce n’est pas parce que nous définissons une interface dans le code que cela entraîne un contrat et un respect de l’architecture hexagonale. L’argument du type GoogleStorageNotification utilisé dans la méthode est directement le DTO envoyé par la plate-forme, ce qui signifie que notre service n’est plus découplé de l’infrastructure.
Notre contrat est particulier à l’application pour répondre à son objectif. Dans notre cas, nous avons juste besoin du nom du bucket, du file key du fichier et de la date d’émission de la notification. En pratique, notre contrat est défini par ce code :
// Représentation d'une notification dons notre application
public record StorageNotification (
String bucketId,
String fileKey,
Date timestamp) {}
// Utilisation dans l'interface de cette représentation
public interface EventPublisher EnterpriseEvent publishEventByActionNotification(StorageNotification notification);
}
Lors de la réception d’une donnée de l’infrastructure (GCS dans notre exemple), le port n’est pas l’interface de notre service et l’adapter n’est pas son implémentation. Le port est l’interface http handler, souvent pris en charge par le framework ou la librairie utilisée, et l’adapter est l’implémentation du contrôleur.
Dans un contexte SpringBoot, cela nous donne :
J’espère vous avoir aidé à mieux appréhender l’architecture hexagonale. Le but n’est pas de faire le plus d’interfaces possibles ou de suivre aveuglément une certaine organisation de vos packages ou modules. L’objectif est de bien définir vos contrats entre la partie métier de votre application et la partie infrastructure, afin de s’assurer d’un découplage complet entre les deux.
Note: Cet article a également été publié sur le blog d’Arolla.