Introduction à l’architecture hexagonale

Vous avez dit « layered architecture » ?

Tout commence ici. Une architecture en couches. Qu’est-ce que c’est ? à quoi ça sert ? Comment la mettre en place?

En programmation on parle souvent de SOLID. Le ‘S’ c’est ‘single responsability principle’. Ca se rapproche du mantra UNIX: « Do One Thing And Do It Well ».

Le concept d’architecture en couches permet de répondre à la question : « comment on structure notre programme en séparant les responsabilités ? »

Il est donc fréquent de voir les applications découpées en 3 couches:

3 Layers architecture - application
3 layers architetcure


Une autre version courante utilise 4 couches:

4 layers architecture
4 layers architecture

En terme de responsabilité:

  1. La couche présentation gère les entrées/sorties de l’application.
  2. La couche application gère les cas d’utilisation.
  3. La couche domaine exprime le métier et garantit que les invariants sont respectés.
  4. La couche persistance s’occupe de stocker et de récupérer les données.

Note 1: Dans le système à 3 couches, les couches application et domaine sont regroupées dans « Logique métier ».

Note 2: La couche persistance est également appelée couche « Infrastructure » pour permettre de prendre en compte les différentes sources de données (RestAPI, NAS, S3, BDD, etc.) et les communications avec des systèmes externes (middleware synchrone, asynchrone, webhooks, etc.).

La règle c’est qu’on ne peut pas casser cette chaine de responsabilité (la couche Présentation ne peut pas appeller la couche Persistance).

Alors, si vous en êtes là dans vos projets c’est déjà pas mal. (sisi même en 2022)

… enfin … tout ça c’est bien beau sur l’écran mais on fait comment ?

Eh bien il y a des gens qui ont cherché et qui ont apporté des réponses.

Parmi eux: Martin Fowler, Alistair Cockburn, Eric Evans, Robert C. Martin (uncle Bob), Vaughn Vernon, Sam Newman, Kent Beck, Greg Young, Jeffrey Palermo, j’en passe…

Comment implémenter une architecture en couches ?

Donc comme souvent dans l’informatique et le développement, les problèmes se ressemblent, les solutions aussi. Il existe de nombreux patrons de conception (design patterns) permettant de répondre à ces problèmes.

Disclaimer: Je sais, j’emploie le terme patron de conception en sortant du cadre du GoF.

Donc comment on implémente un architecture en couches ?

Il existe en fait 3 architectures principales qui décrivent comment faire:

  1. Onion Architecture – Jeffrey Palermo
Onion architecture
Onion Architecture

2. Clean Architecture – Robert C. Martin

Clean architecture


3. Hexagonal Architecture – Alistair Cockburn

Architecture hexagonale
Hexagonal architecture

Allez j’ose : c’est la même chose ! En fait ce dont on parle c’est de l’isolation du domaine et de l’interfaçage avec les systèmes externes.

Pour le domaine: faut aller là et 

Architecture hexagonale

ça y est ! on est dans le sujet !

L’architecture hexagonale c’est aussi appelé port/adapteur (ports adapters)!

hub

Comment fait-on pour brancher de l’HDMI à un port USB-C ? ou: Comment utilise-t-on postgresql pour stocker les données de notre domaine ? Et un bucket S3 ? Et une API Rest ? …

Via des adapteurs !

Les adapteurs

Tout ce qui ne fait pas partie du domaine est un adapteur.

  • J’expose mon application via un service REST ? -> adapteur
  • J’écoute sur une queue RabbitMQ ? -> adapteur
  • Je stocke du cache dans Redis ? -> adapteur

Je répète: si pas domaine alors adapteur.

Et donc comment on assemble le tout ? via ce qu’on appelle des APIs et des SPIs. (on en parle après)

Quelques règles:

  1. les adapteurs ne peuvent pas s’entre-appeler : l’adapteur persistance ne peut pas appeler l’adapteur messaging ou l’application la persistance.
  2. le domaine ne peut pas dépendre des adapteurs.

APIs

API : Application Programming Interface. On en parle dans plein de contextes : Rest API, l’API d’une librairie, etc. et c’est parce que c’est le même principe. Il en va de même en architecture hexagonale.

Votre domaine va proposer une API : il va exposer des fonctionnalités.

Si on reprend l’exemple de l’article précédent ici nous avons :

namespace Commerce {
  public class Profil {
    IEnumerable<Commerce.Competence> Competences { get; }
    Commerce.Disponibilite Disponibilite { get; }
    
    void ChangeDisponibilite(Commerce.Disponibilite disponibilite) {
      /* validation des règles métier + stockage de l'information */
      Disponibilite = disponibilite;
      EmetEvenement(ProfilAChangeDeDisponibilite());
    }
  }
}

Cela signifie que quand on dispose d’un agrégat Profil l’API nous permet de:

  1. Accèder à ses compétences.
  2. Accèder à sa Disponibilité.
  3. Changer sa Disponibilité.

On peut donc imaginer dans l’adapteur Application un use-case : ChangeDisponibiliteProfile:

namespace Application {
  public class ChangeDisponibiliteProfile {
    private readonly IProfilRepository _profilRepository;

    public ChangeDisponibiliteProfile(IProfilRepository profilRepository) {
      _profilRepository = profilRepository;
    }

    void Execute(ChangeDisponibiliteRequest changeDisponibiliteRequest) {
      var profil = this._profilRepository.FindById(changeDisponibiliteRequest.profilId);
      profil.ChangeDisponibilite(MapToDisponibilite(changeDisponibiliteRequest.Disponibilite));
    }
  }
}

Notre adapteur Application a utilisé l’API du Domaine.

SPIs

SPI : Service Provider Interface. C’est un service dont le domaine a besoin pour fonctionner mais dont la responsabilité appartient à un adapteur.

Le domaine va donc fournir une interface (au sens POO) exposant son besoin pour qu’un adapteur puisse l’implémenter.

Note: la granularité de l’interface varie beaucoup d’un projet à l’autre, plusieurs options sont possibles.

Pour reprendre notre exemple, la classe ChangeDisponibiliteProfile fait appel à this._profilRepository qui semble aller chercher les informations quelque part (une BDD sûrement, ou une REST API).

Mais dans les règles on a dit les adapteurs ne peuvent pas s’entre-appeler

C’est vrai mais ce n’est pas exactement ce qu’il se passe.

Dans notre domaine nous avons déclaré une interface:

namespace Commerce {
  public interface IProfilRepository {
    Profil FindById(Guid profilId);
  }
}

L’adapteur Application utilise cette interface comme une API. Mais c’est aussi une SPI étant donné que l’implémentation ne fait pas partie de la responsabilité du domaine.

Imaginons que les informations du profil soient stockées dans un ERP accessible par une API REST. Nous aurions donc une implémentation dans l’adapteur ErpClient de IProfilRepository.

namespace ErpClient {
  public class ProfilRepository implements IProfilRepository {
    public Profil FindById(Guid profilId) {
      var remoteProfil = this._httpClient.get<ErpProfilResponse>(profilId);
      return MapToDomain(remoteProfil);
    }
  }
}

Disclaimer: Si on veut parler du ‘I’ de SOLID on peut remplacer l’interface IProfilRepository par une interface plus précise, c’est même conseillé.

Wiring up

On a parlé du ‘S’ de SOLID. Parlons maintenant du ‘L’ et du ‘D’.

Le ‘L’ : Liskov Substitution Principle implique que des classes héritant d’une même classe peuvent être interchangées. Cela fonctionne aussi avec l’implémentation d’interface. Le ‘D’ : Dependency Inversion Principle implique que la ressource dont dépend un objet doit être fournie à la construction et que le code doit utiliser la plus haute abstraction possible.

Tous les frameworks modernes proposent de faire de l’IOC (Inversion Of Control) permettant (en très très gros) de configurer quelles implémentations utiliser pour quelles interfaces.

Nous pourrons donc spécifier que notre IProfilRepository est implémenté dans l’adapteur ErpClient par la class ProfilRepository.

Via autofac (en C#) ça donnerait:

builder.RegisterType<ErpClient.ProfilRepository>().As<Commerce.IProfilRepository>();

Conclusion

L’architecture hexagonale est une approche pour implémenter une architecture en couches. Comme pour toutes les architectures en couches l’objectif est d’isoler le domaine de toutes dépendances et interférences induites des contraintes techniques : le code du domaine doit rester ‘pur’.

Cela permet de développer des applications qui ne sont pas couplées avec une technologie en particulier : dans notre exemple les profils peuvent être stockés dans une BDD et nous aurions eu un adapteur spécifique dans ce cas-là. Il est également possible de changer en cours de projet.

Finalement je ne peux pas parler d’architecture hexagonale sans parler de tests unitaires. Les règles : le domaine ne peut pas dépendre des adapteurs. et Tout ce qui n'est pas domaine est un adapteur. implique que le domaine n’a pas de dépendance technique forte. Via les SPI nous disposons des interfaces permettant facilement de fournir un mock de nos dépendances. On peut donc facilement obtenir un test coverage de 100% sur la partie Domaine de l’application et ainsi augmenter le niveau de qualité et de confiance.

1 réaction sur “ Introduction à l’architecture hexagonale ”

  1. Ping Microservice ou 'pas tout à fait un monolithe ? ' -

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée.