Modèle de domaine riche vs anémique [fermé]

93

Je décide si je devrais utiliser un modèle de domaine riche sur un modèle de domaine anémique et je recherche de bons exemples des deux.

J'ai créé des applications Web à l'aide d'un modèle de domaine anémique, soutenu par un système de couche Service -> Repository -> Storage , en utilisant FluentValidation pour la validation BL et en plaçant tout mon BL dans la couche Service.

J'ai lu le livre DDD d'Eric Evan, et lui (avec Fowler et d'autres) semble penser que les modèles de domaine anémique sont un anti-pattern.

Je voulais donc vraiment avoir un aperçu de ce problème.

En outre, je suis vraiment à la recherche de bons exemples (de base) d'un modèle de domaine riche et des avantages par rapport au modèle de domaine anémique qu'il offre.

Sam
la source
Vous pouvez également consulter ce blog qui plaide en faveur du modèle de domaine anémique
Japheth Ongeri - inkalimeva
14
DDD> ADM , ADM> DDD , DDD> ADM , ADM> DDD , ADM + DDD ... DDD / ADM, ou comment ne pas être d'accord sur la conception de logiciels !
sp00m
Voici un exemple de la façon d'éviter le modèle de domaine anémique: medium.com/@wrong.about/…
Vadim Samokhin
11
C'est drôle que cette question ait pu trouver une réponse avec un seul lien vers un projet du monde réel financé par une organisation réelle. Après 5 ans, pas de bonne réponse, IMO. Parler n'est pas cher. Montre-moi le code.
Mateusz Stefek

Réponses:

57

La différence est qu'un modèle anémique sépare la logique des données. La logique est souvent placé dans les classes nommées **Service, **Util, **Manager, **Helperet ainsi de suite. Ces classes implémentent la logique d'interprétation des données et prennent donc le modèle de données comme argument. Par exemple

public BigDecimal calculateTotal(Order order){
...
}

tandis que l'approche du domaine riche inverse cela en plaçant la logique d'interprétation des données dans le modèle du domaine riche. Ainsi, il associe la logique et les données et un modèle de domaine riche ressemblerait à ceci:

order.getTotal();

Cela a un impact important sur la cohérence des objets. Puisque la logique d'interprétation des données encapsule les données (les données ne sont accessibles que via des méthodes objet), les méthodes peuvent réagir aux changements d'état d'autres données -> C'est ce que nous appelons le comportement.

Dans un modèle anémique, les modèles de données ne peuvent pas garantir qu'ils sont dans un état légal alors qu'ils le peuvent dans un modèle de domaine riche. Un modèle de domaine riche applique des principes OO tels que l'encapsulation, le masquage d'informations et le rapprochement des données et de la logique.Par conséquent, un modèle anémique est un anti-pattern d'un point de vue OO.

Pour un aperçu plus approfondi, jetez un œil à mon blog https://www.link-intersystems.com/blog/2011/10/01/anemic-vs-rich-domain-models/

René Link
la source
15
Disons que le calcul du prix total d'une commande implique: 1) L'application d'une remise qui dépend du fait que le client est membre de l'un des nombreux programmes de fidélité possibles. 2) Appliquer une remise pour les commandes contenant un groupe spécifique d'articles en fonction de la campagne marketing en cours menée par le magasin. 3) Calcul de la taxe où le montant de la taxe dépend de chaque article spécifique de la commande. À votre avis, à quoi appartient toute cette logique? Pourriez-vous s'il vous plaît donner un exemple de pseudo-code simple. Je vous remercie!
Nik
4
@Nik Dans le modèle enrichi, la commande aurait une référence à l'objet Client et l'objet Client ferait référence au programme de fidélité. Ainsi, l'Ordre aurait accès à toutes les informations dont il avait besoin sans avoir besoin de références explicites à des éléments tels que des services et des référentiels à partir desquels récupérer ces informations. Cependant, il semble facile de se heurter à un cas où des références cycliques se produisent. C'est-à-dire que la commande fait référence au client, le client a une liste de toutes les commandes. Je pense que c'est peut-être en partie pourquoi les gens préfèrent Anemic maintenant.
écraser
3
@crush L'approche que vous décrivez fonctionne très bien. Il y a un hic. Nous stockons probablement les entités dans une base de données. Ainsi, pour calculer le total d'une commande, nous devons récupérer la commande, le client, le programme de fidélisation, la campagne marketing, le tableau des taxes dans la base de données. Considérez également, un client a une collection de commandes, un programme de fidélité a une collection de clients et ainsi de suite. Si nous récupérons naïvement tout cela, nous finirons par charger toute la base de données dans la RAM. Ce n'est bien sûr pas viable, nous avons donc recours à ne charger que les données pertinentes de la base de données ... 1/2
Nik
3
@Nik "Si nous récupérons tous ces éléments de manière native, nous finirons par charger toute la base de données dans la RAM." C'est également l'un des principaux inconvénients du modèle riche dans mon esprit. Le modèle riche est agréable jusqu'à ce que votre domaine devienne grand et complexe, puis vous commencez à atteindre les limites de l'infrastructure. C'est là que les ORM à chargement paresseux peuvent être utiles. Trouvez-en un bon et vous pourrez conserver des modèles riches sans charger la totalité de la base de données en mémoire alors que vous n'en aviez besoin que d'un vingtième. Cela dit, j'ai tendance à utiliser moi-même le modèle anémique avec le CQRS après de nombreuses années de va-et-vient entre anémique et riche.
écraser le
2
Une autre chose à considérer est l'emplacement de la logique de votre domaine métier. De plus en plus de développeurs le déplacent hors de la base de données et vers les applications auxquelles il appartient à mon avis. Mais si vous êtes coincé dans une situation où votre entreprise exige que la logique métier reste dans la couche de base de données (procédures stockées), vous ne bénéficierez certainement pas de l'ajout de cette logique dans un modèle de domaine riche. En fait, vous pourriez simplement vous préparer à rencontrer des conflits où les procédures stockées ont des règles différentes de celles de la couche de domaine de votre application ...
écraser le
53

