DDD - la règle selon laquelle les entités ne peuvent pas accéder directement aux référentiels


185

Dans Domain Driven Design, il semble y avoir beaucoup d' accord sur le fait que les entités ne doivent pas accéder directement aux référentiels.

Cela vient-il du livre d'Eric Evans Domain Driven Design , ou est-il venu d'ailleurs?

Où y a-t-il de bonnes explications pour le raisonnement derrière cela?

edit: Pour clarifier: je ne parle pas de la pratique OO classique de séparer l'accès aux données dans une couche distincte de la logique métier - je parle de l'arrangement spécifique par lequel dans DDD, les entités ne sont pas censées parler aux données couche d'accès du tout (c'est-à-dire qu'ils ne sont pas censés contenir des références aux objets du référentiel)

mise à jour: j'ai donné la prime à BacceSR parce que sa réponse semblait la plus proche, mais je suis toujours assez dans le noir à ce sujet. Si c'est un principe si important, il devrait y avoir de bons articles à ce sujet en ligne quelque part, sûrement?

mise à jour: mars 2013, les votes positifs sur la question impliquent qu'il y a beaucoup d'intérêt pour cela, et même s'il y a eu beaucoup de réponses, je pense toujours qu'il y a de la place pour plus si les gens ont des idées à ce sujet.


Jetez un œil à ma question stackoverflow.com/q/8269784/235715 , elle montre une situation où il est difficile de capturer la logique, sans que l'entité ait accès au référentiel. Bien que je pense que les entités ne devraient pas avoir accès aux référentiels, il existe une solution à ma situation où le code peut être réécrit sans référence au référentiel, mais actuellement, je ne peux penser à aucune.
Alex Burtsev

Je ne sais pas d'où ça vient. Mes pensées: Je pense que ce malentendu vient des gens qui ne comprennent pas ce qu'est DDD. Cette approche n'est pas pour mettre en œuvre un logiciel mais pour le concevoir (domaine .. conception). À l'époque, nous avions des architectes et des implémenteurs, mais il n'y a plus que des développeurs de logiciels. DDD est destiné aux architectes. Et lorsqu'un architecte conçoit un logiciel, il a besoin d'un outil ou d'un modèle pour représenter une mémoire ou une base de données pour les développeurs qui mettront en œuvre la conception préparée. Mais la conception elle-même (d'un point de vue commercial) n'a pas ou n'a pas besoin d'un référentiel.
berhalak

Réponses:


47

Il y a un peu de confusion ici. Les référentiels accèdent aux racines agrégées. Les racines agrégées sont des entités. La raison en est la séparation des préoccupations et une bonne stratification. Cela n'a pas de sens pour les petits projets, mais si vous faites partie d'une grande équipe, vous voulez dire: "Vous accédez à un produit via le référentiel de produits. Le produit est une racine agrégée pour une collection d'entités, y compris l'objet ProductCatalog. Si vous souhaitez mettre à jour le ProductCatalog, vous devez passer par le ProductRepository. »

De cette façon, vous avez une séparation très, très claire sur la logique métier et où les choses sont mises à jour. Vous n'avez pas d'enfant qui soit seul et qui écrit tout ce programme qui fait toutes ces choses compliquées au catalogue de produits et quand il s'agit de l'intégrer au projet en amont, vous êtes assis là à le regarder et à le réaliser. tout doit être abandonné. Cela signifie également que lorsque les gens rejoignent l'équipe, ajoutent de nouvelles fonctionnalités, ils savent où aller et comment structurer le programme.

Mais attendez! Le référentiel fait également référence à la couche de persistance, comme dans le modèle de référentiel. Dans un monde meilleur, un référentiel d'Eric Evans et le modèle de référentiel auraient des noms séparés, car ils ont tendance à se chevaucher un peu. Pour obtenir le modèle de référentiel, vous avez un contraste avec d'autres moyens d'accès aux données, avec un bus de service ou un système de modèle d'événement. Habituellement, lorsque vous arrivez à ce niveau, la définition du référentiel d'Eric Evans passe de côté et vous commencez à parler d'un contexte borné. Chaque contexte borné est essentiellement sa propre application. Vous disposez peut-être d'un système d'approbation sophistiqué pour insérer des éléments dans le catalogue de produits. Dans votre conception originale, le produit était la pièce maîtresse, mais dans ce contexte limité, le catalogue de produits l'est. Vous pouvez toujours accéder aux informations sur le produit et mettre à jour le produit via un bus de service,

Revenez à votre question initiale. Si vous accédez à un référentiel depuis une entité, cela signifie que l'entité n'est vraiment pas une entité commerciale mais probablement quelque chose qui devrait exister dans une couche de service. En effet, les entités sont des objets métier et doivent se préoccuper d'être autant que possible un DSL (langage spécifique au domaine). Avoir uniquement des informations commerciales dans cette couche. Si vous résolvez un problème de performances, vous saurez qu'il faut chercher ailleurs, car seules les informations commerciales doivent figurer ici. Si soudainement, vous avez des problèmes d'application ici, vous rendez très difficile l'extension et la maintenance d'une application, ce qui est vraiment le cœur de DDD: rendre le logiciel maintenable.

Réponse au commentaire 1 : Bien, bonne question. Ainsi, toutes les validations ne se produisent pas dans la couche de domaine. Sharp a un attribut "DomainSignature" qui fait ce que vous voulez. Il est conscient de la persistance, mais être un attribut maintient la couche de domaine propre. Cela garantit que vous n'avez pas d'entité en double avec, dans votre exemple, le même nom.

Mais parlons de règles de validation plus compliquées. Disons que vous êtes Amazon.com. Avez-vous déjà commandé quelque chose avec une carte de crédit expirée? J'ai, où je n'ai pas mis à jour la carte et acheté quelque chose. Il accepte la commande et l'interface utilisateur m'informe que tout est pêche. Environ 15 minutes plus tard, je recevrai un e-mail indiquant qu'il y a un problème avec ma commande, ma carte de crédit est invalide. Ce qui se passe ici, c'est que, idéalement, il y a une certaine validation regex dans la couche de domaine. Est-ce un bon numéro de carte de crédit? Si oui, persistez la commande. Cependant, il existe une validation supplémentaire au niveau de la couche des tâches d'application, où un service externe est interrogé pour voir si le paiement peut être effectué sur la carte de crédit. Sinon, n'envoyez rien, suspendez la commande et attendez le client.

N'ayez pas peur de créer des objets de validation au niveau de la couche de service qui peuvent accéder aux référentiels. Gardez-le simplement hors de la couche de domaine.


15
Merci. Mais je devrais m'efforcer d'obtenir autant de logique commerciale que possible dans les entités (et leurs usines et spécifications associées, etc.), n'est-ce pas? Mais si aucun d'entre eux n'est autorisé à récupérer des données via des référentiels, comment suis-je censé écrire une logique métier (raisonnablement compliquée)? Par exemple: l'utilisateur de Chatroom n'est pas autorisé à changer son nom en un nom déjà utilisé par quelqu'un d'autre. J'aimerais que cette règle soit intégrée par l'entité ChatUser, mais ce n'est pas très facile à faire si vous ne pouvez pas accéder au référentiel à partir de là. Donc qu'est ce que je devrais faire?
codeulike

Ma réponse était plus large que la zone de commentaire ne le permettait, voir la modification.
kertose

6
Votre entité doit savoir comment se protéger du mal. Cela inclut de s'assurer qu'il ne peut pas entrer dans un état invalide. Ce que vous décrivez avec l'utilisateur de la salle de conversation est une logique métier qui réside EN PLUS de la logique dont l'entité doit se maintenir valide. La logique métier comme ce que vous voulez appartient vraiment à un service Chatroom, pas à l'entité ChatUser.
Alec

9
Merci Alec. C'est une manière claire de l'exprimer. Mais il me semble que la règle d'or d'Evans centrée sur le domaine de «toute logique métier devrait aller dans la couche de domaine» est en conflit avec la règle «les entités ne devraient pas accéder aux référentiels». Je peux vivre avec cela si je comprends pourquoi, mais je ne trouve aucune bonne explication en ligne sur les raisons pour lesquelles les entités ne devraient pas accéder aux référentiels. Evans ne semble pas le mentionner explicitement. D'où vient-il? Si vous pouvez publier une réponse indiquant de la bonne littérature, vous pourrez peut-être vous empocher une prime de 50
points :)

4
"ça n'a pas de sens sur les petits" C'est une grosse erreur que font les équipes ... c'est un petit projet, en tant que tel je peux faire ceci et cela ... arrêter de penser comme ça. La plupart des petits projets avec lesquels nous travaillons finissent par devenir gros en raison des exigences de l'entreprise. Si vous faites quelque chose de petit ou de grand, faites-le correctement.
MeTitus

35

Au début, j'étais persuadé d'autoriser certaines de mes entités à accéder aux référentiels (c'est-à-dire le chargement paresseux sans ORM). Plus tard, je suis arrivé à la conclusion que je ne devrais pas et que je pourrais trouver des moyens alternatifs:

  1. Nous devons connaître nos intentions dans une requête et ce que nous attendons du domaine, nous pouvons donc faire des appels au référentiel avant de construire ou d'appeler le comportement Aggregate. Cela permet également d'éviter le problème de l'état de la mémoire incohérent et la nécessité d'un chargement différé (voir cet article ). L'odeur est que vous ne pouvez plus créer une instance en mémoire de votre entité sans vous soucier de l'accès aux données.
  2. CQS (Command Query Separation) peut aider à réduire le besoin d'appeler le référentiel pour les éléments de nos entités.
  3. Nous pouvons utiliser une spécification pour encapsuler et communiquer les besoins de la logique de domaine et la transmettre au référentiel à la place (un service peut orchestrer ces choses pour nous). La spécification peut provenir de l'entité chargée de maintenir cet invariant. Le référentiel interprétera des parties de la spécification dans sa propre implémentation de requête et appliquera les règles de la spécification sur les résultats de la requête. Cela vise à conserver la logique de domaine dans la couche de domaine. Il sert également mieux la langue omniprésente et la communication. Imaginez dire "spécification de commande en retard" au lieu de dire "ordre de filtre de tbl_order où placé_at est moins de 30 minutes avant sysdate" (voir cette réponse ).
  4. Cela rend le raisonnement sur le comportement des entités plus difficile car le principe de responsabilité unique est violé. Si vous avez besoin de résoudre des problèmes de stockage / persistance, vous savez où aller et où ne pas aller.
  5. Cela évite le danger de donner à une entité un accès bidirectionnel à l'état global (via le référentiel et les services de domaine). Vous ne voulez pas non plus briser la limite de votre transaction.

Vernon Vaughn dans le livre rouge Implementation Domain-Driven Design fait référence à ce problème à deux endroits que je connais (note: ce livre est entièrement approuvé par Evans comme vous pouvez le lire dans l'avant-propos). Dans le chapitre 7 sur les services, il utilise un service de domaine et une spécification pour contourner le besoin d'un agrégat d'utiliser un référentiel et un autre agrégat pour déterminer si un utilisateur est authentifié. Il a déclaré:

En règle générale, nous devrions essayer d'éviter l'utilisation de référentiels (12) depuis l'intérieur des agrégats, si possible.

Vernon, Vaughn (06/02/2013). Implémentation de la conception pilotée par domaine (emplacement Kindle 6089). Pearson Education. Édition Kindle.

Et dans le chapitre 10 sur les agrégats, dans la section intitulée "Navigation dans les modèles", il dit (juste après avoir recommandé l'utilisation d'identifiants uniques globaux pour référencer d'autres racines d'agrégats):

La référence par identité n'empêche pas complètement la navigation dans le modèle. Certains utiliseront un référentiel (12) à l'intérieur d'un agrégat pour la recherche. Cette technique est appelée modèle de domaine déconnecté, et c'est en fait une forme de chargement différé. Il existe cependant une approche différente recommandée: utilisez un référentiel ou un service de domaine (7) pour rechercher des objets dépendants avant d'appeler le comportement d'agrégation. Un service d'application client peut contrôler cela, puis envoyer à l'agrégat:

Il continue à montrer un exemple de ceci dans le code:

public class ProductBacklogItemService ... { 

   ... 
   @Transactional 
   public void assignTeamMemberToTask( 
        String aTenantId, 
        String aBacklogItemId, 
        String aTaskId, 
        String aTeamMemberId) { 

        BacklogItem backlogItem = backlogItemRepository.backlogItemOfId( 
                                        new TenantId( aTenantId), 
                                        new BacklogItemId( aBacklogItemId)); 

        Team ofTeam = teamRepository.teamOfId( 
                                  backlogItem.tenantId(), 
                                  backlogItem.teamId());

        backlogItem.assignTeamMemberToTask( 
                  new TeamMemberId( aTeamMemberId), 
                  ofTeam,
                  new TaskId( aTaskId));
   } 
   ...
}     

Il poursuit en mentionnant également une autre solution sur la façon dont un service de domaine peut être utilisé dans une méthode de commande agrégée avec une double répartition . (Je ne saurais trop recommander à quel point il est bénéfique de lire son livre. Une fois que vous êtes fatigué de fouiller sans fin sur Internet, versez l'argent bien mérité et lisez le livre.)

J'ai ensuite eu une discussion avec le toujours aimable Marco Pivetta @Ocramius qui m'a montré un peu de code pour extraire une spécification du domaine et l'utiliser:

1) Ce n'est pas recommandé:

$user->mountFriends(); // <-- has a repository call inside that loads friends? 

2) Dans un service de domaine, c'est bien:

public function mountYourFriends(MountFriendsCommand $mount) { /* see http://store.steampowered.com/app/296470/ */ 
    $user = $this->users->get($mount->userId()); 
    $friends = $this->users->findBySpecification($user->getFriendsSpecification()); 
    array_map([$user, 'mount'], $friends); 
}

1
Question: On nous apprend toujours à ne pas créer un objet dans un état invalide ou incohérent. Lorsque vous chargez des utilisateurs à partir du référentiel, puis que vous appelez getFriends()avant de faire quoi que ce soit d'autre, il sera vide ou chargé paresseusement. Si vide, alors cet objet est couché et dans un état invalide. Des pensées à ce sujet?
Jimbo

Le référentiel appelle le domaine pour créer une nouvelle instance. Vous n'obtenez pas une instance d'Utilisateur sans passer par le domaine. Le problème que cette réponse aborde est l'inverse. Où le domaine fait référence au référentiel, et cela doit être évité.
prograhammer

28

C'est une très bonne question. J'attends avec impatience une discussion à ce sujet. Mais je pense que c'est mentionné dans plusieurs livres de DDD et Jimmy Nilssons et Eric Evans. Je suppose que cela est également visible à travers des exemples d'utilisation du modèle de stockage.

MAIS permet de discuter. Je pense qu'une réflexion très valable est de savoir pourquoi une entité devrait-elle savoir comment persister une autre entité? L'important avec DDD est que chaque entité a la responsabilité de gérer sa propre «sphère de connaissances» et ne doit rien savoir sur la façon de lire ou d'écrire d'autres entités. Bien sûr, vous pouvez probablement simplement ajouter une interface de référentiel à l'entité A pour lire les entités B.

Comme vous pouvez le voir, l'entité A peut s'impliquer davantage dans le cycle de vie de l'entité B et cela peut ajouter plus de complexité au modèle.

Je suppose (sans aucun exemple) que les tests unitaires seront plus complexes.

Mais je suis sûr qu'il y aura toujours des scénarios où vous serez tenté d'utiliser des référentiels via des entités. Vous devez regarder chaque scénario pour porter un jugement valide. Avantages et inconvénients. Mais à mon avis, la solution de référentiel-entité commence par de nombreux inconvénients. Ce doit être un scénario très spécial avec des pros qui équilibrent les inconvénients ...


1
Bon point. Le modèle de domaine de la vieille école aurait probablement l'entité B responsable de se valider avant de se laisser persister, je suppose. Êtes-vous sûr qu'Evans mentionne les entités n'utilisant pas de référentiels? Je suis à mi-chemin du livre et il ne l'a pas encore mentionné ...
codeulike

Et bien j'ai lu le livre il y a plusieurs années (enfin 3 ...) et ma mémoire me fait défaut. Je ne me souviens pas s'il l'a exactement exprimé MAIS cependant je crois qu'il l'a illustré par des exemples. Vous pouvez également trouver une interprétation communautaire de son exemple Cargo (extrait de son livre) sur dddsamplenet.codeplex.com . Téléchargez le projet de code (regardez le projet Vanilla - c'est l'exemple du livre). Vous constaterez que les référentiels ne sont utilisés que dans la couche Application pour accéder aux entités du domaine.
Magnus Backeus

1
En téléchargeant l'exemple DDD SmartCA du livre p2p.wrox.com/ ... vous verrez une autre approche (bien que ce soit un client Windows RIA) où les référentiels sont utilisés dans les services (rien d'étrange ici) mais les services sont utilisés à l'intérieur d'entités. C'est quelque chose que je ne ferais pas MAIS je suis un gars d'applications Webb. Étant donné le scénario de l'application SmartCA où vous devez pouvoir travailler hors ligne, peut-être que la conception ddd aura un aspect différent.
Magnus Backeus

