Software Architecture

Découpler votre code : Un guide pratique de l'architecture hexagonale et en couches

À mesure que les applications deviennent plus complexes, la structure traditionnelle de « code spaghetti » devient ingérable. La logique métier s'entremêle avec les frameworks d'interface utilisateur et les requêtes de base de données, rendant les tests cauchemardesques et les modifications difficiles. Voici l'architecture hexagonale (également connue sous le nom de Ports et Adaptateurs) et l'architecture en couches. Ces modèles fournissent une feuille de route robuste pour organiser le code, garantissant que vos règles métier fondamentales restent indépendantes des préoccupations externes telles que les bases de données, les serveurs web ou les interfaces utilisateur.

Le principe fondamental : L'inversion des dépendances

La règle d'or de ces deux architectures est simple : les dépendances pointent vers l'intérieur. La couche la plus interne contient votre logique métier pure, tandis que les couches externes contiennent les détails techniques. Il s'agit d'une application du principe d'inversion des dépendances (DIP). Les modules de haut niveau ne doivent pas dépendre des modules de bas niveau ; tous deux doivent dépendre d'abstractions.

Dans une architecture en couches standard, nous pourrions voir des couches telles que Présentation, Métier et Données. Cependant, l'architecture hexagonale va plus loin en séparant explicitement le « domaine » (logique centrale) de l'« infrastructure » (outils externes). Cela vous permet de remplacer une base de données MySQL par PostgreSQL, ou une API REST par gRPC, sans toucher à vos règles métier fondamentales.

Ports et Adaptateurs : Le mécanisme d'interaction

L'architecture hexagonale tire son nom de la forme du système : le cœur est un hexagone, et les ports sont les interfaces qui permettent aux acteurs externes d'interagir avec lui.

  • Ports : Interfaces définies par l'application, et non par l'infrastructure. Elles définissent ce que le système peut faire (par exemple, UserRepository ou PaymentGateway).
  • Adaptateurs : Implémentations de ces ports. Ils traduisent les requêtes externes en appels au port et formatent les réponses renvoyées vers le monde extérieur (par exemple, JpaUserRepository ou StripePaymentAdapter).

Exemple pratique : L'injection de dépendances

Considérons un cas d'utilisation simple : l'enregistrement d'un utilisateur. Dans un système fortement couplé, la classe de service instancierait directement un MysqlUserDAO. Dans une conception hexagonale, le service dépend d'une interface.

// Le Port (Interface) - Défini dans la couche Core/Domain
public interface UserRepository {
    User save(User user);
    Optional<User> findByEmail(String email);
}

// Le Service d'application - Également dans la couche Core
public class RegistrationService {
    private final UserRepository userRepository;

    // Injection de dépendance via le constructeur
    public RegistrationService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    public void registerUser(String email, String password) {
        if (userRepository.findByEmail(email).isPresent()) {
            throw new IllegalArgumentException("Email already exists");
        }
        User user = new User(email, password);
        userRepository.save(user);
    }
}

Remarquez que RegistrationService ne sait rien des bases de données, du SQL ou du JSON. Il ne connaît que l'interface UserRepository. Cela rend les tests unitaires trivial. Vous pouvez injecter une implémentation fictive (mock) pendant les tests.

// Exemple de test
@Test
public void testRegistration() {
    UserRepository mockRepo = mock(UserRepository.class);
    RegistrationService service = new RegistrationService(mockRepo);
    
    service.registerUser("test@example.com", "password");
    
    verify(mockRepo).save(any(User.class));
}

Avantages de cette approche

  1. Testabilité : La logique centrale peut être testée de manière isolée sans démarrer un serveur ou se connecter à une vraie base de données.
  2. Flexibilité : Vous pouvez modifier les implémentations technologiques (comme changer de files d'attente de messages) sans refactoriser la logique métier.
  3. Clarté : Il devient immédiatement évident où résident les règles métier par rapport aux détails techniques.

Conclusion

L'adoption d'une architecture en couches ou hexagonale nécessite un changement initial d'état d'esprit et plus de code boilerplate (interfaces et adaptateurs). Cependant, les avantages à long terme en termes de maintenabilité, de testabilité et d'évolutivité sont significatifs. En appliquant strictement l'inversion des dépendances et en séparant les ports des adaptateurs, vous construisez des systèmes résilients face aux changements et plus faciles à comprendre et à modifier pour votre équipe au fil du temps.

Share: