Fermer

octobre 23, 2025

Comment utiliser DDD pour des systèmes plus durables

Comment utiliser DDD pour des systèmes plus durables


Apprenez à identifier le modèle anémique problématique et à le transformer en un modèle riche qui expose vos intentions et est aligné sur les principes de la conception axée sur le domaine.

La conception pilotée par domaine (DDD) est une approche permettant de créer des logiciels complexes à la fois cohérents, efficaces et faciles à maintenir. DDD explore plusieurs domaines et sujets pertinents pour aider les développeurs à atteindre leurs objectifs lors du développement d’applications durables. Certains des sujets principaux incluent les domaines anémiques et leurs domaines riches opposés.

Dans cet article, nous examinerons le concept de domaine anémique et comprendrons pourquoi ce modèle, bien qu’apparemment simple, peut conduire à une architecture fragile et difficile à maintenir. Ensuite, nous verrons comment identifier un domaine anémique dans la pratique et comment le refactoriser, en le transformant en un domaine riche, où les règles métier sont encapsulées dans les entités elles-mêmes, rendant le modèle conforme aux préceptes de DDD.

Qu’est-ce qu’un domaine anémique ?

Martin Fowler a proposé le concept de modèle de domaine anémique et est disponible sur son blog : Modèle de domaine anémique – Martin Fowler.

Selon Fowler, le modèle de domaine anémique est celui où les objets de domaine (généralement appelés entités ou objets de modèle) contiennent uniquement des données (attributs) et aucune logique ou comportement métier pertinent.

Dans une structure de domaine anémique commune, les entités ne sont que des structures de données avec des getters et des setters. En conséquence, la logique métier est répartie entre les services, qu’ils soient statiques ou non. Il est courant de trouver des classes de service portant des noms tels que OrderService, CustomerServiceetc. qui utilisent des classes d’entités uniquement comme objets de transfert de données (DTO), sans aucune encapsulation ni règles métier.

Fowler considère un domaine anémique comme un anti-modèle : une solution courante à un problème récurrent, mais qui, bien qu’elle semble utile ou pratique au début, finit par entraîner des conséquences négatives ou plus de problèmes qu’elle n’en résout. En effet, cela rompt l’encapsulation orientée objet, puisque la logique n’est pas là où se trouvent les données. De plus, un domaine anémique utilise un style procédural déguisé en orientation objet, car, malgré l’utilisation de classes, son style de programmation est impératif.

Pourquoi éviter les domaines anémiques ?

Un domaine anémique peut provoquer plusieurs problèmes qui compromettent la qualité et la maintenance du logiciel. Le principal est la violation de l’encapsulation, puisque les entités exposent leurs données via des getters et setters publics, permettant à n’importe quelle partie du système de changer d’état sans aucune restriction. Un autre aspect est que l’utilisation de domaines anémiques rend plus difficile la garantie de la cohérence et de l’intégrité des règles métier, puisque ces règles sont réparties entre des services externes, généralement avec peu de cohésion.

De plus, ce modèle conduit à une architecture avec un couplage fort entre les données et les processus qui les manipulent, privilégiant un style de programmation plus procédural qu’orienté objet. En conséquence, l’expressivité du domaine est perdue, ce qui rend difficile la communication avec les spécialistes métier et compromet l’utilisation efficace d’un langage omniprésent, qui est le principe central du DDD.

Un autre problème pertinent est la duplication de la logique métier, puisque le manque d’encapsulation conduit souvent différents services à répéter des validations ou des calculs. À long terme, cela affecte négativement la maintenance, les tests et l’évolutivité de l’application, en plus d’augmenter le risque d’erreurs et d’incohérences dans les données et dans le comportement du système dans son ensemble.

Vous trouverez ci-dessous une représentation d’un exemple courant de modèle anémique :

Exemple de modèle anémique

Si les comportements ne font pas partie de la classe Entité, où sont-ils ?

Dans les domaines anémiques, il est courant de trouver des comportements et des règles métier dans des classes appelées « services de domaine » ou « services d’application ». Dans l’exemple ci-dessus, l’entité Subscription agit uniquement comme un conteneur de données, alors que toute la logique métier est en dehors d’elle, ce qui brise l’encapsulation et viole les principes DDD, qui renforcent l’idée d’un domaine qui expose ses intentions.