L'exemple SmartCA semble intéressant, dans quel chapitre se trouve-t-il? (les téléchargements de codes sont classés par chapitre)
codeulike

1
@codeulike Je conçois et implémente actuellement un framework utilisant les concepts ddd. Parfois, la validation nécessite d'accéder à la base de données et de l'interroger (exemple: requête pour une vérification d'index unique sur plusieurs colonnes). leurs interfaces de référentiel dans la couche de modèle de domaine afin de placer la validation complètement dans la couche de modèle de domaine. Alors, est-ce que les entités du domaine peuvent enfin avoir accès aux référentiels?
Karamafrooz

13

Pourquoi séparer l'accès aux données?

D'après le livre, je pense que les deux premières pages du chapitre Model Driven Design expliquent pourquoi vous voulez faire abstraction des détails techniques d'implémentation de l'implémentation du modèle de domaine.

  • Vous souhaitez garder un lien étroit entre le modèle de domaine et le code
  • Séparer les problèmes techniques permet de prouver que le modèle est pratique pour la mise en œuvre
  • Vous voulez que le langage omniprésent imprègne la conception du système

Tout cela semble avoir pour but d'éviter un «modèle d'analyse» distinct qui se dissocie de la mise en œuvre réelle du système.

D'après ce que je comprends du livre, il dit que ce «modèle d'analyse» peut finir par être conçu sans tenir compte de l'implémentation logicielle. Une fois que les développeurs essaient de mettre en œuvre le modèle compris par les entreprises, ils forment leurs propres abstractions par nécessité, ce qui crée un mur dans la communication et la compréhension.

Dans l'autre sens, les développeurs introduisant trop de problèmes techniques dans le modèle de domaine peuvent également provoquer cette fracture.

Ainsi, vous pourriez considérer que pratiquer la séparation des préoccupations telles que la persistance peut aider à se prémunir contre ces modèles de conception et d'analyse divergents. S'il s'avère nécessaire d'introduire des éléments comme la persistance dans le modèle, c'est un signal d'alarme. Peut-être que le modèle n'est pas pratique à mettre en œuvre.

Citant:

"Le modèle unique réduit les risques d'erreur, car la conception est désormais une conséquence directe du modèle soigneusement étudié. La conception, et même le code lui-même, a le pouvoir de communication d'un modèle."

La façon dont j'interprète cela, si vous vous retrouvez avec plus de lignes de code traitant de choses comme l'accès à la base de données, vous perdez cette capacité de communication.

Si le besoin d'accéder à une base de données est pour des choses comme la vérification de l'unicité, jetez un œil à:

Udi Dahan: les plus grosses erreurs commises par les équipes lors de l'application de DDD

http://gojko.net/2010/06/11/udi-dahan-the-biggest-mistakes-teams-make-when-applying-ddd/

sous "Toutes les règles ne sont pas égales"

et

Utilisation du modèle de modèle de domaine

http://msdn.microsoft.com/en-us/magazine/ee236415.aspx#id0400119

sous «Scénarios pour ne pas utiliser le modèle de domaine», qui aborde le même sujet.

Comment séparer l'accès aux données

Chargement de données via une interface

La "couche d'accès aux données" a été extraite via une interface que vous appelez pour récupérer les données requises:

var orderLines = OrderRepository.GetOrderLines(orderId);

foreach (var line in orderLines)
{
     total += line.Price;
}

Avantages: L'interface sépare le code de plomberie «d'accès aux données», vous permettant ainsi d'écrire des tests. L'accès aux données peut être géré au cas par cas, ce qui permet de meilleures performances qu'une stratégie générique.

Inconvénients: le code d'appel doit supposer ce qui a été chargé et ce qui ne l'est pas.

Dites que GetOrderLines renvoie des objets OrderLine avec une propriété ProductInfo nulle pour des raisons de performances. Le développeur doit avoir une connaissance approfondie du code derrière l'interface.

J'ai essayé cette méthode sur de vrais systèmes. Vous finissez par changer la portée de ce qui est chargé tout le temps pour tenter de résoudre les problèmes de performances. Vous finissez par jeter un œil derrière l'interface pour regarder le code d'accès aux données pour voir ce qui est et n'est pas en cours de chargement.

Désormais, la séparation des préoccupations devrait permettre au développeur de se concentrer sur un seul aspect du code à la fois, autant que possible. La technique d'interface supprime le COMMENT ces données sont chargées, mais pas COMBIEN de données sont chargées, QUAND elles sont chargées et O elles sont chargées.

Conclusion: séparation assez faible!

Chargement paresseux

Les données sont chargées à la demande. Les appels pour charger des données sont masqués dans le graphique d'objet lui-même, où l'accès à une propriété peut entraîner l'exécution d'une requête SQL avant de renvoyer le résultat.

foreach (var line in order.OrderLines)
{
    total += line.Price;
}

Avantages: Le «QUAND, O et COMMENT» de l'accès aux données est caché au développeur qui se concentre sur la logique du domaine. Il n'y a pas de code dans l'agrégat qui traite du chargement des données. La quantité de données chargées peut être la quantité exacte requise par le code.

Inconvénients: lorsque vous rencontrez un problème de performances, il est difficile de le résoudre lorsque vous disposez d'une solution générique «taille unique». Le chargement différé peut réduire les performances globales et la mise en œuvre du chargement différé peut être délicate.

Interface de rôle / récupération impatiente

Chaque cas d'utilisation est rendu explicite via une interface de rôle implémentée par la classe d'agrégat, permettant de gérer les stratégies de chargement de données par cas d'utilisation.

La stratégie de récupération peut ressembler à ceci:

public class BillOrderFetchingStrategy : ILoadDataFor<IBillOrder, Order>
{
    Order Load(string aggregateId)
    {
        var order = new Order();

        order.Data = GetOrderLinesWithPrice(aggregateId);
    
        return order;
    }

}
   

Ensuite, votre agrégat peut ressembler à:

public class Order : IBillOrder
{
    void BillOrder(BillOrderCommand command)
    {
        foreach (var line in this.Data.OrderLines)
        {
            total += line.Price;
        }

        etc...
    }
}

BillOrderFetchingStrategy est utilisé pour créer l'agrégat, puis l'agrégat fait son travail.

Avantages: permet un code personnalisé par cas d'utilisation, permettant des performances optimales. Est conforme au principe de séparation des interfaces . Aucune exigence de code complexe. Les tests unitaires d'agrégats n'ont pas à imiter la stratégie de chargement. Une stratégie de chargement générique peut être utilisée dans la majorité des cas (par exemple, une stratégie de "chargement tout") et des stratégies de chargement spéciales peuvent être mises en œuvre si nécessaire.

Inconvénients: le développeur doit encore ajuster / revoir la stratégie de récupération après avoir changé le code de domaine.

Avec l'approche de la stratégie de récupération, vous pouvez toujours changer le code de récupération personnalisé pour une modification des règles métier. Ce n'est pas une séparation parfaite des préoccupations mais finira par être plus facile à entretenir et est meilleure que la première option. La stratégie de récupération encapsule les données HOW, WHEN et WHERE. Il a une meilleure séparation des préoccupations, sans perdre en flexibilité comme l'approche de chargement paresseux.


Merci, je vais vérifier les liens. Mais dans votre réponse, confondez-vous «séparation des préoccupations» et «pas d'accès du tout»? La plupart des gens conviendraient certainement que la couche de persistance devrait être séparée de la couche dans laquelle se trouvent les entités. interface'.
codeulike

Chargement des données via une interface ou non, vous êtes toujours préoccupé par le chargement des données lors de la mise en œuvre des règles métier. Je conviens que beaucoup de gens appellent encore cette séparation des préoccupations, mais peut-être que le principe de responsabilité unique aurait été un meilleur terme à utiliser.
ttg le

1
Vous ne savez pas comment analyser votre dernier commentaire, mais je pense que vous suggérez que les données ne devraient pas être chargées lors du traitement des règles métier? Je vois que cela rendrait les règles «plus pures». Mais de nombreux types de règles métier devront faire référence à d'autres données - suggérez-vous qu'elles soient chargées à l'avance par un objet séparé?
codeulike

@codeulike: J'ai mis à jour ma réponse. Vous pouvez toujours charger des données pendant les règles métier si vous pensez que vous devez absolument le faire, mais cela ne nécessite pas l'ajout de lignes de code d'accès aux données dans votre modèle de domaine (par exemple, lazy load). Dans les modèles de domaine que j'ai conçus, les données sont généralement simplement chargées à l'avance, comme vous l'avez dit. J'ai constaté que l'exécution de règles métier ne nécessite généralement pas une quantité excessive de données.
ttg


12

Quelle excellente question. Je suis sur le même chemin de découverte et la plupart des réponses sur Internet semblent apporter autant de problèmes qu'elles apportent de solutions.

Alors (au risque d'écrire quelque chose avec lequel je ne suis pas d'accord dans un an) voici mes découvertes jusqu'à présent.

Tout d'abord, nous aimons un modèle de domaine riche , qui nous donne une grande découvrabilité (de ce que nous pouvons faire avec un agrégat) et une lisibilité (appels de méthodes expressives).

// Entity
public class Invoice
{
    ...
    public void SetStatus(StatusCode statusCode, DateTime dateTime) { ... }
    public void CreateCreditNote(decimal amount) { ... }
    ...
}

Nous voulons y parvenir sans injecter de services dans le constructeur d'une entité, car:

  • L'introduction d'un nouveau comportement (qui utilise un nouveau service) pourrait conduire à un changement de constructeur, ce qui signifie que le changement affecte chaque ligne qui instancie l'entité !
  • Ces services ne font pas partie du modèle , mais l'injection de constructeur suggérerait qu'ils l'étaient.
  • Souvent, un service (même son interface) est un détail d'implémentation plutôt qu'une partie du domaine. Le modèle de domaine aurait une dépendance tournée vers l'extérieur .
  • La raison pour laquelle l'entité ne peut exister sans ces dépendances peut prêter à confusion . (Un service de note de crédit, dites-vous? Je ne vais même pas faire quoi que ce soit avec les notes de crédit ...)
  • Cela rendrait l'instanciation difficile, donc difficile à tester .
  • Le problème se propage facilement, car d'autres entités contenant celle-ci obtiendraient les mêmes dépendances - ce qui sur elles peut ressembler à des dépendances très artificielles .

Alors, comment pouvons-nous faire cela? Ma conclusion jusqu'à présent est que les dépendances de méthode et la double répartition fournissent une solution décente.

public class Invoice
{
    ...

    // Simple method injection
    public void SetStatus(IInvoiceLogger logger, StatusCode statusCode, DateTime dateTime)
    { ... }

    // Double dispatch
    public void CreateCreditNote(ICreditNoteService creditNoteService, decimal amount)
    {
        creditNoteService.CreateCreditNote(this, amount);
    }

    ...
}

CreateCreditNote()nécessite désormais un service chargé de créer des notes de crédit. Il utilise une double répartition , déchargeant entièrement le travail vers le service responsable, tout en maintenant la découvrabilité de l' Invoiceentité.

SetStatus()a maintenant une simple dépendance sur un enregistreur, qui assurera évidemment une partie du travail .

Pour ce dernier, pour faciliter les choses sur le code client, nous pourrions plutôt nous connecter via un fichier IInvoiceService. Après tout, la journalisation des factures semble assez intrinsèque à une facture. Un tel simple IInvoiceServicepermet d'éviter le besoin de toutes sortes de mini-services pour diverses opérations. L'inconvénient est qu'il est exactement ce que obscurcir service faire . Cela pourrait même commencer à ressembler à une double répartition, alors que la plupart du travail est encore fait en SetStatus()soi.

Nous pourrions encore nommer le paramètre «logger», dans l'espoir de révéler notre intention. Cela semble un peu faible, cependant.

Au lieu de cela, je choisirais de demander un IInvoiceLogger(comme nous le faisons déjà dans l'exemple de code) et d' IInvoiceServiceimplémenter cette interface. Le code client peut simplement utiliser son unique IInvoiceServicepour toutes les Invoiceméthodes qui demandent un tel `` mini-service '' très particulier, intrinsèque à la facture, tandis que les signatures de méthode indiquent toujours clairement ce qu'elles demandent.

Je remarque que je n'ai pas abordé explicitement les référentiels . Eh bien, l'enregistreur est ou utilise un référentiel, mais permettez-moi également de fournir un exemple plus explicite. Nous pouvons utiliser la même approche, si le référentiel est nécessaire dans une ou deux méthodes.

public class Invoice
{
    public IEnumerable<CreditNote> GetCreditNotes(ICreditNoteRepository repository)
    { ... }
}

En fait, cela fournit une alternative aux charges paresseuses toujours ennuyeuses .

Mise à jour: J'ai laissé le texte ci-dessous à des fins historiques, mais je suggère d'éviter les charges paresseuses à 100%.

Pour les vrais, les charges paresseux basées sur la propriété, je n'utilise actuellement l' injection de constructeur, mais d'une manière de persistance ignorant.

public class Invoice
{
    // Lazy could use an interface (for contravariance if nothing else), but I digress
    public Lazy<IEnumerable<CreditNote>> CreditNotes { get; }

    // Give me something that will provide my credit notes
    public Invoice(Func<Invoice, IEnumerable<CreditNote>> lazyCreditNotes)
    {
        this.CreditNotes = new Lazy<IEnumerable<CreditNotes>>() => lazyCreditNotes(this));
    }
}

D'une part, un référentiel qui charge un à Invoicepartir de la base de données peut avoir un accès libre à une fonction qui chargera les notes de crédit correspondantes et injectera cette fonction dans le fichier Invoice.

D'un autre côté, le code qui crée un nouveau réel Invoicepassera simplement une fonction qui renvoie une liste vide:

new Invoice(inv => new List<CreditNote>() as IEnumerable<CreditNote>)

(Une coutume ILazy<out T>pourrait nous débarrasser de la laideur IEnumerable, mais cela compliquerait la discussion.)

// Or just an empty IEnumerable
new Invoice(inv => IEnumerable.Empty<CreditNote>())

Je serais ravi d'entendre vos opinions, préférences et améliorations!


3

Pour moi, cela semble être une bonne pratique générale liée à l'OOD plutôt que d'être spécifique au DDD.

Les raisons auxquelles je peux penser sont:

  • Séparation des préoccupations (les entités doivent être séparées de la façon dont elles sont persistantes. Car il pourrait y avoir plusieurs stratégies dans lesquelles la même entité serait persistante en fonction du scénario d'utilisation)
  • Logiquement, les entités peuvent être vues à un niveau inférieur au niveau dans lequel les référentiels fonctionnent. Les composants de niveau inférieur ne doivent pas avoir de connaissances sur les composants de niveau supérieur. Par conséquent, les entrées ne doivent pas avoir de connaissances sur les référentiels.

2

simplement Vernon Vaughn donne une solution:

Utilisez un référentiel ou un service de domaine pour rechercher des objets dépendants avant d'appeler le comportement d'agrégation. Un service d'application client peut contrôler cela.


Mais pas d'une entité.
ssmith

De Vernon Vaughn IDDD source: public class Calendar extend EventSourcedRootEntity {... public CalendarEntry scheduleCalendarEntry (CalendarIdentityService aCalendarIdentityService,
Teimuraz

Consultez son article @Teimuraz
Alireza Rahmani Khalili

1

J'ai appris à coder la programmation orientée objet avant que tout ce bourdonnement de couche séparé n'apparaisse, et mes premiers objets / classes DID mappent directement à la base de données.

Finalement, j'ai ajouté une couche intermédiaire car je devais migrer vers un autre serveur de base de données. J'ai vu / entendu parler du même scénario plusieurs fois.

Je pense que séparer l'accès aux données (aka "Repository") de votre logique métier, est une de ces choses, qui ont été réinventées à plusieurs reprises, même si le livre Domain Driven Design, en fait beaucoup de "bruit".

J'utilise actuellement 3 couches (GUI, Logic, Data Access), comme beaucoup de développeurs le font, car c'est une bonne technique.

La séparation des données, en une Repositorycouche (aka Data Accesslayer), peut être considérée comme une bonne technique de programmation, pas seulement comme une règle, à suivre.

Comme beaucoup de méthodologies, vous voudrez peut-être commencer, en NON implémenté, et éventuellement mettre à jour votre programme, une fois que vous les aurez compris.

Citation: L'Iliade n'a pas été totalement inventée par Homer, Carmina Burana n'a pas été totalement inventée par Carl Orff, et dans les deux cas, la personne qui a mis les autres au travail, tout ensemble, a obtenu le crédit ;-)


1
Merci, mais je ne demande pas de séparer l'accès aux données de la logique métier - c'est une chose très claire sur laquelle il y a un très large accord. Je demande pourquoi dans les architectures DDD telles que S # arp, les entités ne sont même pas autorisées à «parler» à la couche d'accès aux données. C'est un arrangement intéressant sur lequel je n'ai pas pu trouver beaucoup de discussions.
codeulike

0

Cela vient-il du livre d'Eric Evans Domain Driven Design, ou est-il venu d'ailleurs?

C'est du vieux truc. Le livre d'Eric l'a juste fait vibrer un peu plus.

Où y a-t-il de bonnes explications pour le raisonnement derrière cela?

La raison est simple - l'esprit humain s'affaiblit lorsqu'il fait face à de multiples contextes vaguement liés. Elles conduisent à l'ambiguïté (l'Amérique du Sud / Amérique du Nord signifie l'Amérique du Sud / du Nord), l'ambiguïté conduit à une cartographie constante de l'information chaque fois que l'esprit "la touche" et cela se résume à une mauvaise productivité et à des erreurs.

La logique métier doit être reflétée aussi clairement que possible. Les clés étrangères, la normalisation, le mappage relationnel des objets sont d'un domaine complètement différent - ces choses sont techniques, liées à l'ordinateur.

Par analogie: si vous apprenez à écrire à la main, vous ne devriez pas avoir à comprendre où le stylo a été fabriqué, pourquoi l'encre tient sur le papier, quand le papier a été inventé et quelles sont les autres inventions chinoises célèbres.

edit: Pour clarifier: je ne parle pas de la pratique OO classique de séparer l'accès aux données dans une couche distincte de la logique métier - je parle de l'arrangement spécifique par lequel dans DDD, les entités ne sont pas censées parler aux données couche d'accès du tout (c'est-à-dire qu'ils ne sont pas censés contenir des références aux objets du référentiel)

La raison est toujours la même que j'ai mentionnée ci-dessus. Ici, c'est juste un pas de plus. Pourquoi les entités devraient être partiellement ignorantes de la persistance si elles peuvent être (au moins proches de) totalement? Moins de problèmes sans rapport avec le domaine que notre modèle tient - plus de marge de manœuvre pour notre esprit quand il doit le réinterpréter.


Droite. Alors, comment une entité totalement ignorante de la persistance met-elle en œuvre la logique métier si elle n'est même pas autorisée à parler à la couche de persistance? Que fait-il quand il a besoin d'examiner des valeurs dans d'autres entités arbitraires?
codeulike

Si votre entité a besoin d'examiner les valeurs dans d'autres entités arbitraires, vous rencontrez probablement des problèmes de conception. Envisagez peut-être de séparer les classes afin qu'elles soient plus cohérentes.
cdaq

0

Pour citer Carolina Lilientahl, «Les modèles devraient empêcher les cycles» https://www.youtube.com/watch?v=eJjadzMRQAk , où elle fait référence aux dépendances cycliques entre les classes. Dans le cas de référentiels à l'intérieur d'agrégats, il y a une tentation de créer des dépendances cycliques hors de la convenance de la navigation d'objets comme seule raison. Le modèle mentionné ci-dessus par prograhammer, qui a été recommandé par Vernon Vaughn, où d'autres agrégats sont référencés par des ids au lieu d'instances racine, (y a-t-il un nom pour ce modèle?) Suggère une alternative qui pourrait guider vers d'autres solutions.

Exemple de dépendance cyclique entre classes (confession):

(Time0): Deux classes, Sample et Well, se réfèrent l'une à l'autre (dépendance cyclique). Well fait référence à Sample, et Sample renvoie à Well, par commodité (parfois en boucle des échantillons, parfois en boucle tous les puits d'une plaque). Je ne pouvais pas imaginer des cas où Sample ne ferait pas référence au puits où il est placé.

(Time1): Un an plus tard, de nombreux cas d'utilisation sont mis en œuvre ... et il y a maintenant des cas où Sample ne devrait pas renvoyer au puits dans lequel il est placé. Il y a des plaques temporaires dans une étape de travail. Ici, un puits fait référence à un échantillon, qui à son tour fait référence à un puits sur une autre plaque. Pour cette raison, un comportement étrange se produit parfois lorsque quelqu'un tente d'implémenter de nouvelles fonctionnalités. Prend du temps pour pénétrer.

J'ai également été aidé par cet article mentionné ci-dessus sur les aspects négatifs du chargement paresseux.


-1

Dans le monde idéal, DDD propose que les entités ne devraient pas avoir de référence aux couches de données. mais nous ne vivons pas dans un monde idéal. Les domaines peuvent avoir besoin de faire référence à d'autres objets de domaine pour la logique métier avec lesquels ils peuvent ne pas avoir de dépendance. Il est logique que les entités se réfèrent à la couche de référentiel à des fins de lecture seule, pour récupérer les valeurs.


Non, cela introduit un couplage inutile aux entités, viole le SRP et la séparation des préoccupations, et rend difficile la désérialisation de l'entité de la persistance (puisque le processus de désérialisation doit maintenant également injecter des services / référentiels que l'entité fréquente).
ssmith
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.