Modèles de domaine riches: comment se situe exactement le comportement?

84

Dans le débat sur les modèles de domaine Rich vs. Anemic, Internet regorge de conseils philosophiques, mais ne contient que des exemples faisant autorité. L’objectif de cette question est de trouver des directives définitives et des exemples concrets de modèles de conception pilotés par domaine appropriés. (Idéalement en C #.)

Pour un exemple concret, cette implémentation de DDD semble être fausse:

Les modèles de domaine WorkItem ci-dessous ne sont que des sacs de propriétés, utilisés par Entity Framework pour une base de données code-first. Per Fowler, c'est anémique .

La couche WorkItemService est apparemment une perception erronée courante des services de domaine; il contient toute la logique comportementale / métier du workItem. Per Yemelyanov et d’autres, c’est une procédure . (p. 6)

Donc, si le dessous est faux, comment puis-je le corriger?
Le comportement, à savoir AddStatusUpdate ou Checkout , devrait appartenir à la classe WorkItem correcte?
Quelles dépendances le modèle WorkItem devrait-il avoir?

entrez la description de l'image ici

public class WorkItemService : IWorkItemService {
    private IUnitOfWorkFactory _unitOfWorkFactory;

    //using Unity for dependency injection
    public WorkItemService(IUnitOfWorkFactory unitOfWorkFactory) {
        _unitOfWorkFactory = unitOfWorkFactory;
    }

    public void AddStatusUpdate(int workItemId, int statusId) {

        using (var unitOfWork = _unitOfWorkFactory.GetUnitOfWork<IWorkItemUnitOfWork>()) {
            var workItemRepo = unitOfWork.WorkItemRepository;
            var workItemStatusRepo = unitOfWork.WorkItemStatusRepository;

            var workItem = workItemRepo.Read(wi => wi.Id == workItemId).FirstOrDefault();
            if (workItem == null)
                throw new ArgumentException(string.Format(@"The provided WorkItem Id '{0}' is not recognized", workItemId), "workItemId");

            var status = workItemStatusRepo.Read(s => s.Id == statusId).FirstOrDefault();
            if (status == null)
                throw new ArgumentException(string.Format(@"The provided Status Id '{0}' is not recognized", statusId), "statusId");

            workItem.StatusHistory.Add(status);

            workItemRepo.Update(workItem);
            unitOfWork.Save();
        }
    }
}

(Cet exemple a été simplifié pour être plus lisible. Le code est certainement toujours maladroit, car c'est une tentative confuse, mais le comportement du domaine était le suivant: statut de la mise à jour en ajoutant le nouveau statut à l'historique des archives. En fin de compte, je suis d'accord avec les autres pourrait juste être manipulé par CRUD.)

Mise à jour

@AlexeyZimarev a donné la meilleure réponse, une vidéo parfaite sur le sujet en C # de Jimmy Bogard, mais elle a apparemment été déplacée dans un commentaire ci-dessous car elle ne donnait pas suffisamment d'informations au-delà du lien. J'ai un brouillon de mes notes résumant la vidéo dans la réponse ci-dessous. N'hésitez pas à commenter la réponse avec des corrections. La vidéo dure une heure mais vaut vraiment la peine d'être visionnée.

Mise à jour - 2 ans plus tard

Je pense que c'est un signe de la maturité naissante de DDD: même après l'avoir étudié pendant 2 ans, je ne peux toujours pas promettre que je connais la "bonne façon" de le faire. Le langage omniprésent, les racines globales et son approche de la conception axée sur le comportement constituent les contributions précieuses de DDD au secteur. L'ignorance de la persistance et le repérage des événements créent de la confusion, et je pense qu'une telle philosophie l'empêche d'adopter plus largement. Mais si je devais refaire ce code, avec ce que j'ai appris, je pense que cela ressemblerait à ceci:

entrez la description de l'image ici

Je suis toujours heureux de recevoir des réponses à ce message (très actif) fournissant un code de bonne pratique pour un modèle de domaine valide.

RJB
la source
6
Toutes les théories philosophiques tombent au sol lorsque vous leur dites "I don't want to duplicate all my entities into DTOs simply because I don't need it and it violates DRY, and I also don't want my client application to take a dependency on EntityFramework.dll". "Entités" dans le jargon du Entity Framework ne sont pas les mêmes que "Entités" mais dans "Modèle de domaine"
Federico Berasategui
Je suis d'accord pour dupliquer mes entités de domaine dans un DTO, en utilisant un outil automatisé tel que Automapper, si c'est ce que cela prend. Je ne suis tout simplement pas sûr de ce à quoi cela devrait ressembler à la fin de la journée.
RJB
16
Je vous recommande de regarder la session NDC 2012 de Jimmy Bogard "Crafting Wicked Domain Models" sur Vimeo . Il explique ce que devrait être le domaine riche et comment le mettre en œuvre dans la vie réelle en ayant un comportement dans vos entités. Les exemples sont très pratiques et tout en C #.
Alexey Zimarev
Merci, je suis à mi-chemin de la vidéo et c'est parfait jusqu'à présent. Je savais que si cela n'allait pas, il devait y avoir une "bonne" réponse quelque part ....
RJB
2
Je demande aussi l'amour pour Java: /
uylmz

Réponses:

59

La réponse la plus utile a été donnée par Alexey Zimarev et a obtenu au moins 7 votes positifs avant qu'un modérateur ne l'ait déplacée dans un commentaire situé sous ma question initiale ....

Sa réponse:

Je vous recommande de regarder la session NDC 2012 de Jimmy Bogard "Crafting Wicked Domain Models" sur Vimeo. Il explique ce que devrait être le domaine riche et comment le mettre en œuvre dans la vie réelle en ayant un comportement dans vos entités. Les exemples sont très pratiques et tout en C #.

http://vimeo.com/43598193

J'ai pris quelques notes pour résumer la vidéo à la fois pour le bénéfice de mon équipe et pour fournir un peu plus de détail dans ce post. (La vidéo dure une heure, mais chaque minute vaut vraiment la peine si vous en avez le temps. Jimmy Bogard mérite beaucoup de mérite pour son explication.)

  • "Pour la plupart des applications ... nous ne savons pas qu'elles seront complexes au début. Elles le sont tout simplement."
    • La complexité augmente naturellement à mesure que le code et les exigences sont ajoutés. Les applications peuvent être très simples au début, comme CRUD, mais le comportement et les règles peuvent devenir essentiels.
    • «Ce qui est bien, c’est que nous n’avons pas à commencer par des complexes. Nous pouvons commencer avec le modèle de domaine anémique, c’est juste des sacs de propriétés, et avec des techniques de refactorisation standard, nous pouvons évoluer vers un véritable modèle de domaine.»
  • Modèles de domaine = objets métier. Comportement de domaine = règles de gestion.
  • Le comportement est souvent caché dans une application - il peut être dans PageLoad, Button1_Click ou souvent dans des classes auxiliaires telles que 'FooManager' ou 'FooService'.
  • Les règles métier distinctes des objets de domaine "nous obligent à nous en souvenir".
    • Dans mon exemple personnel ci-dessus, une règle de gestion est WorkItem.StatusHistory.Add (). Nous ne faisons pas que changer le statut, nous l'archivons pour l'audit.
  • Les comportements de domaine "éliminent beaucoup plus facilement les bogues dans une application que la simple écriture de tests." Les tests nécessitent que vous sachiez pour écrire ces tests. Les comportements de domaine vous offrent les bons chemins à tester .
  • Les services de domaine sont des "classes d'assistance permettant de coordonner les activités entre différentes entités de modèle de domaine".
    • Services de domaine! = Comportement de domaine. Les entités ont un comportement, les services de domaine ne sont que des intermédiaires entre les entités.
  • Les objets de domaine ne doivent pas posséder l'infrastructure dont ils ont besoin (par exemple, IOfferCalculatorService). Le service d'infrastructure doit être transmis au modèle de domaine qui l'utilise.
  • Les modèles de domaine devraient proposer de vous dire ce qu'ils peuvent faire, et ils ne devraient être capables que de faire ces choses.
  • Les propriétés des modèles de domaine doivent être protégées par des paramètres privés, afin que seul le modèle puisse définir ses propres propriétés, par le biais de ses propres comportements . Sinon, c'est "promiscuous".
  • Les objets de modèle de domaine anémique, qui ne sont que des poches de propriétés pour un ORM, ne constituent "qu'un mince placage - une version fortement typée sur la base de données".
    • "Aussi facile que soit d'obtenir une ligne de base de données dans un objet, c'est ce que nous avons."
    • «La plupart des modèles d'objet persistants ne sont que cela. Ce qui différencie un modèle de domaine anémique d'une application qui n'a pas vraiment de comportement, c'est si un objet a des règles de gestion, mais que ces règles ne se trouvent pas dans un modèle de domaine. '
  • "Pour de nombreuses applications, il n'est pas vraiment nécessaire de créer une couche logique d'application métier, c'est juste quelque chose qui peut communiquer avec la base de données et peut-être un moyen simple de représenter les données qui s'y trouvent."
    • En d'autres termes, si vous ne faites que CRUD sans objet métier ni règle de comportement particuliers, vous n'avez pas besoin de DDD.

N'hésitez pas à commenter tout autre point qui, à votre avis, devrait être inclus, ou si vous pensez que l'une de ces notes n'est pas conforme aux attentes. J'ai essayé de citer directement ou de paraphraser autant que possible.

RJB
la source
Belle vidéo, en particulier pour voir comment fonctionne le refactoring dans un outil. Une grande partie concerne l'encapsulation correcte des objets de domaine (pour s'assurer de leur cohérence). Il fait un excellent travail en énonçant les règles commerciales sur les offres, les membres, etc. Il mentionne le mot invariant à quelques reprises (ce qui est une modélisation de domaine basée sur un contrat). Je souhaite que le code .net communique mieux en quoi consiste une règle de gestion formelle, car ces règles changent et vous devez les maintenir.
Fuhrmanator
6

Votre question ne peut pas être répondue, parce que votre exemple est faux. Plus précisément, parce qu'il n'y a pas de comportement. Du moins pas dans la zone de votre domaine. L'exemple de AddStatusUpdateméthode n'est pas une logique de domaine, mais une logique qui utilise ce domaine. Ce type de logique a du sens d'être dans un type de service, qui gère les demandes externes.

Par exemple, s'il était exigé qu'un élément de travail spécifique ne puisse avoir que des statuts spécifiques ou qu'il ne puisse avoir que des statuts N, il s'agit alors d'une logique de domaine qui doit faire partie de l'une WorkItemou de l'autre StatusHistoryméthode.

La raison de votre confusion est que vous essayez d’appliquer une directive à un code qui n’en a pas besoin. Les modèles de domaine ne sont pertinents que si vous avez beaucoup de logique de domaine complexe. Par exemple. logique qui fonctionne sur les entités elles-mêmes et découle d'exigences. Si le code concerne la manipulation d'entités à partir de données externes, il ne s'agit probablement pas d'une logique de domaine. Mais au moment où vous obtenez beaucoup de ifdonnées en fonction des données et des entités avec lesquelles vous travaillez, alors c'est la logique de domaine.

L'un des problèmes de la modélisation de domaine véritable est qu'il s'agit de gérer des exigences complexes. Et en tant que tel, son véritable pouvoir et ses avantages ne peuvent être présentés sur un simple code. Vous avez besoin de dizaines d'entités ayant des tonnes d'exigences autour d'elles pour vraiment en voir les avantages. Encore une fois, votre exemple est trop simple pour qu'un modèle de domaine puisse vraiment briller.

Enfin, certains aspects de l'ergothérapie que je voudrais mentionner, c'est qu'un véritable modèle de domaine avec une conception POO réelle serait vraiment difficile à conserver avec Entity Framework. Alors que les ORM ont été conçus avec la mise en correspondance de la vraie structure POO avec des structures relationnelles, de nombreux problèmes subsistent et le modèle relationnel se retrouvera souvent dans le modèle POO. Même avec nHibernate, que je considère beaucoup plus puissant que EF, cela peut poser problème.

Euphorique
la source
Bons points. À quoi la méthode AddStatusUpdate appartiendrait-elle alors, dans Data ou dans un autre projet dans Infrastrcture? Qu'est-ce qu'un exemple de comportement pouvant théoriquement appartenir à WorkItem? Tout code ou maquette serait grandement apprécié. Mon exemple a en fait été simplifié pour être plus lisible. Il existe d'autres entités, et par exemple, AddStatusUpdate a un comportement supplémentaire: il prend en fait un nom de catégorie d'état et, si cette catégorie n'existe pas, elle est créée.
RJB
@RJB Comme je l'ai dit, AddStatusUpdate est un code qui utilise le domaine. Donc, soit un type de service Web, soit une application utilisant les classes de domaine. Et comme je l'ai dit, vous ne pouvez vous attendre à aucune sorte de maquette ou de pseudocode, car vous auriez besoin de créer un projet suffisamment complexe pour présenter le véritable avantage du modèle de domaine de programmation orientée objet.
Euphoric
5

Votre hypothèse selon laquelle l’encapsulation de votre logique d’entreprise associée à WorkItem dans un «gros service» est un anti-modèle inhérent, ce qui, à mon avis, ne l’est pas nécessairement.

Indépendamment de vos réflexions sur le modèle de domaine anémique, les schémas et pratiques standard typiques d’une application .NET sectorielle encouragent une approche en couches transactionnelle comprenant divers composants. Ils encouragent la séparation de la logique métier du modèle de domaine afin de faciliter la communication d'un modèle de domaine commun entre d'autres composants .NET ainsi que des composants sur différentes piles de technologies ou sur des niveaux physiques.

Un exemple de ceci serait un service Web SOAP basé sur .NET qui communique avec une application cliente Silverlight ayant une DLL contenant des types de données simples. Ce projet d'entité de domaine peut être intégré à un assemblage .NET ou à un assemblage Silverlight, dans lequel les composants Silverlight intéressés qui possèdent cette DLL ne seront pas exposés à des comportements d'objet pouvant dépendre de composants uniquement disponibles pour le service.

Indépendamment de votre position sur ce débat, il s'agit du modèle adopté et accepté présenté par Microsoft et, selon mon opinion professionnelle, ce n'est pas une mauvaise approche, mais un modèle objet qui définit son propre comportement n'est pas nécessairement un anti-modèle non plus. Si vous continuez avec cette conception, il est préférable de comprendre et de comprendre certaines des limitations et des difficultés que vous pourriez rencontrer si vous devez intégrer d'autres composants qui doivent voir votre modèle de domaine. Dans ce cas particulier, vous voudrez peut-être qu'un traducteur convertisse votre modèle de domaine de style orienté objet en objets de données simples qui n'exposent pas certaines méthodes de comportement.

arbre_érable
la source
1
1) Comment pouvez-vous séparer la logique métier du modèle de domaine? C'est le domaine dans lequel vit cette logique métier; les entités de ce domaine exécutent le comportement associé à cette logique métier. Le monde réel n’a pas de services et n’existe pas non plus dans la tête des experts du domaine. 2) Tout composant souhaitant s’intégrer à vous doit créer son propre modèle de domaine, car ses besoins seront différents et il aura une vision différente de votre modèle de domaine. Depuis longtemps, vous pouvez créer un modèle de domaine pouvant être partagé.
Stefan Billiet
1
@StefanBilliet Ce sont des points positifs sur l'erreur d'un modèle de domaine universel, mais il est possible de créer des composants plus simples et une interaction de composants comme je l'ai déjà fait auparavant. Mon opinion est que la logique de traduction entre les modèles de domaine peut générer beaucoup de code fastidieux et passe-partout et que si cela peut être évité en toute sécurité, cela peut constituer un bon choix de conception.
maple_shaft
1
Pour être franc, je pense que le seul bon choix en matière de conception est un modèle sur lequel un expert en affaires peut raisonner. Vous construisez un modèle de domaine que votre entreprise peut utiliser pour résoudre certains problèmes de ce domaine. La division du comportement des entités de domaine en services rend la tâche plus difficile, car vous devez constamment mapper ce que les experts du domaine disent en un code de service qui ne ressemble en rien à la conversation en cours. D'après mon expérience, vous perdez beaucoup plus de temps avec cela qu'avec taper au clavier. Cela ne veut pas dire qu'il n'y a pas moyen de contourner le code de la chaudière.
Stefan Billiet
@StefanBilliet Dans un monde parfait, je suis d'accord avec vous, où un expert en affaires a le temps de s'asseoir avec les développeurs. La réalité de l’industrie du logiciel est que l’expert métier n’a ni le temps ni l’intérêt d’être impliqué à ce niveau ou pire, mais les développeurs ne doivent le découvrir qu’avec des directives vagues.
maple_shaft
C'est vrai, mais ce n'est pas une raison pour accepter cette réalité. Poursuivre dans cette voie est une perte de temps (et éventuellement de réputation) des développeurs et de l’argent du client. Le processus que j'ai décrit est une relation qui doit être construite au fil du temps. cela demande beaucoup d’efforts, mais les résultats sont bien meilleurs. Il y a une raison pour laquelle le "langage ubiquitaire" est souvent considéré comme l'aspect le plus important de DDD.
Stefan Billiet
5

Je me rends compte que cette question est assez ancienne, donc cette réponse est pour la postérité. Je veux répondre avec un exemple concret au lieu d'un exemple basé sur la théorie.

Encapsulez le "changement d'état de l'élément de travail" sur la WorkItemclasse comme suit:

public SomeStatusUpdateType Status { get; private set; }

public void ChangeStatus(SomeStatusUpdateType status)
{
    // Maybe we designed this badly at first ;-)
    Status = status;       
}

Maintenant, votre WorkItemclasse est responsable de se maintenir dans un état légal. La mise en œuvre est assez faible, cependant. Le propriétaire du produit souhaite qu'un historique de toutes les mises à jour de statut soit effectué sur le WorkItem.

Nous changeons cela en quelque chose comme ceci:

private ICollection<SomeStatusUpdateType> StatusUpdates { get; private set; }
public SomeStatusUpdateType Status => StatusUpdates.OrderByDescending(s => s.CreatedOn).FirstOrDefault();

public void ChangeStatus(SomeStatusUpdateType status)
{
    // Better...
    StatusUpdates.Add(status);       
}

L'implémentation a radicalement changé, mais l'appelant de la ChangeStatusméthode ignore les détails de l'implémentation sous-jacente et n'a aucune raison de se modifier lui-même.

Ceci est un exemple d'entité modèle de domaine riche, IMHO.

Don
la source