Le Guide Ultime pour Maîtriser l'Architecture Hexagonale : Focus sur le Domaine
Une sonde hexagonale sur Mars par DALL•E

Bien qu’elle existe depuis de nombreuses années, l’Architecture Hexagonale connait un réel essor ces derniers temps. Au cœur de cette architecture se trouve le Domaine : il y joue un rôle central en encapsulant la logique métier et en assurant une séparation claire entre les préoccupations fonctionnelles et techniques.

Cet article a pour objectif de vous guider, pas à pas, dans la mise en place de la partie domaine d’une architecture hexagonale. Nous aborderons des questions essentielles que tout développeur doit se poser pour construire un domaine applicatif solide : Comment structurer les ports inbound et outbound ? Quel est le rôle des services métier et des entités ? Comment gérer les exceptions et les types de retour ? Quelles sont les bonnes pratiques pour la validation des données ou encore à quoi peuvent servir les DTO ?

En explorant ces thématiques, nous présenterons les solutions adéquates et les choix d’implémentation qui vous permettront de construire votre domaine avec les bons outils, tout en respectant l’état de l’art. Ce guide vous apportera les clés pour maîtriser la conception d’un domaine efficace et cohérent au sein de votre application.



1. DĂ©finition des Ports Inbound et Outbound

Dans une architecture hexagonale, les ports définissent les points d’interaction entre la logique métier du domaine et les couches externes. Ils sont découpés en deux catégories principales : les ports inbound et les ports outbound.

Ports Inbound (Interfaces Applicatives)

Les ports inbound, représentés par des interfaces comme UserApiPort, exposent les opérations que l’application offre aux couches externes. Ces ports définissent les cas d’utilisation ou les services applicatifs que le système propose, tels que createUser, findUserById, updateUser et deleteUser.

public interface UserApiPort {
    User createUser(User user);
    User findUserById(Long id);
    User updateUser(Long id, User user);
    void deleteUser(Long id);
}
Java
  • Utilisation des appels aux ports inbound :

    • Les ports inbound servent de contrats applicatifs entre le domaine et les adaptateurs externes (par exemple, les contrĂ´leurs REST, les interfaces utilisateur).
    • Ils permettent aux couches externes d’invoquer des opĂ©rations mĂ©tier sans connaĂ®tre les dĂ©tails de l’implĂ©mentation interne.
    • En se concentrant sur les besoins fonctionnels de l’application, ils offrent une interface claire pour rĂ©aliser les cas d’utilisation dĂ©finis.
  • DiffĂ©rences de nommage et de responsabilitĂ© :

    • Les interfaces inbound peuvent ĂŞtre nommĂ©es avec le suffixe ApiPort, reflĂ©tant leur rĂ´le d’interface applicative (API) pour les opĂ©rations offertes.
    • Elles se concentrent sur la logique fonctionnelle et les services que l’application fournit aux utilisateurs.
  • Gestion des retours et des exceptions :

    • Les mĂ©thodes des ports inbound renvoient directement les objets mĂ©tiers, comme User, ou lèvent des exceptions mĂ©tier en cas de problème (par exemple, ResourceNotFoundException, BusinessRuleViolationException).
    • Cela permet aux adaptateurs externes de gĂ©rer les erreurs de manière appropriĂ©e, en fournissant des rĂ©ponses claires aux clients de l’application.

Note

  • La mĂ©thode findUserById(Long id) renvoie un User ou lève une ResourceNotFoundException si l’utilisateur n’existe pas.
  • La mĂ©thode createUser(User user) lève une BusinessRuleViolationException si le nom de l’utilisateur est vide ou nul.

Avantages :

  • DĂ©couplage fonctionnel : Les ports inbound isolent la logique mĂ©tier des dĂ©tails techniques des couches externes.
  • ClartĂ© des services : Ils dĂ©finissent explicitement les opĂ©rations disponibles, facilitant la comprĂ©hension et l’utilisation de l’application.

Inconvénients :

  • Conception initiale complexe : Cela nĂ©cessite une bonne comprĂ©hension des cas d’utilisation pour dĂ©finir des interfaces pertinentes.

Ports Outbound (Interfaces Techniques)

Les ports outbound, tels que UserSpiPort, définissent comment le domaine interagit avec les systèmes externes. Ils sont axés sur les aspects techniques nécessaires pour réaliser les opérations métier, comme l’accès à la base de données ou à des services externes.

public interface UserSpiPort {
    User saveUser(User user);
    Optional<User> findUser(Long userId);
    User updateUser(User user);
    void deleteUser(Long userId);
}
Java
  • Utilisation des appels aux ports outbound :

    • Les ports outbound agissent comme des interfaces techniques que le domaine utilise pour accomplir ses tâches, sans se soucier des implĂ©mentations concrètes.
    • Ils permettent de dĂ©lĂ©guer les opĂ©rations techniques Ă  des adaptateurs spĂ©cialisĂ©s, tout en maintenant le domaine indĂ©pendant des technologies spĂ©cifiques.
  • DiffĂ©rences de nommage et de responsabilitĂ© :

    • Les interfaces outbound peuvent ĂŞtre nommĂ©es avec le suffixe SpiPort, indiquant leur rĂ´le de Service Provider Interface ou SPI.
    • Elles se concentrent sur les dĂ©tails techniques nĂ©cessaires au domaine pour fonctionner, sans inclure de logique mĂ©tier.
  • Gestion des retours et des exceptions :

    • Les mĂ©thodes des ports outbound renvoient souvent des Optional<User>, reflĂ©tant l’incertitude technique quant Ă  l’existence d’une ressource.
    • Elles ne lèvent pas d’exceptions mĂ©tier, laissant au domaine le soin de dĂ©cider comment gĂ©rer les cas oĂą les donnĂ©es ne sont pas disponibles.

Note

La méthode findUserById(Long id) renvoie un Optional<User>, indiquant que l’utilisateur peut être présent ou non dans le système externe.