Ainsi, la classe de service dans un domaine anémique pourrait être représentée comme suit :

Exemple de modèle Service Anémique

Transformer un domaine anémique en un domaine riche

Un modèle riche est à l’opposé d’un modèle anémique, il concentre la logique métier au sein des entités et des objets de valeur eux-mêmes, au lieu de la laisser dispersée dans des services externes ou des classes d’utilité.

On peut dire qu’un modèle riche présente les caractéristiques suivantes :

  • Comportement et état ensemble : Les entités et les objets de valeur n’agissent pas uniquement comme des structures de transport de données ; au lieu de cela, ils encapsulent à la fois les données et les règles métier qui s’appliquent à ces données.
  • Encapsulation : Les invariants métier sont préservés dans les objets, empêchant ainsi l’apparition d’états non valides. Par exemple, un Order l’objet sait se valider, calculer le total, ajouter des éléments, annuler, etc.
  • Faible exposition des setters : La plupart des champs du modèle sont modifiés uniquement par des méthodes de domaine, évitant ainsi les propriétés avec des setters publics.
  • Plus d’expressivité : Le modèle utilise un langage omniprésent, permettant au code d’être plus lisible et de décrire fidèlement le problème réel.
  • Isolation de la logique et de l’infrastructure des applications : Le modèle de domaine ne dépend pas de frameworks, de persistance ou de couches externes. C’est le « cœur » de la logique métier, et tout ce qui s’y rapporte se trouve à l’intérieur.

Maintenant que nous savons ce que sont un modèle anémique et un modèle riche, transformons l’exemple du domaine anémique exploré précédemment. Pour ce faire, nous allons créer une nouvelle application en utilisant le même exemple, en modifiant uniquement ce qui doit être modifié pour refléter un modèle riche.

Création de l’exemple d’application

❗Important : certains détails d’implémentation ne seront pas abordés dans l’article afin de rester concentrés sur le sujet principal, mais vous pouvez consulter le code complet de l’application dans ce référentiel GitHub : Sous-piste – Code source.

Pour créer l’application de base, vous pouvez utiliser les commandes ci-dessous dans le terminal. Cet exemple utilise .NET 9, qui est la dernière version stable au moment de la rédaction de cet article.

Ces commandes créeront un projet API, des bibliothèques de classes et une solution. Ajoutez les projets à la solution et ajoutez toutes ses dépendances.


dotnet new web -o SubTrack.API
dotnet new classlib -o SubTrack.Domain
dotnet new classlib -o SubTrack.Application
dotnet new classlib -o SubTrack.Infrastructure


dotnet new sln -n SubTrack


dotnet sln SubTrack.sln add SubTrack.API/SubTrack.API.csproj
dotnet sln SubTrack.sln add SubTrack.Domain/SubTrack.Domain.csproj
dotnet sln SubTrack.sln add SubTrack.Application/SubTrack.Application.csproj
dotnet sln SubTrack.sln add SubTrack.Infrastructure/SubTrack.Infrastructure.csproj


dotnet add SubTrack.API/SubTrack.API.csproj reference SubTrack.Application/SubTrack.Application.csproj
dotnet add SubTrack.Application/SubTrack.Application.csproj reference SubTrack.Domain/SubTrack.Domain.csproj
dotnet add SubTrack.Infrastructure/SubTrack.Infrastructure.csproj reference SubTrack.Domain/SubTrack.Domain.csproj

A la fin du poste, le projet complet aura la structure suivante :

SubTrack.sln
│
├── SubTrack.API/
│   ├── Program.cs               📌Endpoint minimal API
│
├── SubTrack.Application/
│   └── Services/
│       └── SubscriptionService.cs      📌 Application Service
│
├── SubTrack.Domain/
│   └── Entities/
│       └── Subscription.cs                   📌 Domain entity
│
└── SubTrack.Infrastructure/
    └── Persistence/
        └── SubscriptionDbContext.cs         📌 Persistence

Ouvrez le projet et commençons à créer les dossiers et les classes.

Création de l’entité de domaine

La classe d’entité de domaine sera la Subscription classe, mais c’est désormais un modèle riche et non plus anémique. Ainsi, dans le SubTrack.Domain projet, créez un nouveau dossier appelé « Entités » et ajoutez-y la classe suivante :

