Développement piloté par le domaine (DDD)

English version

Guide complet du développement piloté par le domaine (DDD) : une approche de développement logiciel qui consiste à construire le logiciel autour des domaines métier et de leurs modèles. Découvrez les concepts fondamentaux, les patrons de conception stratégique, les patrons tactiques et les stratégies de mise en œuvre concrètes.

Table des matières

1. Qu'est-ce que le développement piloté par le domaine ?

Le développement piloté par le domaine (Domain-Driven Development, DDD) est une approche de développement logiciel introduite par Eric Evans dans son ouvrage « Domain-Driven Design: Tackling Complexity in the Heart of Software ». Le DDD vise à construire un logiciel qui reflète une compréhension approfondie du domaine métier, en mettant l'accent sur la collaboration entre experts techniques et experts du domaine afin de créer un modèle partagé de l'espace problème.

Au cœur du DDD se trouve le modèle de domaine, placé au centre du processus de conception logicielle. Plutôt que de se concentrer principalement sur les préoccupations techniques, le DDD encourage les développeurs à comprendre en profondeur le domaine métier et à exprimer cette compréhension dans le code. Le modèle de domaine devient une représentation vivante des concepts, règles et processus métier.

Le DDD est particulièrement pertinent pour les domaines métier complexes, où l'espace problème est riche et nuancé. Il aide les équipes à éviter le piège courant des modèles de domaine anémiques qui ne servent que de conteneurs de données, en favorisant des modèles riches qui encapsulent la logique et le comportement métier.

2. Pourquoi utiliser le DDD ?

  • Alignement avec le métier : le DDD garantit que le logiciel reflète étroitement les besoins et la terminologie métier, ce qui réduit l'écart entre ce que les parties prenantes souhaitent et ce que les développeurs livrent.
  • Gestion des domaines complexes : pour les domaines métier complexes, le DDD fournit des patrons et des techniques pour maîtriser la complexité de manière efficace.
  • Maintenabilité : des modèles de domaine bien conçus sont plus faciles à comprendre et à faire évoluer lorsque les exigences métier changent.
  • Collaboration d'équipe : le langage ubiquitaire favorise une meilleure communication entre développeurs et experts du domaine.
  • Testabilité : des modèles de domaine riches aux frontières claires facilitent l'écriture de tests significatifs.
  • Scalabilité : les patrons de conception stratégique aident à organiser de grandes bases de code et à permettre un travail indépendant des équipes.

3. Concepts fondamentaux

3.1 Domaine

Le domaine est la sphère de connaissances ou d'activité autour de laquelle gravite la logique métier. Il représente l'espace problème que le logiciel adresse. Par exemple, dans un système de commerce en ligne, le domaine inclut des concepts tels que les commandes, les produits, les clients, les paiements et l'expédition.

Comprendre le domaine exige une collaboration étroite avec les experts du domaine — des personnes qui maîtrisent le métier en profondeur, comme les chefs de produit, les analystes métier ou les spécialistes du sujet. Le domaine ne concerne pas la technologie ; il concerne le métier lui-même.

3.2 Langage ubiquitaire

Le langage ubiquitaire (Ubiquitous Language) est un vocabulaire commun partagé par les développeurs et les experts du domaine. C'est la langue utilisée dans le code, la documentation et les échanges sur le domaine. Ce langage partagé élimine les erreurs de traduction et garantit que chacun comprend les mêmes concepts.

Exemple : dans un domaine bancaire, des termes comme « Compte », « Transaction », « Solde » et « Découvert » doivent signifier la même chose, que ce soit un banquier qui les prononce ou le code qui les exprime. Si un développeur utilise « AccountBalance » alors que l'expert métier parle de « Solde », ce décalage crée de la confusion.

// Bon : utilise le langage ubiquitaire
public class Account {
    private Balance balance;
    
    public void applyTransaction(Transaction transaction) {
        // Logique métier ici
    }
}

// Mauvais : termes techniques qui ne correspondent pas au langage du domaine
public class AccountEntity {
    private BigDecimal accountBalanceValue;
    
    public void processTransactionRecord(TransactionRecord record) {
        // Décalage avec la terminologie du domaine
    }
}

3.3 Contexte limité

Un contexte limité (Bounded Context) est une frontière explicite au sein de laquelle un modèle de domaine est valide. Différents contextes limités peuvent avoir des modèles différents pour un même concept. Par exemple, « Client » dans le contexte Ventes peut différer de « Client » dans le contexte Expédition.

Les contextes limités aident à gérer la complexité en permettant aux équipes de travailler indépendamment sur différentes parties du système sans modèles conflictuels. Chaque contexte possède son propre langage ubiquitaire et son propre modèle de domaine.

graph TB subgraph "Contexte limité Ventes" A[Modèle Client
- Nom
- E-mail
- Historique d'achats] end subgraph "Contexte limité Expédition" B[Modèle Client
- Nom
- Adresse
- Préférences de livraison] end subgraph "Contexte limité Facturation" C[Modèle Client
- Nom
- Moyen de paiement
- Adresse de facturation] end style A fill:#e1f5ff,stroke:#0273bd,stroke-width:2px style B fill:#fff4e1,stroke:#f57c00,stroke-width:2px style C fill:#e8f5e9,stroke:#2e7d32,stroke-width:2px

3.4 Modèle de domaine

Le modèle de domaine est une abstraction qui représente les concepts du domaine et leurs relations. Ce n'est pas seulement un modèle de données ; c'est un modèle riche qui inclut à la fois les données et le comportement. Le modèle de domaine doit capturer les règles métier, les invariants et les processus.

Un modèle de domaine riche encapsule la logique métier au sein des objets du domaine, plutôt que de la disperser dans des couches de services. Le modèle devient ainsi plus expressif et plus facile à comprendre.

4. Patrons de conception stratégique

Les patrons de conception stratégique aident à organiser les grands systèmes et à gérer la complexité à un niveau élevé. Ils portent sur la manière dont les différentes parties du système sont reliées entre elles.

4.1 Contexte limité

Comme indiqué précédemment, les contextes limités définissent des frontières explicites pour les modèles de domaine. Ils constituent l'unité organisationnelle principale en DDD.

4.2 Cartographie des contextes

La cartographie des contextes (Context Mapping) est une technique pour visualiser les relations entre contextes limités. Elle aide à identifier les patrons d'intégration et les conflits potentiels.

Relations courantes :

  • Noyau partagé (Shared Kernel) : deux contextes partagent un sous-ensemble commun du modèle de domaine.
  • Client-Fournisseur (Customer-Supplier) : un contexte (client) dépend d'un autre (fournisseur).
  • Conformiste (Conformist) : un contexte adopte le modèle d'un autre sans le modifier.
  • Couche anticorruption (Anticorruption Layer) : un contexte se protège du modèle d'un autre contexte.
  • Voies séparées (Separate Ways) : les contextes sont indépendants et ne s'intègrent pas.
  • Service hôte ouvert (Open Host Service) : un contexte fournit un protocole permettant aux autres d'accéder à ses fonctionnalités.
graph LR A[Order Context] -->|Customer-Supplier| B[Payment Context] A -->|Customer-Supplier| C[Shipping Context] D[Legacy System] -->|Anticorruption Layer| A E[Inventory Context] -->|Open Host Service| A style A fill:#e1f5ff,stroke:#0273bd,stroke-width:2px style B fill:#fff4e1,stroke:#f57c00,stroke-width:2px style C fill:#fff4e1,stroke:#f57c00,stroke-width:2px style D fill:#fce4ec,stroke:#c2185b,stroke-width:2px style E fill:#e8f5e9,stroke:#2e7d32,stroke-width:2px

4.3 Événements de domaine

Les événements de domaine (Domain Events) représentent quelque chose de significatif qui s'est produit dans le domaine. Ils servent à communiquer entre contextes limités et à maintenir une cohérence éventuelle.

public class OrderPlacedEvent {
    private final OrderId orderId;
    private final CustomerId customerId;
    private final Money totalAmount;
    private final Instant occurredOn;
    
    // Constructeur, accesseurs...
}

// Publication des événements de domaine
public class Order {
    private List<DomainEvent> domainEvents = new ArrayList<>();
    
    public void place() {
        // Logique métier
        this.status = OrderStatus.PLACED;
        domainEvents.add(new OrderPlacedEvent(this.id, this.customerId, this.total, Instant.now()));
    }
    
    public List<DomainEvent> getDomainEvents() {
        return Collections.unmodifiableList(domainEvents);
    }
}

5. Patrons de conception tactique

Les patrons tactiques fournissent les briques de base pour implémenter des modèles de domaine au sein d'un contexte limité. Ils aident à créer des modèles de domaine riches et expressifs.

5.1 Entité

Une entité (Entity) est un objet doté d'une identité unique qui persiste dans le temps, même si ses attributs changent. Les entités sont identifiées par leur identifiant, et non par leurs attributs.

public class Order {
    private OrderId id;  // Identité unique
    private CustomerId customerId;
    private OrderStatus status;
    private List<OrderItem> items;
    
    // Égalité basée sur l'identité
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Order order = (Order) o;
        return Objects.equals(id, order.id);
    }
    
    @Override
    public int hashCode() {
        return Objects.hash(id);
    }
}

5.2 Objet valeur

Un objet valeur (Value Object) est un objet défini par ses attributs plutôt que par une identité. Les objets valeur sont immuables et comparés par valeur. Des exemples incluent Money, Address et Email.

public class Money {
    private final BigDecimal amount;
    private final Currency currency;
    
    public Money(BigDecimal amount, Currency currency) {
        if (amount == null || currency == null) {
            throw new IllegalArgumentException("Amount and currency are required");
        }
        this.amount = amount;
        this.currency = currency;
    }
    
    public Money add(Money other) {
        if (!this.currency.equals(other.currency)) {
            throw new IllegalArgumentException("Cannot add different currencies");
        }
        return new Money(this.amount.add(other.amount), this.currency);
    }
    
    // Égalité basée sur la valeur
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Money money = (Money) o;
        return Objects.equals(amount, money.amount) && 
               Objects.equals(currency, money.currency);
    }
    
    @Override
    public int hashCode() {
        return Objects.hash(amount, currency);
    }
}

5.3 Agrégat

Un agrégat (Aggregate) est un ensemble d'objets liés traité comme une unité unique. Il possède une entité racine (racine d'agrégat, Aggregate Root) qui contrôle l'accès aux objets internes de l'agrégat. Les agrégats définissent des frontières de cohérence.

// Racine d'agrégat
public class Order {
    private OrderId id;
    private CustomerId customerId;
    private List<OrderItem> items;  // Fait partie de l'agrégat
    private OrderStatus status;
    
    // Logique métier qui préserve les invariants
    public void addItem(ProductId productId, Quantity quantity, Money unitPrice) {
        if (this.status != OrderStatus.DRAFT) {
            throw new IllegalStateException("Cannot modify placed order");
        }
        
        OrderItem item = new OrderItem(productId, quantity, unitPrice);
        this.items.add(item);
    }
    
    public void place() {
        if (this.items.isEmpty()) {
            throw new IllegalStateException("Cannot place empty order");
        }
        this.status = OrderStatus.PLACED;
        // Publier un événement de domaine...
    }
    
    // Seule la racine d'agrégat expose des méthodes publiques
    public Money getTotal() {
        return items.stream()
            .map(OrderItem::getSubtotal)
            .reduce(Money.ZERO, Money::add);
    }
}

5.4 Service de domaine

Un service de domaine (Domain Service) contient une logique métier qui ne s'intègre pas naturellement dans une entité ou un objet valeur. C'est un service sans état qui opère sur des objets du domaine.

public class OrderPricingService {
    public Money calculateTotal(Order order, Customer customer) {
        Money subtotal = order.getSubtotal();
        Money discount = calculateDiscount(customer, subtotal);
        Money tax = calculateTax(subtotal, customer.getAddress());
        Money shipping = calculateShipping(order, customer.getAddress());
        
        return subtotal
            .subtract(discount)
            .add(tax)
            .add(shipping);
    }
    
    private Money calculateDiscount(Customer customer, Money amount) {
        // Logique complexe de calcul de remise
        // Elle n'appartient ni à Order ni à Customer
    }
}

5.5 Dépôt (Repository)

Un dépôt (Repository) fournit une abstraction pour accéder aux agrégats. Il encapsule la logique nécessaire pour récupérer et persister les agrégats, en séparant la logique du domaine des préoccupations de persistance.

public interface OrderRepository {
    void save(Order order);
    Order findById(OrderId id);
    List<Order> findByCustomerId(CustomerId customerId);
    void remove(Order order);
}

// Implémentation (couche infrastructure)
@Repository
public class JpaOrderRepository implements OrderRepository {
    @PersistenceContext
    private EntityManager entityManager;
    
    @Override
    public void save(Order order) {
        entityManager.persist(order);
        // Gérer les événements de domaine...
    }
    
    @Override
    public Order findById(OrderId id) {
        return entityManager.find(Order.class, id);
    }
}

5.6 Fabrique (Factory)

Une fabrique (Factory) encapsule la logique complexe de création d'objets. Les fabriques sont utiles lorsque la création d'agrégats exige une initialisation complexe ou lorsque le processus de création doit rester masqué.

public class OrderFactory {
    private final ProductRepository productRepository;
    private final PricingService pricingService;
    
    public Order createOrder(CustomerId customerId, List<OrderLineRequest> lineRequests) {
        Order order = new Order(OrderId.generate(), customerId);
        
        for (OrderLineRequest request : lineRequests) {
            Product product = productRepository.findById(request.getProductId());
            Money unitPrice = pricingService.getPrice(product, customerId);
            order.addItem(product.getId(), request.getQuantity(), unitPrice);
        }
        
        return order;
    }
}

6. Exemples de mise en œuvre

6.1 Exemple complet d'agrégat Commande

// Objet valeur : OrderId
public class OrderId {
    private final String value;
    
    private OrderId(String value) {
        this.value = value;
    }
    
    public static OrderId of(String value) {
        return new OrderId(value);
    }
    
    public static OrderId generate() {
        return new OrderId(UUID.randomUUID().toString());
    }
    
    // equals, hashCode, toString...
}

// Objet valeur : Money
public class Money {
    private final BigDecimal amount;
    private final Currency currency;
    
    public static final Money ZERO = new Money(BigDecimal.ZERO, Currency.USD);
    
    public Money(BigDecimal amount, Currency currency) {
        this.amount = amount;
        this.currency = currency;
    }
    
    public Money add(Money other) {
        validateSameCurrency(other);
        return new Money(this.amount.add(other.amount), this.currency);
    }
    
    // Autres méthodes...
}

// Entité : OrderItem (partie de l'agrégat Order)
public class OrderItem {
    private ProductId productId;
    private Quantity quantity;
    private Money unitPrice;
    
    public OrderItem(ProductId productId, Quantity quantity, Money unitPrice) {
        this.productId = productId;
        this.quantity = quantity;
        this.unitPrice = unitPrice;
    }
    
    public Money getSubtotal() {
        return unitPrice.multiply(quantity.getValue());
    }
}

// Racine d'agrégat : Order
public class Order {
    private OrderId id;
    private CustomerId customerId;
    private List<OrderItem> items = new ArrayList<>();
    private OrderStatus status;
    private List<DomainEvent> domainEvents = new ArrayList<>();
    
    public Order(OrderId id, CustomerId customerId) {
        this.id = id;
        this.customerId = customerId;
        this.status = OrderStatus.DRAFT;
    }
    
    public void addItem(ProductId productId, Quantity quantity, Money unitPrice) {
        if (status != OrderStatus.DRAFT) {
            throw new IllegalStateException("Cannot modify order in status: " + status);
        }
        items.add(new OrderItem(productId, quantity, unitPrice));
    }
    
    public void place() {
        if (items.isEmpty()) {
            throw new IllegalStateException("Cannot place empty order");
        }
        this.status = OrderStatus.PLACED;
        domainEvents.add(new OrderPlacedEvent(id, customerId, getTotal(), Instant.now()));
    }
    
    public Money getTotal() {
        return items.stream()
            .map(OrderItem::getSubtotal)
            .reduce(Money.ZERO, Money::add);
    }
    
    public List<DomainEvent> getDomainEvents() {
        return Collections.unmodifiableList(domainEvents);
    }
    
    public void clearDomainEvents() {
        domainEvents.clear();
    }
}

6.2 Exemple de service applicatif

@Service
public class OrderApplicationService {
    private final OrderRepository orderRepository;
    private final ProductRepository productRepository;
    private final DomainEventPublisher eventPublisher;
    
    @Transactional
    public OrderId createOrder(CreateOrderCommand command) {
        Order order = new Order(OrderId.generate(), command.getCustomerId());
        
        for (OrderLineCommand line : command.getLines()) {
            Product product = productRepository.findById(line.getProductId());
            order.addItem(product.getId(), line.getQuantity(), product.getPrice());
        }
        
        orderRepository.save(order);
        
        // Publier les événements de domaine
        order.getDomainEvents().forEach(eventPublisher::publish);
        order.clearDomainEvents();
        
        return order.getId();
    }
    
    @Transactional
    public void placeOrder(OrderId orderId) {
        Order order = orderRepository.findById(orderId);
        order.place();
        orderRepository.save(order);
        
        order.getDomainEvents().forEach(eventPublisher::publish);
        order.clearDomainEvents();
    }
}

7. Bonnes pratiques

7.1 Conserver la logique métier dans la couche domaine

La logique métier doit résider dans la couche domaine, et non dans les services applicatifs ou l'infrastructure. Les services applicatifs orchestrent ; les objets du domaine portent les règles métier.

7.2 Utiliser des objets valeur pour l'obsession des primitives

Évitez d'utiliser des primitives partout. Créez des objets valeur pour les concepts du domaine comme Email, Money, Address, etc. Le code devient plus expressif et les erreurs sont réduites.

7.3 Maintenir la cohérence des agrégats

Les agrégats doivent toujours être dans un état cohérent. Tous les invariants doivent être appliqués à l'intérieur de la frontière de l'agrégat. Utilisez les événements de domaine pour la communication entre agrégats.

7.4 Garder des agrégats de taille modeste

Les grands agrégats sont difficiles à maintenir et peuvent poser des problèmes de performance. Gardez les agrégats ciblés et compacts. Si un agrégat devient trop volumineux, envisagez de le scinder.

7.5 Utiliser les événements de domaine pour l'intégration

Utilisez les événements de domaine pour communiquer entre contextes limités et agrégats. Cela préserve un couplage faible et permet une cohérence éventuelle.

7.6 Affinement continu

Le modèle de domaine doit évoluer à mesure que la compréhension s'approfondit. N'hésitez pas à refactoriser le modèle lorsque vous en apprenez davantage sur le domaine.

8. Pièges courants

8.1 Modèle de domaine anémique

Un modèle de domaine anémique comporte des entités qui ne sont que des conteneurs de données avec des accesseurs et mutateurs. La logique métier se retrouve dans des classes de service, ce qui rend le modèle de domaine moins expressif.

// Mauvais : modèle de domaine anémique
public class Order {
    private OrderId id;
    private List<OrderItem> items;
    private OrderStatus status;
    
    // Uniquement des accesseurs et mutateurs
    public void setStatus(OrderStatus status) {
        this.status = status;
    }
}

// Bon : modèle de domaine riche
public class Order {
    private OrderId id;
    private List<OrderItem> items;
    private OrderStatus status;
    
    public void place() {
        if (items.isEmpty()) {
            throw new IllegalStateException("Cannot place empty order");
        }
        this.status = OrderStatus.PLACED;
    }
}

8.2 Agrégats « dieu »

Créer des agrégats trop volumineux qui concentrent trop de responsabilités. Ils deviennent difficiles à maintenir et peuvent provoquer des problèmes de concurrence.

8.3 Abstractions qui fuient

Laisser les préoccupations d'infrastructure s'infiltrer dans la couche domaine. Le domaine ne doit pas dépendre de frameworks ou de bases de données.

8.4 Ignorer les contextes limités

Tenter de créer un modèle unifié unique pour l'ensemble du système. Différents contextes peuvent exiger des modèles différents pour un même concept.

9. Quand utiliser le DDD

Le DDD n'est pas adapté à tous les projets. Envisagez le DDD lorsque :

  • Domaine métier complexe : la logique métier est complexe et exige une connaissance approfondie du domaine.
  • Projets de longue durée : le système évoluera dans le temps et devra s'adapter aux exigences métier changeantes.
  • Collaboration d'équipes : plusieurs équipes travaillent sur le système et ont besoin de frontières claires.
  • Expertise métier disponible : vous avez accès à des experts du domaine capables de collaborer sur le modèle.

Le DDD peut être excessif pour :

  • les applications CRUD simples ;
  • les applications centrées sur les données avec peu de logique métier ;
  • les projets à court terme ;
  • les projets sans complexité métier significative.

10. Conclusion

Le développement piloté par le domaine offre une approche puissante pour construire un logiciel étroitement aligné sur les besoins métier. En se concentrant sur le domaine, en utilisant le langage ubiquitaire et en appliquant les patrons stratégiques et tactiques, les équipes peuvent créer un logiciel maintenable et expressif qui évolue avec les exigences métier.

La clé d'un DDD réussi réside dans la collaboration entre développeurs et experts du domaine, l'affinement continu du modèle de domaine et l'application disciplinée des patrons DDD. Bien que le DDD demande un investissement en apprentissage et en pratique, il rapporte largement dans les domaines complexes où l'alignement avec le métier est critique.

Rappelez-vous que le DDD n'est pas une solution miracle — c'est un ensemble de principes et de patrons à appliquer avec discernement selon votre contexte et vos besoins. Commencez modestement, apprenez en continu et affinez votre approche à mesure que vous gagnez de l'expérience en conception pilotée par le domaine.

Résumé : Développement piloté par le domaine (DDD)

Problème / besoin : Gérer la complexité des systèmes métiers en alignant la structure logicielle sur les règles, concepts et processus du domaine métier afin de réduire le couplage et les incohérences fonctionnelles.

Solution proposée : Décomposer le système en Bounded Contexts autonomes représentant chacun un sous-domaine métier cohérent, avec un modèle métier riche, un langage omniprésent (Ubiquitous Language) et des frontières explicites entre les composants.

Principe d'implémentation : Le DDD est généralement implémenté via une architecture en couches ou hexagonale, souvent associée à des microservices ou des modules indépendants, où chaque contexte possède ses entités, agrégats, repositories, événements métier et parfois sa propre base de données.



Post a Comment

0 Comments