Bozhidar Bozhanov semble plaider en faveur du modèle anémique dans ce billet de blog.

Voici le résumé qu'il présente:

  • les objets de domaine ne doivent pas être gérés par le ressort (IoC), ils ne doivent pas avoir de DAO ou quoi que ce soit lié à l'infrastructure injecté en eux

  • les objets de domaine ont les objets de domaine dont ils dépendent définis par la mise en veille prolongée (ou le mécanisme de persistance)

  • les objets de domaine exécutent la logique métier, comme l'idée de base de DDD est, mais cela n'inclut pas les requêtes de base de données ou CRUD - uniquement les opérations sur l'état interne de l'objet

  • il y a rarement besoin de DTO - les objets de domaine sont les DTO eux-mêmes dans la plupart des cas (ce qui permet d'économiser du code standard)

  • les services effectuent des opérations CRUD, envoient des e-mails, coordonnent les objets de domaine, génèrent des rapports basés sur plusieurs objets de domaine, exécutent des requêtes, etc.

  • la couche de service (application) n'est pas si fine, mais n'inclut pas les règles métier intrinsèques aux objets du domaine

  • la génération de code doit être évitée. L'abstraction, les modèles de conception et la DI devraient être utilisés pour surmonter le besoin de génération de code et, finalement, pour se débarrasser de la duplication de code.

METTRE À JOUR

J'ai récemment lu cet article où l'auteur préconise de suivre une sorte d'approche hybride - les objets du domaine peuvent répondre à diverses questions en fonction uniquement de leur état (ce qui dans le cas de modèles totalement anémiques serait probablement fait dans la couche service)

geoand
la source
11
Je ne peux pas extraire de cet article que Bozho semble plaider en faveur du modèle de domaine anémique. la couche de service (application) n'est pas si fine, mais n'inclut pas les règles métier intrinsèques aux objets du domaine . Ce que je comprends, c'est que les objets de domaine doivent contenir la logique métier qui leur est intrinsèque, mais ils ne doivent contenir aucune autre logique d' infrastructure . Cette approche ne me semble pas du tout un modèle de domaine anémique.
Utku
8
Aussi celui-ci: les objets de domaine exécutent la logique métier, comme l'idée de base de DDD est, mais cela n'inclut pas les requêtes de base de données ou CRUD - uniquement les opérations sur l'état interne de l'objet . Ces déclarations ne semblent pas du tout favoriser le modèle du domaine anémique. Ils indiquent uniquement que la logique d'infrastructure ne doit pas être couplée à des objets de domaine. Du moins c'est ce que je comprends.
Utku
@Utku À mon avis, il semble assez clair que Bozho prône une sorte d'hybride entre les deux modèles, un hybride qui, je dirais, est plus proche du modèle anémique que du modèle riche.
geoand
41

Mon point de vue est le suivant:

Modèle de domaine anémique = tables de base de données mappées sur des objets (uniquement des valeurs de champ, pas de comportement réel)

Modèle de domaine riche = une collection d'objets qui exposent le comportement

Si vous souhaitez créer une application CRUD simple, peut-être qu'un modèle anémique avec un framework MVC classique suffit. Mais si vous souhaitez implémenter une sorte de logique, le modèle anémique signifie que vous ne ferez pas de programmation orientée objet.

* Notez que le comportement des objets n'a rien à voir avec la persistance. Une couche différente (mappeurs de données, référentiels, etc.) est responsable de la persistance des objets de domaine.

George
la source
5
Désolé pour mon ignorance, mais comment un modèle de domaine riche peut-il suivre le principe SOLID si vous mettez toute la logique liée à l'entité dans la classe. Cela viole le principe SOLID, le «S» exactement, qui signifie responsabilité unique, qui dit qu'une classe ne doit faire qu'une chose et la faire correctement.
redigaffi
5
@redigaffi Cela dépend de la façon dont vous définissez "une chose". Considérons une classe avec deux propriétés et deux méthodes: x, y, sumet difference. Ça fait quatre choses. Ou vous pourriez argumenter que c'est l'addition et la soustraction (deux choses). Ou vous pourriez dire que ce sont les mathématiques (une chose). Il existe de nombreux articles de blog sur la façon de trouver un équilibre dans l'application du SRP. En voici un: hackernoon.com
...
2
Dans DDD, une seule resposibilité signifie qu'une classe / modèle peut gérer son propre état sans provoquer d'effets secondaires sur le reste du système dans son ensemble. Toute autre définition aboutit à de fastidieux débats philosophiques d'après mon expérience.
ZombieTfk
12

Tout d'abord, j'ai copié-collé la réponse de cet article http://msdn.microsoft.com/en-gb/magazine/dn385704.aspx

La figure 1 montre un modèle de domaine anémique, qui est essentiellement un schéma avec des getters et des setters.

Figure 1 Typical Anemic Domain Model Classes Look Like Database Tables

public class Customer : Person
{
  public Customer()
  {
    Orders = new List<Order>();
  }
  public ICollection<Order> Orders { get; set; }
  public string SalesPersonId { get; set; }
  public ShippingAddress ShippingAddress { get; set; }
}
public abstract class Person
{
  public int Id { get; set; }
  public string Title { get; set; }
  public string FirstName { get; set; }
  public string LastName { get; set; }
  public string CompanyName { get; set; }
  public string EmailAddress { get; set; }
  public string Phone { get; set; }
}

Dans ce modèle plus riche, plutôt que d'exposer simplement les propriétés à lire et à écrire, la surface publique de Customer est constituée de méthodes explicites.

Figure 2 A Customer Type That’s a Rich Domain Model, Not Simply Properties

public class Customer : Contact
{
  public Customer(string firstName, string lastName, string email)
  {
    FullName = new FullName(firstName, lastName);
    EmailAddress = email;
    Status = CustomerStatus.Silver;
  }
  internal Customer()
  {
  }
  public void UseBillingAddressForShippingAddress()
  {
    ShippingAddress = new Address(
      BillingAddress.Street1, BillingAddress.Street2,
      BillingAddress.City, BillingAddress.Region,
      BillingAddress.Country, BillingAddress.PostalCode);
  }
  public void CreateNewShippingAddress(string street1, string street2,
   string city, string region, string country, string postalCode)
  {
    ShippingAddress = new Address(
      street1,street2,
      city,region,
      country,postalCode)
  }
  public void CreateBillingInformation(string street1,string street2,
   string city,string region,string country, string postalCode,
   string creditcardNumber, string bankName)
  {
    BillingAddress = new Address      (street1,street2, city,region,country,postalCode );
    CreditCard = new CustomerCreditCard (bankName, creditcardNumber );
  }
  public void SetCustomerContactDetails
   (string email, string phone, string companyName)
  {
    EmailAddress = email;
    Phone = phone;
    CompanyName = companyName;
  }
  public string SalesPersonId { get; private set; }
  public CustomerStatus Status { get; private set; }
  public Address ShippingAddress { get; private set; }
  public Address BillingAddress { get; private set; }
  public CustomerCreditCard CreditCard { get; private set; }
}
Razan Paul
la source
2
Il y a un problème avec les méthodes qui créent à la fois un objet et attribuent une propriété à un objet nouvellement créé. Ils rendent le code moins extensible et moins flexible. 1) Que faire si le consommateur de code souhaite créer non Address, mais ExtendedAddresshérité de Address, avec plusieurs propriétés supplémentaires? 2) Ou changer CustomerCreditCardles paramètres du constructeur à prendre BankIDau lieu de BankName?
Lightman
Qu'est-ce que la création d'une adresse nécessite des services supplémentaires par rapport à ce qui compose l'objet? Il vous reste une injection de méthode pour obtenir ces services. Et si c'était beaucoup de services?
écraser
8

