Diagramme d'architecture hexagonale : ports et adaptateurs, avec un exemple

L'architecture hexagonale par un diagramme : le cœur, les ports entrants et sortants, les adaptateurs primaires et secondaires, la règle de dépendance et un exemple.

· Fondateur et développeur

Publié le

Diagramme d'architecture hexagonale : ports et adaptateurs, avec un exemple

La plupart des bases de code commencent avec le framework au centre : les contrôleurs appellent les services, les services appellent l'ORM, et les règles métier fuient dans les trois. Tester une règle de prix demande alors un serveur web et une base de données, et changer de prestataire de paiement oblige à toucher au code qui décide du prix d'une commande.

L'architecture hexagonale, aussi appelée ports et adaptateurs, inverse cela. Cet article explique l'idée et le vocabulaire (ports, adaptateurs primaires et secondaires, règle de dépendance), montre un diagramme pour un service de commandes et indique quand le pattern vaut ses fichiers supplémentaires.

L'idée en une phrase

Placez la logique métier dans un cœur qui ne sait rien de HTTP, des bases de données ou des fournisseurs, et connectez tout le reste à ce cœur via des interfaces qui lui appartiennent.

Alistair Cockburn a décrit le pattern en 2005 dans son article original sur l'architecture hexagonale. Son intention déclarée : permettre à une application d'être pilotée indifféremment par des utilisateurs, des programmes, des tests automatisés ou des scripts batch, et d'être développée et testée indépendamment de ses futures bases de données et de ses périphériques. La forme hexagonale n'a pas d'autre sens que de laisser la place pour dessiner plusieurs ports sur ses côtés.

Le vocabulaire

Terme Ce que c'est Exemple du service de commandes
Cœur Les cas d'usage et le modèle de domaine PlaceOrder, Order, Money, règles de prix
Port entrant (primaire) Une interface que le cœur offre à l'extérieur PlaceOrder.execute(command)
Port sortant (secondaire) Une interface dont le cœur a besoin à l'extérieur OrderRepository, PaymentGateway
Adaptateur primaire Transforme une entrée externe en appels de port Contrôleur REST, CLI, consommateur de messages, test
Adaptateur secondaire Implémente un port sortant avec une vraie technologie Repository Postgres, client d'API de paiement

La règle de dépendance relie le tout : les dépendances du code source pointent vers l'intérieur. Les adaptateurs dépendent des ports ; les ports et le cœur ne dépendent de rien hors de l'hexagone. À l'exécution, le cœur appelle la passerelle de paiement, mais dans le code, c'est l'adaptateur de paiement qui dépend de l'interface du cœur, et non l'inverse.

Un exemple complet

Passer une commande demande d'enregistrer la commande et de débiter le client. Dans le cœur :

// ports, owned by the core
interface OrderRepository { save(order: Order): Promise<void> }
interface PaymentGateway { authorize(amount: Money, token: string): Promise<AuthResult> }

// use case
class PlaceOrder {
  constructor(private orders: OrderRepository, private payments: PaymentGateway) {}
  async execute(cmd: PlaceOrderCommand): Promise<OrderId> {
    const order = Order.create(cmd.items, cmd.customerId)   // domain rules
    const auth = await this.payments.authorize(order.total(), cmd.paymentToken)
    if (!auth.ok) throw new PaymentDeclined(auth.reason)
    await this.orders.save(order.markPaid(auth.id))
    return order.id
  }
}

Hors du cœur, PostgresOrderRepository implémente OrderRepository en SQL, et PaymentApiGateway implémente PaymentGateway avec des appels HTTP, des retries et la traduction des erreurs. Un contrôleur REST analyse la requête, construit un PlaceOrderCommand et appelle le cas d'usage. Un test fait la même chose avec des adaptateurs en mémoire, sans serveur web ni base de données.

La traduire en dossiers

La structure se maintient mieux quand les dossiers la disent clairement :

src/
  core/
    domain/        Order, Money, pricing rules
    usecases/      PlaceOrder, CancelOrder
    ports/         OrderRepository, PaymentGateway
  adapters/
    http/          REST controllers (driving)
    postgres/      repository implementations (driven)
    payments/      payment API client (driven)
  main.ts          wiring: builds adapters and passes them to use cases