Avantages :

  • FlexibilitĂ© technique : Facilite le changement d’implĂ©mentation des services techniques sans affecter le domaine.
  • TestabilitĂ© : Les ports outbound peuvent ĂŞtre facilement mockĂ©s lors des tests unitaires, isolant ainsi la logique mĂ©tier.

Inconvénients :

  • NĂ©cessitĂ© d’une abstraction adĂ©quate : Les ports doivent ĂŞtre suffisamment gĂ©nĂ©riques pour ne pas introduire de dĂ©pendances technologiques dans le domaine.

Importance de ces distinctions

  • Gestion cohĂ©rente des erreurs : En sĂ©parant les responsabilitĂ©s, le domaine peut dĂ©cider comment gĂ©rer les cas d’absence de donnĂ©es (lever une exception mĂ©tier) tandis que les ports outbound gèrent les incertitudes techniques.
  • ClartĂ© du code : Les dĂ©veloppeurs peuvent comprendre rapidement le rĂ´le de chaque interface en se basant sur son nom et sa localisation dans le projet.
  • MaintenabilitĂ© : Cette organisation facilite les modifications ultĂ©rieures, qu’il s’agisse d’ajouter de nouvelles fonctionnalitĂ©s ou de changer l’implĂ©mentation technique.

Raison du choix de cette structure

  • DĂ©couplage fort : En distinguant clairement les ports inbound et outbound, l’architecture hexagonale assure un dĂ©couplage entre la logique fonctionnelle de l’application et les dĂ©tails techniques d’implĂ©mentation.
  • AdaptabilitĂ© : Permet de modifier ou remplacer les adaptateurs techniques sans impacter le domaine ou les services applicatifs.
  • CohĂ©rence dans la communication : Les adaptateurs externes interagissent avec le domaine via des interfaces fonctionnelles claires, tandis que le domaine utilise des interfaces techniques bien dĂ©finies pour accĂ©der aux ressources externes.

2. Gestion des Exceptions dans le Domaine

Le domaine est censé être indépendant des détails techniques et se concentrer sur la logique métier. Cela soulève la question suivante : le domaine doit-il uniquement gérer des exceptions métier ou peut-il également être concerné par certaines erreurs techniques ?

Le Domaine et les Erreurs MĂ©tier

Le domaine est responsable de la logique métier et doit gérer les situations où les règles métier sont violées. Pour cela, des exceptions spécifiques au domaine doivent être définies, telles que :

  • ResourceNotFoundException : LevĂ©e lorsqu’une ressource demandĂ©e (comme un utilisateur) n’existe pas.
  • BusinessRuleViolationException : LevĂ©e lorsqu’une règle mĂ©tier est violĂ©e, par exemple, lorsqu’un utilisateur tente de s’inscrire avec une adresse e-mail dĂ©jĂ  utilisĂ©e.
public class ResourceNotFoundException extends RuntimeException {
    public ResourceNotFoundException(String message) {
        super(message);
    }
}
Java
public class BusinessRuleViolationException extends RuntimeException {
    public BusinessRuleViolationException(String message) {
        super(message);
    }
    public BusinessRuleViolationException(String message, Throwable cause) {
        super(message, cause);
    }
}
Java

Ces exceptions permettent au domaine de signaler clairement aux couches appelantes qu’une violation des règles métier a eu lieu, sans exposer les détails techniques internes.

Note

L’utilisation de RuntimeException (unchecked exceptions) simplifie le code en évitant la déclaration explicite des exceptions tout en permettant leur propagation automatique jusqu’aux adaptateurs pour une gestion centralisée des erreurs métier.

Le Domaine Peut-il se Limiter Uniquement aux Erreurs MĂ©tier ?

Idéalement, le domaine devrait se concentrer exclusivement sur les erreurs métier. Les erreurs techniques, telles que les exceptions liées à la base de données, aux réseaux ou aux entrées/sorties, devraient être gérées par les adaptateurs techniques (implémentations des ports SPI). Cependant, dans la pratique, certaines erreurs techniques peuvent avoir un impact sur la logique métier et ne peuvent pas être totalement ignorées par le domaine.

  • Exemples de cas oĂą le domaine doit considĂ©rer des erreurs techniques :

    • IndisponibilitĂ© d’un service externe essentiel : Si une opĂ©ration mĂ©tier dĂ©pend d’un service externe (comme un système de paiement) et que celui-ci est indisponible, le domaine doit dĂ©cider comment rĂ©agir, par exemple en annulant la transaction et en informant l’utilisateur.
    • Violations de contraintes techniques reflĂ©tant des règles mĂ©tier : Par exemple, une violation de contrainte d’unicitĂ© en base de donnĂ©es peut reflĂ©ter une règle mĂ©tier d’unicitĂ© qui n’a pas Ă©tĂ© respectĂ©e en amont.

Responsabilité du Domaine vis-à-vis des API et des SPI en Matière d’Erreurs

Au Niveau des Ports Inbound (API)

Les ports inbound, tels que UserApiPort, définissent les cas d’utilisation que le domaine expose aux adaptateurs externes (comme des contrôleurs REST).

  • ResponsabilitĂ©s du domaine :

    • Lever des exceptions mĂ©tier : Lorsque des règles mĂ©tier sont violĂ©es, le domaine lève des exceptions spĂ©cifiques comme BusinessRuleViolationException ou ResourceNotFoundException.
    • Fournir des retours clairs : Les mĂ©thodes du port API renvoient des objets mĂ©tier ou lèvent des exceptions mĂ©tier, ce qui permet aux adaptateurs externes de gĂ©rer les erreurs de manière appropriĂ©e.

Note

Le domaine ne doit pas propager d’exceptions techniques via les ports inbound mais il peut lever des exceptions métier (comme ResourceNotFoundException, BusinessRuleViolationException). Les adaptateurs externes capturent ces exceptions métier et les traduisent en réponses appropriées pour les clients (par exemple, des codes HTTP comme 404 Not Found ou 409 Conflict dans le cas d’une API REST).

Au Niveau des Ports Outbound (SPI)

Les ports outbound, comme UserSpiPort, définissent comment le domaine interagit avec les systèmes externes (par exemple, une base de données).

  • ResponsabilitĂ©s du domaine :

    • GĂ©rer les incertitudes techniques : Les mĂ©thodes du port SPI peuvent renvoyer des Optional<User> pour signaler que l’utilisateur peut ne pas exister, sans lever d’exceptions techniques.
    • Ne pas gĂ©rer les exceptions techniques : Les adaptateurs qui implĂ©mentent le SPI doivent capturer les exceptions techniques (comme une SQLException ou encore une ConstraintViolationException) et les transformer en rĂ©sultats que le domaine peut comprendre (par exemple, un Optional.empty()).

Note

Le domaine doit être protégé des exceptions techniques provenant des adaptateurs SPI pour maintenir son indépendance vis-à-vis des détails techniques.

En Résumé

  • Le domaine :

    • Gère les erreurs mĂ©tier en levant des exceptions spĂ©cifiques.
    • Doit ĂŞtre informĂ© des erreurs techniques critiques impactant le mĂ©tier, mais sans gĂ©rer les dĂ©tails techniques.
    • Ne propage pas d’exceptions techniques vers les adaptateurs externes.
  • Les adaptateurs techniques (SPI) :

    • Capturent les erreurs techniques et les transforment en rĂ©sultats que le domaine peut comprendre (par exemple, Optional.empty()).
    • Ne propagent pas les exceptions techniques au domaine.
  • Les adaptateurs externes (API) :

    • Reçoivent les exceptions mĂ©tier du domaine et les transforment en rĂ©ponses appropriĂ©es pour les clients (par exemple, des codes d’erreur HTTP).

En respectant ces principes, la gestion des erreurs dans le domaine reste cohérente avec les objectifs de l’architecture hexagonale : maintenir une séparation claire entre la logique métier et les détails techniques, tout en assurant une robustesse et une résilience de l’application face aux diverses erreurs qui peuvent survenir.


3. Les Services Métier dans l’Architecture Hexagonale

Dans l’architecture hexagonale, les services métier encapsulent la logique métier de l’application. Ils orchestrent les opérations nécessaires pour réaliser les cas d’utilisation définis, en s’appuyant sur les ports et les adaptateurs pour interagir avec les systèmes externes et les couches d’infrastructure.

Positionnement des Services MĂ©tier au sein des API et des SPI

public class UserApiService implements UserApiPort {

    private final UserSpiPort userSpiPort;
...
    @Override
    public User addUser(User user) {
        return userSpiPort.saveUser(user);
    }

    @Override
    public User getUser(Long userId) {
        return userSpiPort.findUser(userId)
                .orElseThrow(() -> new ResourceNotFoundException("User not found: " + userId));
    }
...
}
Java

Les services métier se situent au cœur du domaine et interagissent avec les ports inbound (API) et outbound (SPI) :

  • Ports Inbound (API) : Les services mĂ©tier implĂ©mentent les interfaces dĂ©finies par les ports API. Ces interfaces reprĂ©sentent les cas d’utilisation que l’application expose aux adaptateurs externes (par exemple, aux contrĂ´leurs REST).

    • Exemple : Le service UserApiService implĂ©mente l’interface UserApiPort, qui dĂ©finit les opĂ©rations telles que createUser, findUserById, updateUser et deleteUser.
  • Ports Outbound (SPI) : Les services mĂ©tier utilisent les interfaces dĂ©finies par les ports SPI pour interagir avec les systèmes externes (comme la persistance des donnĂ©es). Ils dĂ©lèguent les opĂ©rations techniques aux adaptateurs qui implĂ©mentent ces ports.

    • Exemple : UserApiService utilise UserSpiPort pour accĂ©der aux mĂ©thodes saveUser, findUserById, etc., sans se soucier de savoir oĂą et comment ces donnĂ©es seront sauvegardĂ©es.

Ce que les Services MĂ©tier Peuvent Faire

  • Encapsuler la Logique MĂ©tier : Ils sont responsables de la mise en Ĺ“uvre des règles mĂ©tier, des validations spĂ©cifiques et de l’orchestration des opĂ©rations nĂ©cessaires pour rĂ©aliser un cas d’utilisation.

    • Exemple : VĂ©rifier qu’un utilisateur n’existe pas dĂ©jĂ  avant de le crĂ©er, ou que les donnĂ©es fournies respectent les contraintes mĂ©tier.
  • Lever des Exceptions MĂ©tier : En cas de violation des règles mĂ©tier, les services peuvent lever des exceptions spĂ©cifiques pour signaler le problème aux couches supĂ©rieures.

    • Exemple : Lever une BusinessRuleViolationException si une adresse e-mail est dĂ©jĂ  utilisĂ©e.
  • Utiliser les Ports SPI : Ils dĂ©lèguent les opĂ©rations techniques aux adaptateurs via les ports SPI, assurant ainsi le dĂ©couplage entre la logique mĂ©tier et les dĂ©tails techniques.

    • Exemple : Appeler userSpiPort.saveUser(user) pour persister un utilisateur sans connaĂ®tre les dĂ©tails de la base de donnĂ©es.

Ce que les Services MĂ©tier Ne Doivent Pas Faire

  • GĂ©rer les DĂ©tails Techniques : Ils ne doivent pas inclure de logique liĂ©e aux technologies spĂ©cifiques, telles que les interactions directes avec la base de donnĂ©es, les protocoles rĂ©seau ou les frameworks externes.

    • Explication : Cela violerait le principe de sĂ©paration des prĂ©occupations et rendrait le domaine dĂ©pendant des dĂ©tails techniques.
  • Manipuler les Objets Techniques : Les services mĂ©tier ne doivent pas manipuler directement des objets techniques (par exemple, des entitĂ©s JPA, des DTOs spĂ©cifiques aux frameworks).

    • Explication : Ils doivent travailler avec des objets mĂ©tier purs pour maintenir l’indĂ©pendance du domaine.
  • GĂ©rer les Exceptions Techniques : Ils ne doivent pas traiter les exceptions liĂ©es aux couches techniques (comme les SQLException). Ces exceptions doivent ĂŞtre capturĂ©es et gĂ©rĂ©es par les adaptateurs techniques.

    • Explication : Le domaine doit rester agnostique des dĂ©tails techniques pour assurer sa portabilitĂ© et sa testabilitĂ©.

Avantages des Services MĂ©tier

  • Centralisation de la Logique MĂ©tier : En regroupant les règles et les processus mĂ©tier au sein des services, on facilite la maintenance et l’évolution du système.

  • DĂ©couplage des Couches : Les services mĂ©tier interagissent avec les ports, assurant ainsi une sĂ©paration nette entre le domaine et les couches techniques.

  • TestabilitĂ© AmĂ©liorĂ©e : En isolant la logique mĂ©tier, les services peuvent ĂŞtre testĂ©s indĂ©pendamment des infrastructures externes.

En suivant ces directives, les services métier contribuent à une architecture claire, modulaire et respectueuse des principes du DevOps et du craftsmanship.


4. Utilisation des Entités Métier

Dans le cadre de l’architecture hexagonale, les entités métier représentent les objets principaux du domaine, en encapsulant à la fois l’état et le comportement associés. Elles sont au cœur de la logique métier et doivent être conçues de manière à assurer la cohérence, la maintenabilité et l’indépendance vis-à-vis des couches techniques.

Les Entités Métier

Les entités métier sont des objets qui modélisent les éléments clés du domaine applicatif, tels que les Users, les commandes ou les produits. Elles contiennent les données essentielles et les méthodes qui permettent de manipuler ces données selon les règles métier définies.

public class User {
    private Long id;
    private String name;
    private String email;
    private boolean active;

    public User(Long id, String name, String email) {
        validateName(name);
        validateEmail(email);
        this.id = id;
        this.name = name;
        this.email = email;
        this.active = false;
    }

    // Méthodes métier
    public void activateAccount() {
        this.active = true;
    }

    public void changeEmail(String newEmail) {
        validateEmail(newEmail);
        this.email = newEmail;
    }

    // Validations internes
    private void validateName(String name) {
        if (name == null || name.isEmpty()) {
            throw new BusinessRuleViolationException("Name cannot be null or empty.");
        }
    }

    private void validateEmail(String email) {
        if (email == null || !email.contains("@")) {
            throw new BusinessRuleViolationException("Invalid email address.");
        }
    }

    // Getters et setters
    // ...
}
Java
  • Principales caractĂ©ristiques des entitĂ©s mĂ©tier :

    • Encapsulation de l’état et du comportement : Les entitĂ©s regroupent les attributs (donnĂ©es) et les mĂ©thodes (comportements) qui leur sont propres.
    • IndĂ©pendance technologique : Elles ne dĂ©pendent pas des frameworks, bibliothèques ou technologies spĂ©cifiques, ce qui permet de maintenir le domaine indĂ©pendant des couches externes.
    • CohĂ©rence des règles mĂ©tier : Elles assurent le respect des contraintes et des invariants du domaine.

Différentes Implémentations Possibles

Plusieurs approches peuvent être adoptées pour implémenter les entités métier en Java :

1. Java POJO (Plain Old Java Object)

Les POJOs sont des classes Java classiques sans dépendances particulières à des frameworks. Ils contiennent des attributs privés et des méthodes publiques pour accéder et modifier ces attributs.

  • Avantages :

    • SimplicitĂ© et clartĂ© : Faciles Ă  comprendre et Ă  maintenir.
    • ContrĂ´le total : Permettent une personnalisation complète du comportement.
  • InconvĂ©nients :

    • Verbosity : NĂ©cessitent l’écriture manuelle de code rĂ©pĂ©titif (constructeurs, getters, setters).

2. Records Java

Introduits en Java 14, les records sont des classes immuables concises destinées à contenir des données.

public record User(Long id, String name, String email, boolean active) {
    public User {
        validateName(name);
        validateEmail(email);
    }

    // Méthodes métier renvoyant de nouveaux objets en raison de l'immutabilité
    public User activateAccount() {
        return new User(id, name, email, true);
    }

    public User changeEmail(String newEmail) {
        validateEmail(newEmail);
        return new User(id, name, newEmail, active);
    }

    // Validations internes
    private static void validateName(String name) {
        if (name == null || name.isEmpty()) {
            throw new BusinessRuleViolationException("Name cannot be null or empty.");
        }
    }

    private static void validateEmail(String email) {
        if (email == null || !email.contains("@")) {
            throw new BusinessRuleViolationException("Invalid email address.");
        }
    }
}
Java
  • Avantages :

    • Concision : RĂ©duisent le code boilerplate.
    • ImmutabilitĂ© : Favorisent la sĂ©curitĂ© et la cohĂ©rence des donnĂ©es.
  • InconvĂ©nients :

    • Limitation des mutations : Chaque modification crĂ©e une nouvelle instance, ce qui peut ĂŞtre moins performant.
    • DisponibilitĂ© : NĂ©cessitent Java 14 ou supĂ©rieur.

3. Lombok

Lombok est une bibliothèque qui génère automatiquement du code répétitif grâce à des annotations.

@Data
@AllArgsConstructor
public class User {
    private Long id;
    private String name;
    private String email;
    private boolean active;

    // Méthodes métier
    public void activateAccount() {
        this.active = true;
    }

    public void changeEmail(String newEmail) {
        if (newEmail == null || !newEmail.contains("@")) {
            throw new BusinessRuleViolationException("Invalid email address.");
        }
        this.email = newEmail;
    }
}
Java
  • Avantages :

    • RĂ©duction du code rĂ©pĂ©titif : GĂ©nère automatiquement les getters, setters, constructeurs, etc.
    • LisibilitĂ© amĂ©liorĂ©e : Code source plus concis.
  • InconvĂ©nients :

    • DĂ©pendance externe : Introduit une dĂ©pendance supplĂ©mentaire.
    • Magie cachĂ©e : Le code gĂ©nĂ©rĂ© n’est pas visible, ce qui peut compliquer le dĂ©bogage.

