Aller au contenu

Démarche de conception Domain-Driven Design (DDD)

Introduction

Dans une application métier, la difficulté ne vient pas toujours du nombre de classes ou du volume de données. Parfois, elle vient des règles métier complexes à comprendre : quand une commande peut-elle être validée ? Qui peut modifier un tarif ? Que signifie le terme « client » pour la facturation et pour le support ; ce terme a-t-il la même signification pour ces deux contextes ?

L'approche de conception logicielle Domain-Driven Design, aussi appelée DDD, aide l’équipe à construire un logiciel autour de ce genre de questions. Elle permet de lier entre elles, la compréhension du métier, le vocabulaire employé et la structure du code.

Présentation

Le Domain-Driven Design est une approche de conception logicielle centrée sur le domaine métier. L’équipe en charge de la conception et du développement du logiciel échange avec les personnes qui connaissent le métier, identifie les règles importantes, puis construit des modèles qui rendent ces règles explicites au sein du logiciel. Elle ne constitue pas une architecture à part entière ; elle peut être utilisée au sein d'une application conçue avec une autre architecture compatible (monolithe modulaire, architecture hexagonale, Onion Architecture, etc.).

Le DDD comporte deux dimensions complémentaires :

  • La conception stratégique, qui délimite les sous-domaines et les contextes délimités (bounded contexts). Un même terme peut avoir une signification ou des règles différentes selon le contexte.
  • La conception tactique, qui aide à modéliser les règles à l’intérieur d’un contexte, notamment avec des entités, des objets valeur et des agrégats.

Approche de conception logicielle Domain-Driven Design - Présentation

Les notions essentielles

Le langage partagé

Les développeurs et les experts métier emploient les mêmes termes dans les discussions, les règles et, autant que possible, dans le code. Si « commande validée » signifie qu’un paiement a été accepté, cette distinction doit apparaître clairement dans le modèle. Ce vocabulaire évolue au fur et à mesure que l’équipe comprend mieux le métier.

Les contextes délimités

Un contexte délimité fixe la portée d’un modèle. Par exemple, un Client peut représenter un titulaire de contrat dans la facturation et un interlocuteur dans le support. Chaque contexte définit les données et les règles dont il a besoin ; les échanges entre les contextes sont réalisés au travers de contrats explicites.

Les entités et les objets valeur

Une entité est un élément métier identifié de manière unique. Son identité reste la même lorsque ses propriétés évoluent. Par exemple, une commande conserve son identité après une modification de son adresse de livraison.

Un objet valeur est défini par ses valeurs, sans identité métier propre. Un montant et sa devise peuvent, par exemple, former un objet valeur Amount. Cette distinction dépend toutefois du contexte. Par exemple, une adresse peut être une propriété dans une commande ou une entité avec ses propres caractéristiques.

Les agrégats et leurs invariants

Un agrégat regroupe des éléments métier qui doivent rester cohérents entre eux. Sa racine constitue le point d’entrée des modifications et protège ses invariants, c’est-à-dire les règles qui doivent être respectées après chaque opération métier.

Un exemple simple en C#

Dans un domaine de commandes, une règle peut interdire la validation d’une commande vide. La classe métier peut porter cette décision directement :

public class Order
{
    private readonly List<OrderLine> _lines;

    public Guid Uid { get; }
    public bool IsValidated { get; private set; }
    public IReadOnlyCollection<OrderLine> Lines => _lines.AsReadOnly();

    public Order(Guid uid)
    {
        Uid = uid;
        IsValidated = false;
        _lines = [];
    }

    public void AddLine(OrderLine line)
    {
        if (IsValidated)
            throw new InvalidOperationException(
                "A validated order can no longer be modified.");

        _lines.Add(line);
    }

    public void Validate()
    {
        if (IsValidated)
            throw new InvalidOperationException(
                "The order is already validated.");

        if (_lines.Count == 0)
            throw new InvalidOperationException(
                "An empty order cannot be validated.");

        IsValidated = true;
    }
}

public record OrderLine(Guid ProductUid, int Quantity);

Dans l'exemple ci-dessus, Le record OrderLine est un objet valeur défini par l'identifiant du produit référencé et la quantité commandée. Il ne possède pas d’identité propre. La classe Order est la racine de l’agrégat : elle décide si ses lignes peuvent être modifiées et si la commande peut être validée. Dans une application, il faudrait également vérifier les règles propres aux lignes, comme une quantité des produits commandés strictement positive. Les objets métier doivent faire respecter leurs règles pour conserver un état cohérent, même si l’API contrôle déjà les données reçues. Ces règles s’appliquent ainsi quel que soit le composant qui utilise ces objets.

Les avantages

Un modèle proche du métier

Les noms, les opérations et les règles du code reflètent les concepts utilisés par les experts métier.

Des règles regroupées

Les agrégats protègent les invariants et rendent les décisions métier plus faciles à retrouver et à tester.

Des périmètres métier plus explicites

Les contextes délimités réduisent les confusions entre des concepts métier qui ont le même nom mais une signification différente.

Une évolution mieux maîtrisée

Une règle peut être modifiée dans le contexte qui en a la responsabilité, à condition que ses échanges avec les autres contextes soient définis.

Les limites

Un investissement dans la compréhension du métier

Des échanges réguliers avec les experts métier et doit être revu lorsque la compréhension progresse.

Des périmètres métier à bien définir

Des contextes ou des agrégats mal délimités peuvent créer des dépendances gênantes ou compliquer les opérations métier.

Une complexité parfois disproportionnée

Pour une simple application CRUD, multiplier agrégats, services et abstractions, peut entraîner une complexité supérieure aux bénéfices apportés.

Des modèles à faire évoluer

Le vocabulaire et les règles changent ; l’équipe doit accepter de faire évoluer le code lorsque sa compréhension du domaine s’affine.

Bonnes pratiques avec .NET

Partir des cas métier

Nommer les opérations à partir du langage du domaine, puis préciser les règles à garantir avant de choisir les classes et les tables.

Délimiter les contextes

Regrouper les concepts qui partagent le même sens et définir des contrats explicites pour communiquer entre contextes.

Délimiter l'utilisation d'EF Core

Configurer la persistance des données sans laisser les contraintes du schéma dicter toutes les règles métier.

Tester les comportements

Vérifier les transitions autorisées et refusées par le domaine avec des tests unitaires, puis vérifier la persistance des agrégats avec des tests d’intégration utilisant EF Core.

Adapter les objets valeur aux besoins

Les objets valeur regroupent des valeurs et les règles métier associées. Leur enregistrement dans une base de données avec EF Core peut nécessiter une configuration spécifique, selon leur structure et la manière dont les données doivent être stockées.