You are currently viewing Architecture Hexagonale : Comment créer des applications polyvalentes

Architecture Hexagonale : Comment créer des applications polyvalentes

La définition de l’architecture hexagonale trouve son origine en 2005. A cette époque, Alistair Cockburn réalisa que l’interaction entre l’interface utilisateur et la base de données avec une application n’était pas très différente, car ils sont tous deux des acteurs externes interchangeables avec des composants similaires qui interagiraient de manière équivalente avec une application. En adoptant cette perspective, on pourrait se concentrer sur le maintien de l’agnosticisme de l’application vis-à-vis de ces acteurs « externes », afin qu’ils interagissent via des Ports et des Adapters, évitant ainsi l’enchevêtrement et la fuite de logique entre la logique métier et les composants externes.

Cet article tentera de vous guider à travers les concepts principaux de l’Architecture Hexagonale, ses avantages et inconvénients, et de décrire de manière simplifiée comment vous pourrez profiter de ce modèle dans vos projets.

Qu’est-ce que l’architecture Hexagonale ?

diagramme succinct de l'architecture hexagonale

L’Architecture Hexagonale, également connue sous le nom de Ports et Adapters, suit un modèle architectural dans lequel les entrées des utilisateurs ou des systèmes externes arrivent dans l’Application via un Port grâce à un Adaptateur, et où les sorties de l’Application sont envoyées via un Port à un Adaptateur. Cela crée une couche d’abstraction qui protège le coeur de l’application et l’isole des outils et technologies externes – qui peuvent être considérés comme étant sans importance pour l’application.

Pourquoi un hexagone?

L’idée d’Alistair d’utiliser un hexagone est une représentation visuelle des multiples combinaisons de Ports/Adaptateurs qu’une application pourrait avoir. Elle sert aussi à représenter comment le côté gauche de l’application, ou « côté Conducteur – Driving Side », a des interactions et des implémentations différentes par rapport au côté droit, ou « côté Conduit – Driven Side ».

Il y a trois principes sur lesquels s’appuie cette architecture :

  1. Séparer clairement l’interface de programmation de l’application (API), le domaine et l’interface du fournisseur de services (SPI).
  2. Gérer les dépendances de l’extérieur vers l’intérieur, en direction du domaine.
  3. Isoler le domaine des préoccupations extérieures en utilisant des ports et des adaptateurs, via des interfaces.

Côté Conducteur vs Côté Conduit – Driving Side vs Driven Side

Les acteurs conducteurs (ou principaux) sont ceux qui initient l’interaction, et sont toujours représentés sur le côté gauche. Par exemple, un adaptateur conducteur pourrait être un contrôleur qui prend l’entrée (de l’utilisateur) et la transmet à l’Application via un Port.

Les acteurs conduits (ou secondaires) sont ceux qui sont « déclenchés dans leur comportement » par l’Application. Par exemple, un adaptateur de base de données est appelé par l’Application afin de récupérer un certain ensemble de données depuis la persistance.

En ce qui concerne la mise en œuvre, il y a quelques détails importants à ne pas manquer :

  • Les Ports seront (la plupart du temps, selon le langage que vous choisissez) représentés comme des interfaces dans le code.
  • Les adaptateurs conducteurs – Driving Adapters utiliseront un Port et un service d’application implémentera l’interface définie par le Port, dans ce cas, à la fois l’interface et l’implémentation du Port sont à l’intérieur de l’hexagone.
  • Les adaptateurs conduits – Driven Adapters implémenteront le Port et un service d’application l’utilisera, dans ce cas, le Port est à l’intérieur de l’hexagone, mais l’implémentation est dans l’adaptateur, donc à l’extérieur de l’hexagone.

3 modules & 2 couches

En se basant sur le premier principe, nous pouvons séparer explicitement trois modules principaux :

API / Exposition

Ce module est l’interface client, situé du côté gauche. Il fait partie de l’infrastructure et peut être considéré comme une « exposition ». Il sert aux applications du monde extérieur d’interagir avec cette application en exposant des API (HTTP, REST, Web-socket, etc.). L’application externe peut être une application à page unique (SPA), un programme ou un middleware. Ce côté est le côté conducteur car il pilote le domaine qui appliquera ensuite les règles métier. Il peut également être considéré comme faisant partie de l’infrastructure.

Domaine

Ce module est celui que nous voulons isoler du reste, car il héberge toute la logique métier. En référence à l’approche Domain-Drive-Design, nous nous appuyons sur la langue d’ubiquité, donc nous utiliserions le vocabulaire spécifique au domaine.

SPI / Infrastructure

Presque toutes les applications ont besoin d’interagir avec des outils et/ou d’autres systèmes, spécifiquement connus sous le nom d’interface de fournisseur de services (SPI). Il s’agit du côté passif et constitue la deuxième partie de l’infrastructure. Et il est généralement désigné comme faisant partie de l’infrastructure. Dans ce module, nous voulons donner à l’application les moyens de récupérer ou d’envoyer des données vers le monde extérieur. La nature de l’interaction peut être de n’importe quel type, et les types courants sont HTTP, système de fichiers, SMTP, SQL.

Sur le diagramme ci-dessus, nous pouvons voir trois modules (exposition, domaine et infrastructure). Lors de la structuration du code d’un projet, nous les séparons généralement clairement.

2 couches

L’architecture hexagonale est un modèle de conception d’applications logicielles qui est basé sur deux couches, la couche domaine (interne) et la couche infrastructure (externe), délimitées par les bordures hexagonales de l’extérieur.
Le domaine ne doit pas dépendre de l’infrastructure. Les côtés internes de l’hexagone représentent simplement des ports. Et les côtés externes sont les adaptateurs.
L’hexagone trouve son équilibre avec certains services externes à gauche (consommateurs) et d’autres à droite (fournisseurs de services).
L’exécution se déroule de gauche à droite.

Quels sont les principaux composants de l’architecture Hexagonale?

Avant de parler de tous les composants, il est important de préciser que cette architecture repose sur le principe d’inversion de dépendance – Dependency Inversion Principle.

Le Principe d’Inversion de Dépendance est l’un des 5 principes énoncés par Bob Martin dans son article OO Design Quality Metrics et plus tard dans son livre Agile Software Development Principles, Patterns and Practices, où il le définit comme suit :

  • Les modules de haut niveau ne devraient pas dépendre de modules de bas niveau. Les deux devraient dépendre d’abstractions.
  • Les abstractions ne devraient pas dépendre des détails. Les détails devraient dépendre des abstractions.

Comme mentionné précédemment, les côtés gauche et droit de l’hexagone contiennent 2 types d’acteurs différents: les Conducteurs et les Conduits et où ils existent à la fois des Ports et des Adaptateurs.

Du côté Conducteur, l’Adaptateur dépend du Port implémenté par le Service d’Application, donc l’Adaptateur ne sait pas qui réagira à ses invocations, il sait seulement quels sont les méthodes garanties d’être disponibles, donc il dépend d’une abstraction.

Du côté Conduit, le Service d’Application est celui qui dépend du Port, et l’Adaptateur est celui qui implémente l’Interface du Port, inversant ainsi la dépendance car l’adaptateur « de bas niveau » (c’est-à-dire le dépôt de base de données).

Ports

Nous pouvons considérer un Port comme un point d’entrée agnostique en termes de technologie, il détermine l’interface qui servira aux acteurs externes à communiquer avec l’application, peu importe qui ou quoi implémentera ladite interface. Tout comme un port USB sert à communiquer à plusieurs types de périphériques avec un ordinateur tant qu’ils disposent d’un adaptateur USB. Les Ports servent également à l’application pour communiquer avec des systèmes ou services externes, tels que des bases de données, des brokers de messages, d’autres applications, etc.

Adaptateurs – Adapters

Un Adaptateur initiera l’interaction avec l’Application via un Port, en utilisant une technologie spécifique. Par exemple, un contrôleur REST représenterait un adaptateur qui permet à un client de communiquer avec l’Application. Il peut y avoir autant d’Adaptateurs pour un seul Port que nécessaire, sans que cela représente un risque pour les Ports ou l’Application elle-même.

Application

L’Application est le cœur du système, elle contient les Services d’Application qui orchestrent la fonctionnalité ou les cas d’utilisation. Elle est représentée par un hexagone qui reçoit des commandes ou des requêtes des Ports, et envoie des demandes à d’autres acteurs externes, comme les bases de données, via les Ports également.

Associée à la conception pilotée par le domaine, l’Application, ou Hexagone, contient à la fois les couches Application et Domaine, laissant l’interface utilisateur et les couches d’infrastructure à l’extérieur.

Ce composant est responsable de la préparation de l’environnement pour vos modèles, afin qu’ils puissent exécuter leurs règles métier.
Il implémente les règles d’application (parfois appelées cas d’utilisation). Les règles d’application sont différentes des règles métier. Les premières sont des règles qui s’éxecutent pour implémenter un cas d’utilisation de votre application. Les dernières sont des règles qui appartiennent à l’entreprise elle-même.

Exemple de règle d’application ou cas d’utilisation:

Charger un compte à partir d’un référentiel, appeler la méthode Account.Withdraw() en passant un montant, et envoyer le nouveau solde par e-mail au propriétaire du compte.

public void accountWithdraw(String accountID, double amount) {
    // Il n'y a pas de gestion d'erreur pour simplifier le code
    Account account = accountRepository.getAccount(accountID);

    account.withdraw(amount);

    accountRepository.saveAccount(account);
    emailService.sendBalanceEmail(account);
}

Services

Un Service d’Application est un morceau de code qui implémente un cas d’utilisation.
Il peut recevoir des objets qui implémentent certaines interfaces connues (injection de dépendance), et il peut importer des entités de la couche de domaine.

Domaine – Domain

Le composant de domaine est la couche la plus interne de l’architecture. Les modèles de domaine et les services seront à l’intérieur et contiendront toutes les règles métier du logiciel. Il doit être purement logique, ne réalisant aucune opération d’entrée/sortie.
Comme il est purement logique, il devrait être assez facile de la tester, car vous n’avez pas à vous soucier de la simulation des opérations d’entrée/sortie.

Cette couche ne doit pas non plus connaître quoi que de déclaré dans les couches supérieures. Elle a par ailleurs la charge des règles métiers.

Exemple de règle métier :

Lorsqu’un retrait est demandé sur un compte, une taxe de 0,1 % doit être facturée.

public void withdraw(double amount) {
    double deductions = amount + Taxes.WithdrawTaxes;
    this.amount -= deductions;
}

Pour faire le distinguo entre règle métier et règle d’application, il suffit de poser la question :
Est-ce-que cette règle serait appliquée si les ordinateurs n’existaient pas?

Si la réponse est oui, il s’agit d’une règle métier sinon c’est une règle d’application

Les modèles de domaine – Domain Models

Les modèles de domaine sont au cœur du domaine. Ils représentent les modèles commerciaux, contenant les règles métier de leur domaine.

Un exemple d’un modèle de compte bancaire

import java.time.LocalDateTime;
import java.util.List;

public class Account {
    private double amount;
    private User owner;
    private List<Transaction> transactions;
    private LocalDateTime createdAt;

    public double getAmount() {
        return amount;
    }

    public void setAmount(double amount) {
        this.amount = amount;
    }

    public User getOwner() {
        return owner;
    }

    public void setOwner(User owner) {
        this.owner = owner;
    }

    public List<Transaction> getTransactions() {
        return transactions;
    }

    public void setTransactions(List<Transaction> transactions) {
        this.transactions = transactions;
    }

    public LocalDateTime getCreatedAt() {
        return createdAt;
    }

    public void setCreatedAt(LocalDateTime createdAt) {
        this.createdAt = createdAt;
    }

    public void withdraw(double amount) {
        // implementation
    }

    public void deposit(double amount) {
        // implementation
    }

    public void getStatement(double amount) {
        // implementation
    }
}

Les services de domaine – Domain Services

Il y a des cas où il est difficile d’adapter un comportement à un modèle de domaine unique. Imaginez que vous modélisiez un système bancaire, où vous avez le modèle de domaine Compte. Ensuite, vous devez implémenter la fonctionnalité de transfert, qui implique deux comptes. Le modèle de Compte ne doit pas obligatoire implémenté ce comportement, donc vous pouvez choisir de l’implémenter dans un service de domaine.

Un service de domaine contient un comportement qui n’est pas attaché à un modèle de domaine spécifique. Il pourrait être essentiellement une fonction, au lieu d’une méthode.

Notez qu’idéalement, vous devriez toujours essayer de mettre en œuvre des comportements dans des modèles de domaine pour éviter de tomber dans le piège du modèle de domaine anémique.

Les objets de valeur – Value Objects

Un objet de valeur est un objet qui n’a pas d’identité et qui est immuable. Ces objets n’ont pas de comportement, étant simplement des sacs de données utilisés aux côtés de vos modèles. Exemples :

  • Un objet Adresse, qui représente un emplacement physique.
  • Un objet Point, qui représente une coordonnée.

Vos modèles de domaine peuvent avoir des objets de valeur dans leurs attributs, mais l’inverse n’est pas autorisé.

public record Point(int x, int y) {}

Un exemple simple d’objet de valeur

Pourquoi devrais-je utiliser l’architecture Hexagonale ou Ports et Adapters ?

Il y a de nombreux avantages à utiliser l’architecture Hexagonale, l’un d’entre eux est de pouvoir isoler complètement la logique de votre application et la logique de votre domaine de manière entièrement testable. Comme cela ne dépend pas de facteurs externes, le tester devient naturel et simuler ses dépendances est facile.