Recommandations

Après avoir évalué les différentes options, voici des préconisations claires :

  1. Favoriser les POJOs pour un ContrĂ´le Complet

    • Pourquoi : Ils offrent une grande flexibilitĂ© et indĂ©pendance vis-Ă -vis des versions de Java ou des dĂ©pendances externes.
    • Bonnes pratiques :
      • Utiliser des attributs privĂ©s avec des mĂ©thodes publiques pour l’accès.
      • Inclure des validations dans les constructeurs et les setters.
      • Éviter de trop exposer l’état interne (principe d’encapsulation).
  2. Utiliser les Records pour les Entités Immuables

    • Pourquoi : Si l’entitĂ© mĂ©tier est naturellement immuable, les records offrent une syntaxe concise et sĂ»re.
    • Bonnes pratiques :
      • Inclure des validations dans le constructeur compact.
      • GĂ©rer les mutations en retournant de nouvelles instances.
  3. Utiliser Lombok avec Précaution

    • Pourquoi : Lombok peut accĂ©lĂ©rer le dĂ©veloppement, mais peut introduire de la complexitĂ©.
    • Bonnes pratiques :
      • S’assurer que l’équipe est Ă  l’aise avec Lombok.
      • Documenter clairement l’utilisation des annotations.
      • Limiter Lombok aux cas oĂą le gain est significatif.

Validation des Données dans les Entités Métier

La validation des données est essentielle pour maintenir l’intégrité du domaine.

  • Mise en place de la validation :

    • Dans les constructeurs et mĂ©thodes : IntĂ©grer des validations pour chaque attribut lors de la crĂ©ation ou de la modification.
    • Lever des exceptions mĂ©tier : Utiliser des exceptions spĂ©cifiques pour signaler les violations des règles mĂ©tier.
public class User {
    // Attributs privés

    public User(Long id, String name, String email) {
        validateName(name);
        validateEmail(email);
        // Initialisation des attributs
    }

    public void changeEmail(String newEmail) {
        validateEmail(newEmail);
        this.email = newEmail;
    }

    private void validateName(String name) {
        if (name == null || name.isEmpty()) {
            throw new BusinessRuleViolationException("Name cannot be null or empty.");
        }
    }

    private void validateEmail(String email) {
        if (email == null || !email.contains("@")) {
            throw new BusinessRuleViolationException("Invalid email address.");
        }
    }

    // Autres méthodes et getters/setters
}
Java

5. Choix des Types de Retour des MĂ©thodes

Dans une architecture hexagonale, le choix des types de retour pour les méthodes du domaine, du SPI et de l’API est d’une importance capitale. Ce choix influence directement les capacités et le rôle de chaque composant, et il doit être effectué avec soin pour maintenir une séparation claire entre la logique métier, les détails techniques et la communication avec les clients externes.

Les types de retour des méthodes agissent comme des points d’interface entre le domaine, le SPI et l’API. En définissant judicieusement ces types, on s’assure que chaque couche remplit sa fonction spécifique sans empiéter sur les responsabilités des autres. Ainsi :

  • Le domaine peut se concentrer sur la logique mĂ©tier, en retournant des objets mĂ©tier clairs ou en levant des exceptions mĂ©tier appropriĂ©es.
  • Le SPI gère les dĂ©tails techniques et les incertitudes des systèmes externes, en utilisant des types de retour techniques comme Optional ou des codes d’erreur.
  • L’API interagit avec les clients externes, en traduisant les rĂ©sultats du domaine en rĂ©ponses adaptĂ©es et en respectant les protocoles de communication standard.

Note

Introduit en Java 8, Optional est une classe conteneur qui peut ou non contenir une valeur non nulle. Elle est utilisée pour représenter explicitement l’absence possible d’une valeur, évitant ainsi les problèmes liés aux NullPointerException.

Exemples Illustratifs

Pour mieux comprendre comment cette séparation fonctionne en pratique, voici quelques scénarios concrets présentant les interactions entre le SPI, le domaine et l’API.

ScénarioSPIDomaineAPI (ex. REST)
1. Recherche d’un utilisateur inexistantRenvoie Optional.empty()Lève une exception métier ResourceNotFoundException.Capture l’exception et renvoie une réponse HTTP 404 Not Found au client.
2. Création d’un utilisateur déjà existantCapture l’exception technique de contrainte d’unicité.Avant de sauvegarder, le domaine vérifie si l’utilisateur existe déjà. S’il existe, il lève une BusinessRuleViolationException.Capture l’exception et renvoie une réponse HTTP 409 Conflict au client.
3. Mise à jour d’une ressource inexistanteRenvoie un booléen indiquant si la mise à jour a réussi.Si la mise à jour a échoué (retour false), le domaine lève une ResourceNotFoundException.Capture l’exception et renvoie une réponse HTTP 404 Not Found au client.
4. Erreur de connexion à la base de donnéesCapture l’exception technique DatabaseConnectionException.Peut lever une ServiceUnavailableException ou gérer l’erreur selon les règles métier.Capture l’exception et renvoie une réponse HTTP 503 Service Unavailable au client.
5. Liste de ressources vide lors de la récupération des utilisateursRenvoie une liste, pouvant être vide.La liste vide est considérée comme une réponse valide et la retourne telle quelle.Renvoie une réponse HTTP 200 OK avec une liste vide au client.

Avantages de cette Approche

  • DĂ©couplage des Couches : Chaque couche a une responsabilitĂ© bien dĂ©finie, ce qui facilite la maintenance et l’évolutivitĂ©.
  • ClartĂ© dans la Gestion des Erreurs : Les erreurs techniques ne traversent pas les couches, et les clients reçoivent des messages cohĂ©rents.
  • FlexibilitĂ© : Permet de changer l’implĂ©mentation technique du SPI sans impacter le domaine ou l’API.