Ajoutez une règle de lint ou un test d'architecture qui fait échouer le build dès que quelque chose sous core/ importe depuis adapters/ ou depuis un paquet de framework. La règle est facile à enfreindre par accident et peu coûteuse à faire respecter automatiquement.

Erreurs fréquentes

  • Des ports calqués sur la technologie. OrderRepository.executeQuery(sql) est une API de base de données sous un nouveau nom. Nommez les ports d'après ce dont le cœur a besoin : save, findOpenOrdersFor(customer).
  • Des objets du domaine qui sont des entités ORM. Les annotations du framework de persistance font entrer la base de données dans le cœur. Faites la correspondance entre objets du domaine et modèles de persistance dans l'adaptateur.
  • Des adaptateurs qui portent des règles métier. Un adaptateur de paiement qui décide que les remboursements de plus de 500 dollars nécessitent une validation a sorti une règle du cœur. Limitez les adaptateurs à la traduction et au transport.
  • Un port par classe. Les ports servent aux frontières, pas à chaque collaborateur à l'intérieur du cœur.

Le diagramme d'architecture

Un hexagone figure le cœur avec le cas d'usage PlaceOrder, le modèle de domaine et trois ports. Un contrôleur REST et un pilote de test appellent le port entrant. Le cas d'usage appelle les ports OrderRepository et PaymentGateway, qu'implémentent un adaptateur Postgres et un de paiement.

14 composants · 10 connexions

Ouvrir dans Archboard
Contenu du diagramme
  • PlaceOrder use case
  • Domain model
  • PlaceOrder port
  • OrderRepository port
  • PaymentGateway port
  • REST controller
  • Test driver
  • Postgres adapter
  • Payment API adapter
  • Postgres
  • Payment provider
  • calls
  • implements
  • SQL
  • HTTPS
Un service de commandes en architecture hexagonale. Le cœur contient le cas d'usage PlaceOrder et le modèle de domaine ; il appelle des ports sortants implémentés par des adaptateurs Postgres et de paiement, et un contrôleur REST ou un pilote de test le pilote via un port entrant.

Comment le lire :

  • L'hexagone est le cœur de l'application : le cas d'usage PlaceOrder et le modèle de domaine, sans code de framework ni de fournisseur.
  • À gauche, côté primaire : un contrôleur REST et un pilote de test appellent tous deux le port entrant PlaceOrder. Du point de vue du cœur, ils sont interchangeables.
  • À droite, côté secondaire : le cas d'usage appelle deux ports sortants. Les arêtes en tirets à pointe de flèche creuse montrent l'adaptateur Postgres et l'adaptateur d'API de paiement qui les implémentent : c'est le sens de la dépendance du code.
  • Tout à droite : la vraie base Postgres et le prestataire de paiement, que seuls les adaptateurs connaissent.
  • Le bloc Markdown énonce la règle de dépendance, la seule chose que les relecteurs doivent vérifier.

Les tests coûtent moins cher

L'essentiel du bénéfice apparaît dans les tests :

  • Les tests du cœur utilisent des adaptateurs en mémoire et s'exécutent en quelques millisecondes. Règles de prix, transitions d'état et gestion des erreurs sont testées sans I/O.
  • Les tests d'adaptateurs vérifient un adaptateur face à la vraie technologie : le repository Postgres face à un vrai Postgres, l'adaptateur de paiement face au sandbox du prestataire.
  • Quelques tests de bout en bout prouvent le câblage.

Ce découpage, c'est la pyramide des tests rendue concrète, et c'est pourquoi les équipes adoptent le pattern même dans de petits services.

Ajouter une nouvelle entrée

Le port entrant devient rentable dès qu'un deuxième canal arrive. Disons que l'entreprise veut recevoir des commandes depuis la file de messages d'un partenaire en plus du web. Vous écrivez un consommateur qui lit un message, construit le même PlaceOrderCommand et appelle le même port. La validation, le calcul des prix et la logique de paiement ne changent pas, pas plus que leurs tests. Sans le port, on obtient en général une seconde copie de la logique de commande dans le consommateur, qui s'éloigne de la première en quelques versions.