Elle vous offre également la possibilité de concevoir toutes les interfaces de votre système « par objectif » plutôt que par technologie. Elle vous empêche ainsi de vous verrouiller et facilite l’évolution de la pile technologique de votre application avec le temps. Si vous avez besoin de changer la couche de persistance, allez-y. Il devient possible d’appeler votre application par des bots Slack au lieu d’humains! Tout ce que vous avez à faire est de mettre en œuvre de nouveaux Adaptateurs et vous êtes prêt à partir.

L’architecture des Ports et Adapters fonctionne également très bien avec la conception orientée domaine (Domain-Driven Design), son principal avantage est qu’elle empêche la logique de domaine de fuir hors du cœur de votre application. Soyez simplement vigilant à l’éventuelle fuite entre les couches Application et Domaine.

Organisation du code & exemple

Il serait agréable de pouvoir pointer du doigt une zone dans un diagramme d’architecture et savoir instantanément quelle partie du code est responsable.

Packaging

Dans une architecture hexagonale, nos principaux éléments sont les entités, les cas d’utilisation, les ports conducteurs et conduits et les adaptateurs conducteurs et conduits.

hexagonal
|
| - account
| - adapter
| | - driving
| | | - api
| | | | - AccountApi
| | | | - AccountResource
| | | - web
| | | - AccountController
| | - driven
| | | - persistence
| | - AccountRepository
| - application
| | - port
| | | - driving
| | | | - SendMoneyUseCase
| | | - driven
| | | - LoadAccountPort
| | | - UpdateAccountStatePort
| | - service
| | - AccountServiceImpl
| - domain
| - model
| - Account
| - Transaction

Au plus haut niveau, nous avons un package nommé « Account », indiquant que c’est le module qui implémente les cas d’utilisation autour d’un compte.

Au niveau suivant, nous avons :

  • un package de domaine contenant nos modèles de domaine ;
  • un package d’application contenant le service AccountService qui implémente le port d’entrée, SendMoneyUseCase, et utilise les interfaces des ports de sortie LoadAccountPort et UpdateAccountStatePort qui sont implémentées par l’adaptateur AccountRepository ;
  • un package d’adaptateur contenant les adaptateurs entrants qui appellent les ports d’entrée et les adaptateurs sortants qui fournissent des implémentations pour les ports de sortie.

Imaginons que nous parlions à un collègue de la modification d’un client vers une API tierce que nous consommons, nous pouvons alors pointer l’adaptateur sortant correspondant sur le diagramme pour mieux nous comprendre, puis nous asseoir devant notre IDE et commencer à travailler sur le client immédiatement.

Dans cette structure de code, le package correspond exactement au diagramme d’architecture.

Injection de dépendance

Nous ne voulons pas instancier manuellement les ports au sein de la couche d’application car nous ne voulons pas introduire une dépendance à un adaptateur.

C’est là que la injection de dépendance entre en jeu. Nous introduisons un composant neutre qui a une dépendance sur toutes les couches. Ce composant est responsable de l’instanciation de la plupart des classes qui composent notre architecture.

Le composant de l’injection de dépendance neutre crée des instances de classes AccountController, AccountService et AccountRepository . Comme AccountController nécessite une interface SendMoneyUseCase, l’injection de dépendance lui fournira une instance de la classe AccountService lors de la construction. Le contrôleur ne sait pas qu’il a réellement une instance de AccountService car il a seulement besoin de connaître l’interface.

Répartition en terme de projet de développement

Le projet pourrait se diviser en deux sous-projets:

  • le projet API : ce projet a pour vocation de donner accès à l’ensemble des contrats associés au domaine cible
hexagonal
|
| - account
| - adapter
| | - driving
| | | - api
| | | | - AccountApi
| - application
| | - port
| | | - driving
| | | | - SendMoneyUseCase
| | | - driven
| | | - LoadAccountPort
| | | - UpdateAccountStatePort
| - domain
| - model
| - Account
| - Transaction
  • le projet CORE: ce projet implémente l’ensemble des adapters, des services et fournit le bootstrap de démarrage.
hexagonal
|
| - account
| - adapter
| | - driving
| | | - api
| | | | - AccountResource
| | | - web
| | | - AccountController
| | - driven
| | | - persistence
| | - AccountRepository
| - application
| | - service
| | - AccountServiceImpl

Exemple

Je vous propose un exemple d’implémentation de l’architecture Hexagonale en utilisant Quarkus.
Vous pouvez retrouver le code dans ce repository Github.

Architectures similaires

Il existe d’autres architectures similaires qui utilisent certains des mêmes principes :

Laisser un commentaire

un × 2 =