Bonnes Pratiques

  • Ne pas Exposer les Types Techniques du SPI au Domaine : Le domaine doit travailler avec des objets mĂ©tier et ne pas dĂ©pendre des types techniques spĂ©cifiques.
  • Utiliser des Exceptions MĂ©tier dans le Domaine : Pour signaler des problèmes liĂ©s aux règles mĂ©tier.
  • Traduire les Exceptions MĂ©tier en Codes HTTP AppropriĂ©s : L’API doit mapper les exceptions aux codes HTTP standard pour une communication claire avec le client.
  • GĂ©rer les Exceptions Techniques dans le SPI : Le SPI doit capturer les exceptions techniques et fournir des retours que le domaine peut interprĂ©ter.

6. Validation des Données

Dans une architecture hexagonale, la validation des données peut être effectuée à plusieurs niveaux, mais le service métier est le principal responsable des validations métier. Cependant, les adaptateurs d’entrée (par exemple, les contrôleurs REST ou les services d’application) peuvent également jouer un rôle en validant la syntaxe e t la structure des données avant qu’elles ne soient transmises au domaine.

Voici la répartition des responsabilités.

Adaptateurs d’Entrée (REST, UI, etc.)

Ils peuvent vérifier que les données reçues respectent la syntaxe et le format attendu (par exemple, des champs obligatoires, des formats de date valides, etc.).

Ces adaptateurs peuvent utiliser des bibliothèques de validation comme Hibernate Validator (qui suit le standard Bean Validation), pour valider les DTOs avant qu’ils ne soient passés au domaine.

Cela permet de filtrer les erreurs avant que les données ne parviennent au service métier, réduisant ainsi la complexité de gestion des erreurs dans le domaine.

@PostMapping("/users")
public ResponseEntity<UserDto> createUser(@Valid @RequestBody UserDto userDto) {
   // If validation fails, a 400 Bad Request will be returned automatically
   User createdUser = userService.createUser(userDtoMapper.toDomain(userDto));
   return new ResponseEntity<>(userDtoMapper.toDto(createdUser), HttpStatus.CREATED);
}
Java

Service MĂ©tier (domain)

Il est responsable des validations métier qui sont spécifiques au domaine. Il s’agit par exemple de vérifier qu’un utilisateur n’existe pas déjà, ou qu’une règle métier spécifique est respectée (exemple : l’utilisateur doit être majeur).

Le domaine utilise des instructions standards du langage pour encapsuler ces validations dans les objets métier. Les exceptions métier sont levées si des règles sont violées.

La validation métier garantit que les règles métiers sont respectées. Cela permet de maintenir l’intégrité des données dans le domaine.

public User createUser(User user) {
   if (userRepository.findUserByEmail(user.getEmail()).isPresent()) {
       throw new BusinessRuleViolationException("User already exists.");
   }
   return userRepository.saveUser(user);
}
Java

Impacts pour les Autres Composants

  • Adaptateurs d’entrĂ©e :
    • En s’assurant que les donnĂ©es reçues sont valides dès la rĂ©ception, les adaptateurs d’entrĂ©e permettent de rĂ©duire la complexitĂ© et le traitement des erreurs dans le domaine. En cas de validation Ă©chouĂ©e, les adaptateurs retournent directement une 400 Bad Request avec un message explicatif.
  • Service mĂ©tier :
    • Si une validation Ă©choue dans le service mĂ©tier (par exemple, violation d’une règle mĂ©tier), une exception spĂ©cifique (comme une BusinessRuleViolationException) est levĂ©e et capturĂ©e par l’adaptateur d’entrĂ©e pour renvoyer un 409 Conflict ou un autre code HTTP appropriĂ©. Cela garantit que les règles mĂ©tiers sont centrĂ©es dans le domaine et non dans l’infrastructure.

Avantages et Inconvénients

  • Avantages :
    • SĂ©paration des responsabilitĂ©s : Les validations de structure et de syntaxe sont gĂ©rĂ©es au niveau de l’adaptateur, tandis que les validations mĂ©tiers sont concentrĂ©es dans le service mĂ©tier.
    • ClartĂ© des erreurs : Les erreurs liĂ©es Ă  des violations de règles mĂ©tier ou Ă  des formats incorrects sont clairement identifiĂ©es et renvoyĂ©es avec des codes HTTP appropriĂ©s (400, 409, etc.).
  • InconvĂ©nients :
    • Duplication potentielle : Dans certains cas, une mĂŞme validation pourrait ĂŞtre nĂ©cessaire Ă  la fois dans l’adaptateur (pour des raisons de structure) et dans le domaine (pour des raisons mĂ©tiers), ce qui peut entraĂ®ner de la duplication.
    • ComplexitĂ© supplĂ©mentaire : Bien que cette approche soit très modulaire et dĂ©couplĂ©e, elle peut parfois rendre le système plus complexe Ă  implĂ©menter et maintenir.

En résumé, dans une architecture hexagonale, la validation des données est divisée entre les adaptateurs d’entrée et le service métier, avec une nette séparation entre les validations de structure et de syntaxe, et les validations métier. Ce découplage permet de rendre le système plus modulaire, mais demande une attention particulière pour éviter la duplication des validations.


7. Rôle des DTO dans l’Architecture Hexagonale

Dans une architecture hexagonale, les DTO (Data Transfer Objects) servent à transférer des données entre les différentes couches de l’application, notamment entre les adaptateurs externes (comme les contrôleurs REST) et le domaine. Ils permettent de maintenir un découplage strict entre la logique métier et les interfaces externes tout en facilitant l’adaptation aux formats de données spécifiques à chaque couche.