namespace SubTrack.Domain.Entities;

public class Subscription
{
    public Guid Id { get; private set; }
    public string? ServiceName { get; private set; }
    public DateTime StartDate { get; private set; }
    public DateTime? ExpirationDate { get; private set; }
    public decimal MonthlyPrice { get; private set; }
    public int MonthsInPlan { get; private set; }
    public bool IsCancelled { get; private set; }

    private Subscription() { }

    public Subscription(Guid id, string serviceName, DateTime startDate, decimal monthlyPrice, int monthsInPlan)
    {
        if (monthlyPrice <= 0)
            throw new ArgumentException("Price must be greater than zero.", nameof(monthlyPrice));
        if (monthsInPlan <= 0)
            throw new ArgumentException("Months must be greater than zero.", nameof(monthsInPlan));

        Id = id;
        ServiceName = serviceName;
        StartDate = startDate;
        MonthlyPrice = monthlyPrice;
        MonthsInPlan = monthsInPlan;
        ExpirationDate = startDate.AddMonths(monthsInPlan);
        IsCancelled = false;
    }

    public bool IsActive(DateTime? referenceDate = null)
    {
        var date = referenceDate ?? DateTime.UtcNow;
        return !IsCancelled && (!ExpirationDate.HasValue || ExpirationDate > date);
    }

    public void ExtendPlan(int additionalMonths)
    {
        if (additionalMonths <= 0)
            throw new ArgumentException("Months must be greater than zero.", nameof(additionalMonths));

        MonthsInPlan += additionalMonths;
        ExpirationDate = StartDate.AddMonths(MonthsInPlan);
    }
}

Maintenant le Subscription la classe est bien différente. La première chose que l’on peut remarquer est que les propriétés ont des setters privés, ce qui signifie qu’elles ne sont pas exposées en dehors du domaine ; au lieu de cela, seul le domaine peut modifier ses valeurs.

💡 Déclarer des propriétés auprès de setters privés oblige le domaine à assumer la responsabilité des actes qui lui appartiennent.

Deuxièmement, on peut remarquer que le constructeur public Subscription(Guid id, string serviceName, DateTime startDate, decimal monthlyPrice, int monthsInPlan) dispose désormais de quelques validations, qui établissent certains critères que les modifications doivent respecter. Par exemple, empêcher la création d’un abonnement avec une valeur mensuelle nulle ou négative, ou avec un nombre de mois invalide.

💡 C’est une bonne pratique d’ajouter des validations dans le constructeur de classe, car elles empêchent la création d’instances non valides et de comportements inattendus.

Enfin, on peut remarquer les méthodes présentes dans la classe. Le IsActive La méthode encapsule une logique de domaine utile qui indique si l’abonnement est actif à une date donnée, en respectant le statut d’annulation et la date d’expiration. De plus, le ExtendPlan La méthode dispose également de règles de validation claires, qui facilitent l’évolution de l’entité dans le temps.

💡 La création de méthodes dans les classes de domaine rend le modèle plus fidèle à la réalité métier, renforce l’encapsulation, maintient l’intégrité des données et rend le code plus clair, plus maintenable et aligné sur le concept de modèles riches.

Création de la classe de service

Même si notre classe d’entité représente désormais un modèle riche (avec des paramètres et des comportements privés), nous avons toujours besoin d’une classe de service : après tout, certaines règles et comportements n’appartiennent pas au domaine. De plus, il est important de garder chaque classe avec sa propre responsabilité.

Ainsi, à l’intérieur du SubTrack.Application projet, créez un nouveau dossier appelé « Services » et ajoutez-y la classe suivante :

using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using SubTrack.Infrastructure.Repositories;

namespace SubTrack.Domain.Services;

public class SubscriptionService
{
    private readonly SubscriptionRepository _repository;
    private readonly ILogger<SubscriptionService> _logger;

    public SubscriptionService(SubscriptionRepository repository, ILogger<SubscriptionService> logger)
    {
        _repository = repository;
        _logger = logger;
    }