L'un des avantages des classes de domaine riches est que vous pouvez appeler leur comportement (méthodes) chaque fois que vous avez la référence à l'objet dans n'importe quelle couche. De plus, vous avez tendance à écrire des méthodes petites et distribuées qui collaborent ensemble. Dans les classes de domaine anémique, vous avez tendance à écrire des méthodes procédurales lourdes (dans la couche de service) qui sont généralement gérées par un cas d'utilisation. Ils sont généralement moins maintenables que les classes de domaine riches.

Un exemple de classes de domaine avec des comportements:

class Order {

     String number

     List<OrderItem> items

     ItemList bonus

     Delivery delivery

     void addItem(Item item) { // add bonus if necessary }

     ItemList needToDeliver() { // items + bonus }

     void deliver() {
         delivery = new Delivery()
         delivery.items = needToDeliver()
     }

}

La méthode needToDeliver()retournera la liste des articles qui doivent être livrés, y compris les bonus. Il peut être appelé à l'intérieur de la classe, depuis une autre classe associée ou depuis une autre couche. Par exemple, si vous passez Orderà la vue, vous pouvez utiliser les éléments needToDeliver()sélectionnés Orderpour afficher la liste des éléments à confirmer par l'utilisateur avant de cliquer sur le bouton Enregistrer pour conserver leOrder .

Répondre au commentaire

Voici comment j'utilise la classe de domaine du contrôleur:

def save = {
   Order order = new Order()
   order.addItem(new Item())
   order.addItem(new Item())
   repository.create(order)
}

La création de Orderet son LineItemest en une seule transaction. Si l'un des LineItemne peut pas être créé, nonOrder ne sera créé.

J'ai tendance à avoir une méthode qui représente une seule transaction, telle que:

def deliver = {
   Order order = repository.findOrderByNumber('ORDER-1')
   order.deliver()       
   // save order if necessary
}

Tout ce qu'il y a à l'intérieur deliver() sera exécuté en une seule transaction. Si je dois exécuter de nombreuses méthodes non liées en une seule transaction, je créerais une classe de service.

Pour éviter une exception de chargement paresseux, j'utilise le graphique d'entité nommé JPA 2.1. Par exemple, dans l'écran du contrôleur pour la livraison, je peux créer une méthode pour charger l' deliveryattribut et ignorer bonus, comme repository.findOrderByNumberFetchDelivery(). Dans l'écran bonus, j'appelle une autre méthode qui charge l' bonusattribut et l'ignore delivery, telle que repository.findOrderByNumberFetchBonus(). Cela nécessite dicipline car je ne peux toujours pas appeler à l' deliver()intérieur de l'écran bonus.

jocki
la source
1
Et la portée de la transaction?
kboom
5
Les comportements de modèle de domaine ne doivent pas contenir de logique de persistance (y compris de transaction). Ils doivent pouvoir être testés (en test unitaire) sans être connectés à la base de données. La portée de la transaction est la responsabilité de la couche de service ou de la couche de persistance.
jocki
1
Que diriez-vous du chargement paresseux alors?
kboom le
Lorsque vous créez des instances de classes de domaine dans le test unitaire, elles ne sont pas à l'état géré car ce sont des objets simples. Tous les comportements peuvent être testés correctement.
jocki
Et que se passe-t-il lorsque vous attendez l'objet de domaine de la couche de service? N'est-il pas géré alors?
kboom
8

Lorsque j'écrivais des applications de bureau monolithiques, je construisais des modèles de domaine riches, j'avais l'habitude de les créer.

Maintenant, j'écris de minuscules microservices HTTP, il y a aussi peu de code que possible, y compris les DTO anémiques.

Je pense que DDD et cet argument anémique datent de l'ère monolithique des applications de bureau ou de serveur. Je me souviens de cette époque et je conviens que les modèles anémiques sont étranges. J'ai construit une grande application de trading FX monolithique et il n'y avait pas de modèle, vraiment, c'était horrible.

Avec les microservices, les petits services avec leur comportement riche sont sans doute les modèles composables et les agrégats au sein d'un domaine. Ainsi, les implémentations de microservices elles-mêmes peuvent ne pas nécessiter de DDD supplémentaire. L'application de microservice peut être le domaine.

Un microservice de commandes peut avoir très peu de fonctions, exprimées en ressources RESTful ou via SOAP ou autre. Le code du microservice des commandes peut être extrêmement simple.

Un service unique (micro) monolithique plus grand, en particulier celui qui le maintient en RAM, peut bénéficier de DDD.

Luke Puplett
la source
Avez-vous des exemples de code pour les micro-services HTTP qui représentent votre état actuel de la technique? Ne vous demandez pas d'écrire quoi que ce soit, partagez simplement des liens si vous avez quelque chose que vous pourriez pointer. Merci.
Casey Plummer
3

Je pense que la racine du problème réside dans une fausse dichotomie. Comment extraire ces 2 modèles: riche et "anémique" et les opposer? Je pense que ce n'est possible que si vous avez une mauvaise idée de ce qu'est une classe . Je ne suis pas sûr, mais je pense que je l'ai trouvé dans l'une des vidéos de Bozhidar Bozhanov sur Youtube. Une classe n'est pas une data + des méthodes sur ces données. C'est une compréhension totalement invalide qui conduit à la division des classes en deux catégories: données uniquement, donc modèle anémique et données + méthodes - modèle si riche (pour être plus correct il y a une 3ème catégorie: les méthodes seulement même).

Le vrai est que la classe est un concept dans un modèle ontologique, un mot, une définition, un terme, une idée, c'est un DENOTAT . Et cette compréhension élimine les fausses dichotomies: vous ne pouvez pas avoir SEULEMENT un modèle anémique ou UNIQUEMENT un modèle riche, car cela signifie que votre modèle n'est pas adéquat, ce n'est pas pertinent par rapport à la réalité: certains concepts n'ont que des données, certains d'entre eux n'ont que des méthodes, d'autres d’entre eux sont mixtes. Parce que nous essayons de décrire, dans ce cas, certaines catégories, ensembles d'objets, relations, concepts avec des classes, et comme nous le savons, certains concepts sont des processus uniquement (méthodes), certains d'entre eux sont des ensembles d'attributs uniquement (données), certains ce sont des relations avec des attributs (mixtes).

Je pense qu'une application adéquate devrait inclure toutes sortes de classes et éviter de s'auto-limiter fanatiquement à un seul modèle. Peu importe, comment la logique représente: avec du code ou avec des objets de données interprétables (comme les Monades libres ), de toute façon: nous devrions avoir des classes (concepts, dénotats) représentant des processus, de la logique, des relations, des attributs, des caractéristiques, des données, etc. et non d'essayer d'en éviter certains ou de les réduire tous à un seul type.

Ainsi, nous pouvons extraire la logique d'une autre classe et laisser les données dans la classe d'origine, mais cela n'a pas de sens car certains concepts peuvent inclure des attributs et des relations / processus / méthodes et une séparation de ceux-ci dupliquera le concept sous 2 noms qui peuvent être réduit aux motifs: "OBJECT-Attributes" et "OBJECT-Logic". C'est bien dans les langages procéduraux et fonctionnels en raison de leur limitation, mais c'est une trop grande retenue pour un langage qui vous permet de décrire toutes sortes de concepts.

Paul-AG
la source
1

Les modèles de domaine anémique sont importants pour l'ORM et le transfert facile sur les réseaux (le sang vital de toutes les applications commerciales) mais OO est très important pour l'encapsulation et la simplification des parties «transactionnelles / manipulation» de votre code.

Par conséquent, ce qui est important, c'est de pouvoir identifier et convertir d'un monde à l'autre.

Nommez les modèles Anemic quelque chose comme AnemicUser, ou UserDAO etc. pour que les développeurs sachent qu'il y a une meilleure classe à utiliser, puis ont un constructeur approprié pour la classe none Anemic

User(AnemicUser au)

et méthode d'adaptateur pour créer la classe anémique pour le transport / la persistance

User::ToAnemicUser() 

Visez à utiliser l'utilisateur non anémique partout en dehors du transport / de la persistance

andrew pate
la source
-1

Voici un exemple qui pourrait vous aider:

Anémique

class Box
{
    public int Height { get; set; }
    public int Width { get; set; }
}

Non anémique

class Box
{
    public int Height { get; private set; }
    public int Width { get; private set; }

    public Box(int height, int width)
    {
        if (height <= 0) {
            throw new ArgumentOutOfRangeException(nameof(height));
        }
        if (width <= 0) {
            throw new ArgumentOutOfRangeException(nameof(width));
        }
        Height = height;
        Width = width;
    }

    public int area()
    {
       return Height * Width;
    }
}
Alireza Rahmani Khalili
la source
Cela semble pouvoir être converti en ValueObject par rapport à une entité.
code5 du
Juste un copier-coller de Wikipedia sans aucune explication
wst
qui a écrit plus tôt? @wst
Alireza Rahmani Khalili
@AlirezaRahmaniKhalili selon l'histoire de Wikipédia, ils ont été les premiers ... À moins que je n'ai pas compris votre question.
wst
-1

L'approche classique de DDD n'indique pas d'éviter à tout prix les modèles anémiques vs riches. Cependant, MDA peut toujours appliquer tous les concepts DDD (contextes bornés, cartes de contexte, objets de valeur, etc.) mais utiliser les modèles Anemic vs Rich dans tous les cas. Il existe de nombreux cas où l'utilisation des services de domaine pour orchestrer des cas d'utilisation de domaine complexes sur un ensemble d'agrégats de domaine constitue une bien meilleure approche que de simples agrégats appelés à partir de la couche d'application. La seule différence par rapport à l'approche DDD classique est où se trouvent toutes les validations et règles métier? Il existe une nouvelle construction connue sous le nom de validateurs de modèle. Les validateurs garantissent l'intégrité du modèle d'entrée complet avant tout cas d'utilisation ou flux de travail de domaine. Les entités agrégées racine et enfants sont anémiques mais chacune peut avoir ses propres validateurs de modèle invoqués si nécessaire, par son validateur racine. Les validateurs adhèrent toujours au SRP, sont faciles à entretenir et peuvent être testés à l'unité.

La raison de ce changement est que nous nous dirigeons maintenant davantage vers une première approche API plutôt qu'une première approche UX des microservices. REST a joué un rôle très important à cet égard. L'approche API traditionnelle (à cause de SOAP) était initialement fixée sur une API basée sur des commandes par rapport aux verbes HTTP (POST, PUT, PATCH, GET et DELETE). Une API basée sur des commandes correspond bien à l'approche orientée objet Rich Model et est toujours très valable. Cependant, les API simples basées sur CRUD, bien qu'elles puissent s'intégrer dans un modèle riche, sont bien mieux adaptées avec des modèles anémiques simples, des validateurs et des services de domaine pour orchestrer le reste.

J'adore DDD dans tout ce qu'il a à offrir, mais il arrive un moment où vous devez l'étirer un peu pour l'adapter à une approche en constante évolution et meilleure de l'architecture.

code5
la source