Flux de Dépendances et Inversion
Le flux de dépendances est le principe fondamental de l’architecture hexagonale : inverser les dépendances pour que tout dépende du domain.
Ce chapitre explique comment les dépendances circulent dans l’architecture hexagonale et pourquoi l’inversion de dépendances est cruciale.
Le Problème des Dépendances Traditionnelles
Dans une architecture classique, le haut dépend du bas, créant un couplage fort et rendant le code difficile à tester et à maintenir.
❌ Architecture Traditionnelle
Dépendances Traditionnelles (Problème)
┌──────────────┐
│ Controller │ ← Couche présentation
└──────┬───────┘
│ depends on (couplage fort)
▼
┌──────────────┐
│ Service │ ← Couche métier
└──────┬───────┘
│ depends on (couplage fort)
▼
┌──────────────┐
│ Repository │ ← Implémentation concrète (MySQL, PostgreSQL, etc.)
└──────┬───────┘
│ accesses
▼
┌──────────────┐
│ Database │ ← Infrastructure technique
└──────────────┘Problèmes majeurs :
❌ Couplage fort - Service dépend directement de l’implémentation du Repository ❌ Impossible de tester - Besoin d’une vraie base de données pour les tests ❌ Difficile de changer - Remplacer MySQL par PostgreSQL casse le code ❌ Framework omniprésent - Le métier dépend de JPA, Hibernate, etc.
Exemple Problématique
// ❌ MAUVAIS : Couplage fort
public class UserService {
// Dépendance directe vers l'implémentation !
private MySQLUserRepository repository = new MySQLUserRepository();
public User getUser(Long id) {
// Impossible de changer de BDD sans casser ce code
return repository.findById(id);
}
}
// Implémentation MySQL concrète
public class MySQLUserRepository {
@PersistenceContext
private EntityManager em; // Dépendance JPA !
public User findById(Long id) {
return em.find(User.class, id);
}
}Conséquences :
- Tests impossibles sans base de données réelle
- Code métier couplé à JPA/Hibernate
- Changement de technologie = réécriture complète
Les 3 Règles d’Or
Ces 3 règles sont NON NÉGOCIABLES dans l’architecture hexagonale.
Règle 1 : Le Domain ne Dépend de RIEN
┌─────────────────────────────────────────┐
│ DOMAIN (Cœur Métier) │
│ ┌───────────────────────────────────┐ │
│ │ Entités, Value Objects, │ │
│ │ Interfaces (Ports de sortie) │ │
│ └───────────────────────────────────┘ │
│ │
│ ❌ AUCUNE dépendance vers : │
│ - Application │
│ - Infrastructure │
│ - Framework (Micronaut, Spring) │
│ - Bibliothèques techniques │
│ │
│ ✅ Seulement : │
│ - Lombok (annotations de code) │
│ - Java standard (Optional, etc.) │
└─────────────────────────────────────────┘Exemple Domain Pur :
// domain/model/User.java
@Value // Lombok uniquement
public class User {
Long id;
String name;
Email email; // Value Object
// Règles métier (pas de dépendance framework)
public boolean isActive() {
return email != null && email.isValid();
}
}
// domain/port/output/UserRepository.java
public interface UserRepository { // Interface pure
Optional<User> findById(Long id);
}Le domain est 100% indépendant - il peut être extrait dans une lib JAR séparée !
Règle 2 : L’Infrastructure Dépend du Domain
┌────────────────────────────────┐
│ INFRASTRUCTURE │
│ │
│ ┌──────────────────────────┐ │
│ │ Adapters │ │
│ │ (MySQL, API, Mock) │ │
│ └───────────┬──────────────┘ │
│ │ │
│ │ implements │
│ │ │
│ ▼ │
│ ┌──────────────────────────┐ │
│ │ Port de Sortie │ │◄───── Défini dans DOMAIN
│ │ (Interface) │ │
│ └──────────────────────────┘ │
└────────────────────────────────┘Exemple :
// domain/port/output/UserRepository.java (défini dans domain)
public interface UserRepository {
Optional<User> findById(Long id);
}
// infrastructure/adapter/MySQLUserRepository.java
@Singleton
public class MySQLUserRepository implements UserRepository { // ← Implémente l'interface du domain
@PersistenceContext
private EntityManager em;
@Override
public Optional<User> findById(Long id) {
// Détails techniques JPA
// Le domain ne sait même pas que MySQL existe !
return Optional.ofNullable(em.find(User.class, id));
}
}L’infrastructure implémente les contrats définis par le domain. Le domain n’a aucune connaissance de l’infrastructure.
Règle 3 : Inversion via Interfaces (Ports)
Les ports (interfaces) sont le mécanisme d’inversion de dépendances.
Port de Sortie (Output)
Port de Sortie (Output Port)
Définition : Interface définie dans le domain et implémentée par l’infrastructure.
// domain/port/output/TrendRepository.java
public interface TrendRepository {
Optional<TrendResult> getTrends(TrendQuery query);
void save(TrendResult result);
}Plusieurs implémentations possibles :
Mock
@Singleton
@Requires(env = "dev")
public class MockTrendRepository implements TrendRepository {
@Override
public Optional<TrendResult> getTrends(TrendQuery query) {
// Données mockées
return Optional.of(new TrendResult(query.getKeyword(), 85));
}
}Le UseCase ne change pas, quel que soit l’adapter utilisé ! Micronaut injecte automatiquement le bon adapter selon la configuration.
Flux de Données Complet
Voyons le flux complet d’une requête HTTP à travers toutes les couches de l’architecture.
Exemple : GET /api/trends?keyword=java®ion=US
1. Requête HTTP arrive
GET /api/trends?keyword=java®ion=US HTTP/1.1
Host: localhost:8080Le client (cURL, Postman, Frontend) envoie une requête HTTP.
2. Port d’Entrée (Controller)
@Controller("/api/trends")
public class TrendController {
private final GetTrendsUseCase getTrendsUseCase;
@Get
public TrendResponseDto getTrends(
@QueryValue @NotBlank String keyword,
@QueryValue(defaultValue = "US") String region
) {
// Validation automatique par Micronaut
// Appel du use case
return getTrendsUseCase.execute(keyword, region)
.map(TrendResponseDto::fromDomain)
.orElseThrow(() -> new ResourceNotFoundException());
}
}Rôle : Valider les paramètres, appeler le use case, convertir la réponse.
3. Use Case (Orchestration)
@Singleton
@RequiredArgsConstructor
public class GetTrendsUseCase {
private final TrendRepository trendRepository; // Interface !
public Optional<TrendResult> execute(String keyword, String region) {
// Créer l'objet domain (validation incluse)
TrendQuery query = new TrendQuery(keyword, region);
// Appeler le port de sortie
return trendRepository.getTrends(query);
}
}Rôle : Orchestrer la logique métier, coordonner les ports de sortie.
4. Port de Sortie (Interface)
// domain/port/output/TrendRepository.java
public interface TrendRepository {
Optional<TrendResult> getTrends(TrendQuery query);
}Rôle : Définir le contrat. Le use case ne sait pas quelle implémentation sera utilisée.
5. Adapter (Implémentation)
@Singleton
public class GoogleTrendsApiAdapter implements TrendRepository {
private final HttpClient httpClient;
@Override
public Optional<TrendResult> getTrends(TrendQuery query) {
// Appel API externe
String url = buildUrl(query);
GoogleApiResponse response = httpClient.toBlocking()
.retrieve(HttpRequest.GET(url), GoogleApiResponse.class);
// Conversion DTO → Domain
TrendResult result = mapToDomain(response);
return Optional.of(result);
}
}Rôle : Gérer les détails techniques (API, BDD, fichiers, etc.).
6. Réponse remonte
Le TrendResult remonte jusqu’au controller :
Adapter → UseCase → Controller → JSON7. Conversion Domain → DTO
public TrendResponseDto getTrends(...) {
return getTrendsUseCase.execute(keyword, region)
.map(TrendResponseDto::fromDomain) // Domain → DTO
.orElseThrow(() -> new ResourceNotFoundException());
}8. Réponse HTTP
{
"keyword": "java",
"region": "US",
"interestScore": 85,
"popularityLevel": "HIGH",
"queriedAt": "2025-01-15T10:30:00"
}À aucun moment le domain n’a dépendu de l’infrastructure ! Le flux de dépendances est inversé.
Schéma Complet des Dépendances
┌─────────────────────────────────────────────────────────────┐
│ MONDE EXTÉRIEUR │
│ (Client HTTP, Frontend, cURL) │
└───────────────────────┬─────────────────────────────────────┘
│ HTTP Request
▼
┌─────────────────────────────────────────────────────────────┐
│ APPLICATION LAYER │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ PORT D'ENTRÉE (Input Port) │ │
│ │ TrendController │ │
│ │ - Valide les paramètres │ │
│ │ - Appelle le use case │ │
│ └────────────────────┬─────────────────────────────────┘ │
│ │ │
│ ┌────────────────────▼─────────────────────────────────┐ │
│ │ USE CASE │ │
│ │ GetTrendsUseCase │ │
│ │ - Orchestre la logique métier │ │
│ │ - Dépend des ports de sortie (interfaces) │ │
│ └────────────────────┬─────────────────────────────────┘ │
└───────────────────────┼─────────────────────────────────────┘
│ depends on
▼
┌─────────────────────────────────────────────────────────────┐
│ DOMAIN LAYER │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ PORT DE SORTIE (Output Port) │ │
│ │ TrendRepository (INTERFACE) │ │
│ │ - Définit le contrat │ │
│ │ - NE DÉPEND DE RIEN │ │
│ └───────────────────▲──────────────────────────────────┘ │
│ │ │
│ ┌──────────────────┐│┌──────────────────────────────────┐ │
│ │ ENTITÉS ││ VALUE OBJECTS │ │
│ │ TrendResult ││ TrendQuery, Email, Address │ │
│ └──────────────────┘│└──────────────────────────────────┘ │
└──────────────────────┼──────────────────────────────────────┘
│ implements (INVERSION !)
▼
┌─────────────────────────────────────────────────────────────┐
│ INFRASTRUCTURE LAYER │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ ADAPTER (Implémentation) │ │
│ │ GoogleTrendsApiAdapter │ │
│ │ - Implémente TrendRepository │ │
│ │ - Gère les détails techniques │ │
│ └────────────────────┬─────────────────────────────────┘ │
└───────────────────────┼─────────────────────────────────────┘
│ accesses
▼
┌─────────────────────────────────────────────────────────────┐
│ RESSOURCES EXTERNES │
│ (API Google Trends, Base de données, etc.) │
└─────────────────────────────────────────────────────────────┘Remarquez : Les flèches de dépendances pointent toutes vers le haut (vers le domain). C’est l’inversion de dépendances !
Organisation des Packages
Convention : Les ports de sortie sont dans domain/port/output/, et les adapters qui les implémentent sont dans infrastructure/adapter/.
Pièges Courants
1. Dépendance Circulaire
Piège 1 : Dépendance Circulaire
Symptôme
❌ Erreur au démarrage :
Circular dependency detected:
domain → infrastructure → domainLe domain dépend de l’infrastructure ET l’infrastructure dépend du domain.
Checklist Inversion de Dépendances
Pour valider que votre architecture respecte l’inversion de dépendances :
- Le domain ne contient aucun import vers
application/ouinfrastructure/ - Les ports de sortie (interfaces) sont dans
domain/port/output/ - Les adapters implémentent les ports de sortie
- Les use cases dépendent des interfaces (pas des implémentations)
- L’injection de dépendances se fait via constructeur (
@RequiredArgsConstructor) - Les tests mockent les interfaces sans infrastructure réelle
- Micronaut injecte automatiquement le bon adapter via
@Singleton - Les DTOs de l’infrastructure ne remontent jamais au domain
Si toutes les cases sont cochées, votre architecture respecte l’inversion de dépendances ! 🎉
Résumé
L’inversion de dépendances est le cœur de l’architecture hexagonale.
| Principe | Description |
|---|---|
| Domain au centre | Ne dépend de RIEN (ni framework, ni infrastructure) |
| Ports = Contrats | Interfaces définissant les besoins du domain |
| Adapters = Implémentations | Détails techniques implémentant les ports |
| Injection de Dépendances | Micronaut injecte automatiquement les adapters |
| Testabilité | Mock des interfaces pour tests ultra rapides |
Prochaines Étapes
Maintenant que vous comprenez le flux de dépendances, explorez les différentes variantes d’organisation.
- Variantes d’Architecture → - 3 approches pour organiser les ports
- Domain Layer → - Structurer le cœur métier
- Infrastructure Layer → - Implémenter les adapters