Domain-Driven Design en C# : comment ne pas se noyer dans la complexité
\n\nLorsque votre projet C# dépasse le stade de la « simple application CRUD », le moment de vérité arrive. La logique métier commence à s'emmêler, les méthodes des services gonflent jusqu'à des centaines de lignes, et le mot « refactoring » provoque un frisson nerveux. Cela vous parle ? Bienvenue dans le monde où, sans Domain-Driven Design (DDD), vous risquez de transformer votre code en marécage.
\n\nLe DDD n'est pas « un framework de plus ». C'est une philosophie qui place le domaine métier au centre de l'univers. Aujourd'hui, nous allons voir comment appliquer le Domain-Driven Design dans des projets C# pour que le code devienne non seulement fonctionnel, mais aussi compréhensible et flexible. Pas de blabla — seulement des exemples concrets et des conseils pratiques.
\n\nPourquoi le DDD sauve les projets C# ?
\n\nImaginez que vous écrivez un système de gestion des commandes pour une boutique en ligne. Si vous mettez toute la logique dans les contrôleurs ASP.NET, dans six mois vous ne pourrez plus expliquer pourquoi la méthode CreateOrder() appelle CalculateShipping(), qui, à son tour, va chercher les taux de change dans la base de données. Le DDD propose une séparation claire : il y a une couche domaine, où vit la logique métier, et une infrastructure, qui s'occupe des détails techniques.
L'idée clé du DDD est l'Ubiquitous Language (langage omniprésent). Vous et les experts métier parlez le même langage. Dans le code, cela se traduit par des termes comme « Order », « LineItem », « DiscountPolicy », et non « OrderDto », « OrderService » ou « Helper ». Plus le code se rapproche du langage métier, plus il est facile à maintenir.
\n\nConception stratégique et tactique
\n\nLe DDD se divise en deux parties : la stratégique (comment diviser le système en modules) et la tactique (comment écrire le code à l'intérieur d'un module). En C#, la conception tactique nous offre quatre blocs de construction puissants :
\n\n- \n
- Entity — un objet avec un identifiant unique (par exemple,
OrderId). \n - Value Object — un objet défini par ses attributs (par exemple,
MoneyouAddress). \n - Aggregate Root — l'entité racine qui gère un groupe d'objets. \n
- Repository — une abstraction d'accès aux données. \n
Voyons à quoi cela ressemble dans le code.
\n\nExemple : concevoir une commande (Order)
\n\nCommençons simplement : nous avons une commande composée de lignes. La commande peut avoir le statut « Nouveau », « Payé » ou « Annulé ».
\n\n1. Value Object : Money
\n\nL'argent est un Value Object classique. Deux objets Money sont égaux s'ils ont le même montant et la même devise. Ils sont immuables.
public sealed record Money(decimal Amount, string Currency)\n{\n public static Money operator +(Money left, Money right)\n {\n if (left.Currency != right.Currency)\n throw new InvalidOperationException("Currency mismatch");\n return new Money(left.Amount + right.Amount, left.Currency);\n }\n}\n\n\nRemarquez le record — en C# 9+, c'est le moyen idéal de créer un Value Object. Il fournit automatiquement Equals, GetHashCode et l'immuabilité.
2. Entity : Order et LineItem
\n\nLes entités ont un identifiant. Créons une classe de base pour Entity :
\n\npublic abstract class Entity<TId>\n{\n public TId Id { get; protected set; }\n\n protected Entity(TId id) => Id = id;\n\n public override bool Equals(object? obj) =>\n obj is Entity<TId> other && Id!.Equals(other.Id);\n\n public override int GetHashCode() => Id!.GetHashCode();\n}\n\n\nMaintenant, Order et LineItem eux-mêmes :
public class Order : Entity<Guid>\n{\n private readonly List<LineItem> _items = new();\n public IReadOnlyList<LineItem> Items => _items.AsReadOnly();\n public OrderStatus Status { get; private set; }\n public Money Total { get; privat