Pourquoi Utiliser des DTO ?

  • SĂ©paration des PrĂ©occupations :

    Les DTO permettent de séparer la représentation des données dans les interfaces externes (API REST, UI) des objets métiers du domaine. Cela garantit que la logique métier encapsulée dans les objets métier n’est pas directement exposée aux adaptateurs externes.

    Exemple : Un UserDto utilisé pour transmettre les données d’un utilisateur via une API REST ne contient que les informations nécessaires (ID, nom, adresse), tandis que l’objet métier User encapsule des comportements et des règles métier plus complexes.

public class User {
  private Long id;
  private String name;
  private String email;
  private Address address; // Classe qui contient les informations d'adresse de l'utilisateur
  private List<Order> orders; // Liste des commandes passées par l'utilisateur

  // Constructeurs, getters et setters...
}
Java
public class UserDto {
  private Long id;
  private String name;
  private String address; // Adresse représentée sous forme de chaîne de caractères (ex: "123 Main St, City, Country")

  // Constructeurs, getters et setters...
}
Java
  • Adaptation aux Formats de DonnĂ©es :

    Les DTO permettent de mapper des données d’un format adapté aux besoins des clients externes (par exemple, JSON pour une API REST) vers des objets métier plus riches qui respectent les règles du domaine. Cela permet une flexibilité dans la transformation des données.

    Exemple : Un UserDtoMapper peut convertir un UserDto en objet métier User et vice-versa.

public class UserDtoMapper {
// MĂ©thode pour convertir un DTO en objet de domaine
public User toDomain(UserDto dto) {
    Address address = parseAddress(dto.getAddress()); // Conversion de l'adresse sous forme de String vers un objet Address
    return new User(dto.getId(), dto.getName(), dto.getEmail(), address, new ArrayList<>());
}

// MĂ©thode pour convertir un objet de domaine en DTO
public UserDto toDto(User user) {
    String address = formatAddress(user.getAddress()); // Conversion de l'objet Address en String
    return new UserDto(user.getId(), user.getName(), user.getEmail(), address);
}

// Méthode utilitaire pour transformer une chaîne d'adresse en objet Address
private Address parseAddress(String address) {
    // Suppose que l'adresse est sous forme de "123 Main St, City, Country"
    String[] parts = address.split(", ");
    return new Address(parts[0], parts[1], parts[2]);
}

// Méthode utilitaire pour formater un objet Address en une chaîne de caractères
private String formatAddress(Address address) {
    return String.format("%s, %s, %s", address.getStreet(), address.getCity(), address.getCountry());
}
}
Java
  • Protection du Domaine :

    Les DTO offrent un contrôle sur les données exposées aux clients externes, en filtrant les informations sensibles ou inutiles dans le contexte de l’API. Cela protège l’intégrité des données du domaine et évite de dévoiler des détails techniques ou métier inutiles.

    Exemple : Un UserDto peut omettre des champs sensibles tels que des informations financières ou des mots de passe.

Les Avantages des DTO

  • ModularitĂ© : Le dĂ©couplage entre les couches externes et le domaine permet une meilleure modularitĂ© du code. Les changements dans les DTO n’affectent pas directement le domaine, facilitant ainsi la maintenance.

  • RĂ©duction des DĂ©pendances : Les couches externes n’ont pas besoin de connaĂ®tre les dĂ©tails internes du domaine, ce qui limite les dĂ©pendances entre les diffĂ©rentes couches de l’application.

  • AdaptabilitĂ© et ÉvolutivitĂ© : Les DTO permettent d’adapter facilement le format des donnĂ©es en fonction des besoins des interfaces externes (ajout de champs, gestion des versions d’API) sans impacter la logique mĂ©tier.

Les Inconvénients des DTO

  • ComplexitĂ© SupplĂ©mentaire : L’utilisation de DTO nĂ©cessite de maintenir des classes supplĂ©mentaires ainsi que des mappers pour transformer les objets entre les couches, ce qui peut alourdir le code et augmenter la maintenance.

  • Duplication Potentielle : Les DTO peuvent parfois dupliquer certaines informations prĂ©sentes dans les objets mĂ©tier, entraĂ®nant une surcharge de maintenance si les mappers ne sont pas bien gĂ©rĂ©s.

En Résumé

L’utilisation des DTO dans une architecture hexagonale est essentielle pour maintenir l’indépendance du domaine vis-à-vis des technologies externes. Ils permettent de mapper les données entre les différentes couches de manière flexible, de protéger les objets métier contre l’exposition directe, et d’assurer une meilleure modularité de l’application. Cependant, cette approche introduit une certaine complexité et demande un effort supplémentaire pour maintenir les mappers et les DTO.


8. Organisation en Packages du Domaine

Une organisation claire et bien découpée des packages permet d’éviter les erreurs de conception et de bien identifier chaque composant du système. En isolant le domaine dans un module indépendant, on garantit que ce dernier ne soit pas pollué par des dépendances techniques ou des frameworks externes. Cette séparation permet de maintenir l’intégrité du domaine en protégeant sa logique métier des aspects techniques, tout en facilitant l’évolution de l’architecture au fil du temps.

Dans le cadre d’une architecture hexagonale, cette structure modulaire assure que les responsabilités soient bien définies entre le domaine, les ports (inbound et outbound) et les services, favorisant ainsi un découplage clair et une organisation cohérente du code.

Package by Layer vs. Package by Feature

  • L’approche Package by Layer consiste Ă  organiser les classes par leur rĂ´le technique, en les regroupant par couches transversales de l’architecture.
  • L’approche Package by Feature consiste Ă  organiser les classes par fonctionnalitĂ© ou cas d’utilisation.

Pour une architecture moderne, orientée vers la flexibilité et la capacité à évoluer rapidement (comme l’architecture hexagonale), le Package by Feature est recommandé, car il garantit une meilleure séparation des préoccupations et facilite la transformation de fonctionnalités en services autonomes.

Un Exemple de Structure des Packages pour le cas d’utilisation “user”