Quand ça ne vaut pas la peine

  • Du CRUD sans règles. Si le service déplace surtout des données entre HTTP et une table, les ports ne sont que du cérémonial.
  • Du code éphémère. Les prototypes et les scripts ne rentabilisent pas les interfaces supplémentaires.
  • Un seul adaptateur pour toujours. Si vous ne changerez jamais de base de données et que vous testez de toute façon contre une vraie, un port sortant ajoute une indirection qui rapporte peu. Gardez quand même le cœur libre de code de framework.

L'architecture hexagonale est proche parente de l'architecture en oignon et de la clean architecture. Elles diffèrent par le vocabulaire des couches, mais partagent la règle de dépendance.

Ce qu'il faut mettre sur le diagramme

  • La frontière du cœur, dessinée comme une forme qui n'englobe clairement que le domaine et les cas d'usage.
  • Les ports sur la frontière, nommés comme le cœur les nomme.
  • Les adaptateurs à l'extérieur, étiquetés primaires ou secondaires, avec la technologie qu'ils encapsulent.
  • Le sens de la dépendance pour les adaptateurs secondaires, différent du sens d'appel.

Pour un système entier plutôt qu'un seul service, l'exemple de diagramme selon le modèle C4 montre comment zoomer des conteneurs vers les composants, et le diagramme d'architecture microservices montre où s'insèrent des services comme celui-ci. Le diagramme d'intégration d'une passerelle de paiement couvre ce qui se passe derrière l'adaptateur de paiement.

Le construire dans Archboard

  1. Connectez-vous avec Google ou GitHub et créez un diagramme.
  2. Ajoutez un Hexagone du pack Base pour le cœur de l'application, avec un Texte comme étiquette.
  3. À l'intérieur, ajoutez des nœuds Composant du pack Modèle C4 pour le cas d'usage et le modèle de domaine, et des nœuds Composant sur la frontière pour les trois ports.
  4. Ajoutez à l'extérieur des nœuds Conteneur pour les adaptateurs primaires et secondaires, ainsi que Postgres du pack Données et un Système externe pour le prestataire de paiement.
  5. Tracez les arêtes d'implémentation en tirets avec des pointes de flèche creuses, et ajoutez un bloc Markdown du pack Base avec la règle de dépendance.

L'exemple utilise 13 nœuds de diagramme et reste dans les limites de l'offre Free.

FAQ

Quelle est la différence entre un port et un adaptateur ? Un port est une interface qui appartient au cœur. Un adaptateur est le code qui relie cette interface à une technologie précise, comme un framework REST ou une base de données.

Que sont les adaptateurs primaires et secondaires ? Les adaptateurs primaires déclenchent les interactions avec le cœur, comme les contrôleurs, les consommateurs et les tests. Les adaptateurs secondaires sont appelés par le cœur, comme les repositories et les clients d'API.

L'architecture hexagonale est-elle la même chose que la clean architecture ? Elles partagent la règle de dépendance : les dépendances pointent vers le domaine. La clean architecture et l'architecture en oignon ajoutent des couches nommées dans le cœur ; l'architecture hexagonale ne distingue que l'intérieur de l'extérieur.

Chaque service a-t-il besoin d'une architecture hexagonale ? Non. Elle est rentable là où il y a de vraies règles métier et plus d'une façon de les piloter ou de les stocker. Pour des services CRUD minces, ce sont surtout des fichiers en plus.

Où placer les transactions ? En général dans le cas d'usage ou dans un port d'unit of work, pour que le cœur décide de ce qui doit être validé ensemble et l'adaptateur de la manière de le faire.

Commencez gratuitement — un siège, sans carte bancaire.

Articles associés

← Retour au blog

Ouvrez un canevas en une minute

Connectez-vous avec Google ou GitHub. Gratuit pour un siège. Passez à un forfait supérieur quand l’équipe vous rejoint.