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.
Réponses:
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.
la source
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:
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é:
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):
Il continue à montrer un exemple de ceci dans le code:
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é:
2) Dans un service de domaine, c'est bien:
la source
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?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 ...
la source
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.
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:
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.
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:
Ensuite, votre agrégat peut ressembler à:
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.
la source
J'ai trouvé que ce blog avait de très bons arguments contre l'encapsulation des référentiels dans les entités:
http://thinkbeforecoding.com/post/2009/03/04/How-not-to-inject-services-in-entities
la source
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).
Nous voulons y parvenir sans injecter de services dans le constructeur d'une entité, car:
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.
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'Invoice
entité.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 simpleIInvoiceService
permet 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 enSetStatus()
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'IInvoiceService
implémenter cette interface. Le code client peut simplement utiliser son uniqueIInvoiceService
pour toutes lesInvoice
mé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.
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.
D'une part, un référentiel qui charge un à
Invoice
partir 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 fichierInvoice
.D'un autre côté, le code qui crée un nouveau réel
Invoice
passera simplement une fonction qui renvoie une liste vide:(Une coutume
ILazy<out T>
pourrait nous débarrasser de la laideurIEnumerable
, mais cela compliquerait la discussion.)Je serais ravi d'entendre vos opinions, préférences et améliorations!
la source
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:
la source
simplement Vernon Vaughn donne une solution:
la source
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
Repository
couche (akaData Access
layer), 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 ;-)
la source
C'est du vieux truc. Le livre d'Eric l'a juste fait vibrer un peu plus.
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.
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.
la source
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.
la source
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.
la source