domain/
├── common/
│   └── exceptions/
│       ├── BusinessRuleViolationException.java
│       └── ResourceNotFoundException.java
│   
└── user/
    ├── domain/
    │   └── User.java
    ├── port/
    │   ├── inbound/
    │   │   └── UserApiPort.java
    │   └── outbound/
    │       └── UserSpiPort.java
    └── service/
        └── UserApiService.java
Ascii

DĂ©tails des Classes et Interfaces

  1. Package domain.common.exceptions :
    • Le package contient des exceptions mĂ©tier communes pour signaler des violations de règles ou l’absence de ressources, distinctes des exceptions techniques.
    • L’objectif est de centraliser ces exceptions pour maintenir la cohĂ©rence et l’encapsulation du domaine.
  2. Package domain.user :
    • Le package domain.user regroupe l’ensemble des Ă©lĂ©ments liĂ©s au domaine mĂ©tier “user”. En isolant toutes les classes, interfaces, et services pertinents dans ce package unique, plusieurs avantages sont obtenus :

      • FacilitĂ© d’Identification : Le package domain.user permet de regrouper tout ce qui est liĂ© au domaine “user” en un seul endroit. Cela simplifie la comprĂ©hension et la navigation dans le code, car il est facile de repĂ©rer les composants associĂ©s Ă  cette entitĂ© mĂ©tier.

      • ModularitĂ© et RĂ©utilisabilitĂ© : En isolant le package domain.user, celui-ci devient modulaire. Cela facilite l’extensibilitĂ© du système, car de nouveaux comportements et services spĂ©cifiques Ă  user peuvent ĂŞtre ajoutĂ©s sans impacter les autres parties du domaine.

      • FacilitĂ© de DĂ©placement et Maintenance : Puisque le package domain.user est isolĂ©, il peut facilement ĂŞtre dĂ©placĂ©, restructurĂ©, ou mĂŞme extrait vers un autre projet. Par exemple, si l’entitĂ© user devait ĂŞtre externalisĂ©e en tant que microservice indĂ©pendant, il serait relativement simple de le faire car toutes les classes et interfaces liĂ©es sont dĂ©jĂ  bien encapsulĂ©es dans un package unique.

      • CohĂ©rence du Contexte MĂ©tier : Regrouper toutes les parties liĂ©es Ă  user dans un seul package permet de prĂ©server la cohĂ©rence du contexte mĂ©tier. Tous les objets, services, ports (inbound et outbound) restent encapsulĂ©s dans un seul contexte, ce qui aide Ă  Ă©viter les dĂ©pendances circulaires et Ă  garantir une sĂ©paration claire des prĂ©occupations.

  3. Package domain.user.port.inbound :
    • Le package des ports inbound contient des interfaces dĂ©finissant les cas d’utilisation exposĂ©s aux adaptateurs externes.
    • Ces interfaces servent de contrat entre les couches externes et la logique mĂ©tier, dĂ©crivant les opĂ©rations fonctionnelles du domaine sans exposer sa logique interne.
  4. Package domain.user.port.outbound :
    • Les ports outbound dĂ©finissent des interfaces techniques permettant au domaine d’accĂ©der aux systèmes externes (bases de donnĂ©es, services tiers, etc.).
    • Ils dĂ©lèguent les tâches techniques tout en maintenant l’indĂ©pendance du domaine vis-Ă -vis des technologies sous-jacentes, assurant ainsi la flexibilitĂ© de l’infrastructure.
  5. Package domain.user.service :
    • Le package des services contient les implĂ©mentations mĂ©tier qui orchestrent les opĂ©rations des ports inbound et outbound.
    • Ces services implĂ©mentent les interfaces inbound, assurent la logique mĂ©tier et dĂ©lèguent les opĂ©rations techniques aux ports outbound.

Cette organisation permet de structurer le code en respectant les principes de séparation des préoccupations et découplage entre les couches métier et techniques, garantissant ainsi une architecture modulaire et facilement maintenable.


Conclusion - Au-delà de l’Hexagone

L’architecture hexagonale, avec ses principes de découplage et de séparation des responsabilités, offre un cadre robuste et évolutif pour gérer la complexité d’une application moderne. Cependant, au-delà de ces choix techniques, d’autres dimensions de l’architecture logicielle méritent d’être explorées.

L’une des étapes naturelles après avoir maîtrisé l’architecture hexagonale est d’envisager la gestion de l’infrastructure. En effet, le découplage entre le domaine et l’infrastructure ouvre la porte à de nombreuses stratégies d’implémentation techniques : cloud computing, déploiement en conteneurs, microservices…

Chaque approche apporte ses propres défis et opportunités. Le passage à des architectures comme les microservices soulève également des questions sur la gestion de la distribution des services, la résilience et les compromis entre modularité et complexité opérationnelle.

Au-delà de l’infrastructure, d’autres architectures peuvent également être considérées.

Par exemple, l’architecture en couches reste une option viable pour les applications plus simples, où la séparation stricte entre le domaine et l’infrastructure n’est pas nécessaire. De même, les approches event-driven ou CQRS (Command Query Responsibility Segregation) se concentrent sur la gestion des événements et la scalabilité des applications complexes, avec des modèles d’implémentation souvent très différents mais complémentaires à l’architecture hexagonale.

Enfin, le choix des outils et des frameworks pour soutenir cette architecture doit être continuellement réévalué.

En conclusion, l’architecture hexagonale n’est qu’une pièce du puzzle. Elle offre une base solide, mais doit être constamment réfléchie et adaptée dans un contexte technologique plus large. L’infrastructure, l’outillage et l’intégration d’autres paradigmes architecturaux seront les clés pour construire des systèmes toujours plus évolutifs, résilients et performants.

Jean-Jerome Levy

Ecrit par

Jean-JĂ©rĂ´me LĂ©vy

Consultant DevOps

Professionnel chevronné dans le domaine de l’informatique, cumulant plus de 20 années d’expérience au sein de DSI de grandes entreprises, mon expertise diversifiée m’a permis de jouer un rôle clé dans de nombreux projets, caractérisés par la mise en place de pratiques DevOps innovantes.