    public async Task<string> ExtendSubscriptionPlan(Guid subscriptionId, int months)
    {
        if (months <= 0)
        {
            return "Months must be greater than 0.";
        }

        var subscription = await _repository.Subscriptions
            .FirstOrDefaultAsync(s => s.Id == subscriptionId);

        if (subscription is null)
        {
            return "Subscription not found.";
        }

        if (!subscription.IsActive())
        {
            return "Cannot extend an inactive subscription.";
        }

        subscription.ExtendPlan(months);

        _repository.Subscriptions.Update(subscription);
        await _repository.SaveChangesAsync();

        return $"Subscription extended by {months} months.";
    }
}

Notez que le SubscriptionService La classe n’a désormais que des responsabilités qui n’appartiennent pas au domaine, telles que les validations de contrôle de flux.

Un autre aspect attribué à la classe de service qui n’appartient pas au modèle sont les messages d’erreur et les chaînes de retour qui font partie de l’interface avec l’application. Le modèle de domaine doit fonctionner avec des exceptions, des types de résultats ou des événements de domaine spécifiques, tandis que la gestion et la traduction de ces échecs en messages destinés à l’utilisateur doivent relever de la responsabilité d’une couche supérieure.

De plus, la responsabilité de l’accès au référentiel (interrogation de la base de données) n’incombe pas à l’entité, mais à l’application ou au service de domaine. Ici, la méthode effectue la requête et valide si l’abonnement existe. Ce type de logique d’orchestration et de récupération de données est généralement attribué à un service d’application, et non à des entités de domaine.

💡 Gardez à l’esprit que les entités de domaine doivent se concentrer sur le comportement et les règles commerciales, telles que l’encapsulation de ce que signifie être actif, comment prolonger la date d’expiration, si l’extension est autorisée, etc. Le service d’application doit se concentrer sur l’orchestration de ces comportements, la validation des données d’entrée et la communication des résultats (sous forme de messages ou de DTO) au monde extérieur.

Quand un modèle anémique a-t-il un sens ?

DDD fournit des principes et des pratiques qui aident à construire des systèmes plus durables. Cependant, cela ne signifie pas que les systèmes qui ne suivent pas DDD sont erronés. Dans de nombreux scénarios, l’adoption de modèles riches peut s’avérer inutile, voire inappropriée, et des approches plus simples peuvent mieux répondre aux exigences du projet.

En ce sens, un modèle anémique peut être plus adapté dans des contextes qui ne nécessitent pas une grande complexité. Ci-dessous, nous vérifierons les principaux scénarios dans lesquels un modèle anémique est acceptable voire recommandé :

Applications CRUD simples : Lorsque le principe est essentiellement de créer, lire, mettre à jour et supprimer des données sans règles métier complexes, un modèle anémique peut être suffisant et plus rapide à mettre en œuvre.
Exemple: Un système interne d’enregistrement des contacts où vous enregistrez et affichez uniquement les données, avec peu ou pas de validation.

Prototypes ou MVP : Lors de la création d’un prototype ou d’un produit minimum viable (MVP) ou simplement de la validation d’une idée, un modèle anémique permet une livraison plus rapide.

Projets orientés base de données : Les systèmes qui suivent l’approche base de données d’abord, où l’application reflète directement la structure de la base de données, aboutissent généralement à des modèles anémiques. Et, dans ces cas-là, cela est acceptable car de nombreuses règles métier sont présentes dans les bases de données elles-mêmes.

Code temporaire ou scripts administratifs : Pour les tâches de maintenance, les scripts et les tâches planifiées simples, l’utilisation de modèles riches peut s’avérer inutile.

Conclusion

Les applications qui utilisent ou ont l’intention d’utiliser les principes DDD doivent disposer de bons modèles d’entités de domaine. Pour ce faire, il est nécessaire d’appliquer certains concepts, dont celui de modèles riches.

Dans cet article, nous avons compris comment identifier un modèle d’entité anémique, ses principales caractéristiques et quels problèmes il peut apporter aux projets impliquant un certain niveau de complexité. De plus, nous avons vu en pratique comment transformer un modèle anémique en modèle riche.

Ainsi, chaque fois que vous créez quelque chose qui correspond aux caractéristiques décrites dans l’article, envisagez d’utiliser des modèles riches. Ils vous aideront certainement à créer des systèmes plus faciles à maintenir et qui expriment bien l’intention et les complexités du domaine.




Source link