web-dev-qa-db-fra.com

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

Dans la conception pilotée par domaine, il semble y avoir lots sur accord que les entités ne doivent pas accéder directement aux référentiels.

Est-ce que cela vient d'Eric Evans Domain Driven Design livre, ou est-ce 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 classique OO 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 selon lequel dans DDD, Les entités ne sont pas censées parler du tout à la couche d'accès aux données (c'est-à-dire qu'elles ne sont pas censées 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 encore 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 un grand 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.

160
codeulike

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 sur 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 est seul et écrit tout ce programme qui fait toutes ces choses compliquées dans le 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, le référentiel d'Eric Evans et le modèle de référentiel auraient des noms distincts, car ils ont tendance à se chevaucher un peu. Pour obtenir le modèle de référentiel, vous avez un contraste avec d'autres façons d'accéder aux données, avec un bus de service ou un système de modèle d'événement. Habituellement, lorsque vous atteignez ce niveau, la définition du référentiel d'Eric Evans passe par le chemin et vous commencez à parler d'un contexte borné. Chaque contexte borné est essentiellement sa propre application. Vous pourriez avoir un système d'approbation sophistiqué pour mettre des choses dans le catalogue de produits. Dans votre conception d'origine, le produit était la pièce maîtresse, mais dans ce contexte dé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, mais vous devez comprendre qu'un catalogue de produits en dehors du contexte délimité peut signifier quelque chose de complètement différent.

Revenons à votre question d'origine. Si vous accédez à un référentiel à partir d'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 de ressembler autant que possible à un DSL (langage spécifique au domaine). Ne disposez que d'informations commerciales dans cette couche. Si vous dépannez un problème de performances, vous saurez chercher ailleurs car seules les informations commerciales doivent être 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: créer un logiciel maintenable.

Réponse au commentaire 1: Oui, bonne question. Donc toutes les validations ne se produisent pas dans la couche domaine. Sharp a un attribut "DomainSignature" qui fait ce que vous voulez. Il est sensible à la persistance, mais étant un attribut, la couche de domaine reste propre. Il 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 m'informant qu'il y a un problème avec ma commande, ma carte de crédit n'est pas valide. Ce qui se passe ici, c'est que, idéalement, il y a une validation d'expression régulière dans la couche de domaine. S'agit-il d'un numéro de carte de crédit correct? 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. Tout cela devrait avoir lieu dans une couche de service.

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.

44
kertosis

Au début, j'étais persuadé de permettre à certaines de mes entités d'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 pouvais trouver d'autres moyens:

  1. Nous devons connaître nos intentions dans une demande et ce que nous voulons du domaine, nous pouvons donc effectuer des appels de référentiel avant de construire ou d'appeler un comportement d'agrégation. Cela permet également d'éviter le problème de l'état incohérent de la mémoire et la nécessité d'un chargement paresseux (voir ceci article ). L'odeur est que vous ne pouvez plus créer d'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 de vouloir appeler le référentiel pour des choses dans nos entités.
  3. Nous pouvons utiliser une spécification pour encapsuler et communiquer les besoins de logique du domaine et les 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 aux résultats de la requête. Cela vise à conserver la logique du domaine dans la couche domaine. Il sert également mieux la langue omniprésente et la communication. Imaginez dire "spécification d'ordre en retard" par opposition à "ordre de filtre de tbl_order où placé_at est moins de 30 minutes avant sysdate" (voir ceci 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 devez résoudre des problèmes de stockage/persistance, vous savez où aller et où ne pas aller.
  5. Il é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 casser votre frontière de transaction.

Vernon Vaughn dans le livre rouge Implementing 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 la nécessité pour un agrégat d'utiliser un référentiel et un autre agrégat pour déterminer si un utilisateur est authentifié. Il aurait déclaré:

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

Vernon, Vaughn (2013-02-06). Implémentation d'une conception pilotée par domaine (emplacement Kindle 6089). Éducation Pearson. Édition Kindle.

Et dans le chapitre 10 sur les agrégats, dans la section intitulée "Model Navigation" , il dit (juste après avoir recommandé l'utilisation d'ID 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 paresseux. Il existe cependant une approche différente: utilisez un référentiel ou un service de domaine (7) pour rechercher des objets dépendants avant d'invoquer le comportement d'agrégation. Un service d'application client peut contrôler cela, puis envoyer à l'agrégat:

Il montre un exemple de cela 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 mentionne également une autre solution sur la façon dont un service de domaine peut être utilisé dans une méthode de commande Aggregate avec double-dispatch. (Je ne saurais trop recommander à quel point il est bénéfique de lire son livre. Après avoir fatigué de fouiller sans fin sur Internet, fouillez l'argent bien mérité et lisez le livre.)

J'ai ensuite eu quelques 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); 
}
31
prograhammer

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 DDD et Jimmy nilssons et Eric Evans. Je suppose qu'il est également visible à travers des exemples d'utilisation du modèle de référentiel.

MAIS permet de discuter. Je pense qu'une pensée très valable est 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 devrait 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. Mais le risque est que vous exposiez les connaissances sur la persistance B. L'entité A fera-t-elle également la validation sur B avant de persister B dans db?

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 la solution d'entité de référentiel à mon avis commence par beaucoup d'inconvénients. Ce doit être un scénario très spécial avec des avantages qui équilibrent les inconvénients ....

27
Magnus Backeus

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 donnent une justification de la raison pour laquelle vous voulez soustraire les détails techniques de l'implémentation de l'implémentation du modèle de domaine.

  • Vous souhaitez maintenir une connexion étroite entre le modèle de domaine et le code
  • La séparation des préoccupations techniques permet de prouver que le modèle est pratique pour sa mise en œuvre
  • Vous voulez que le langage omniprésent imprègne la conception du système

Cela semble être tout dans le but d'éviter un "modèle d'analyse" séparé 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 considérer la mise en œuvre logicielle. Une fois que les développeurs tentent de mettre en œuvre le modèle compris par le côté commercial, ils forment leurs propres abstractions en raison de la 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 ce fossé.

Vous pourriez donc 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 est nécessaire d'introduire des choses comme la persistance dans le modèle, c'est un drapeau rouge. 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 maintenant une excroissance directe du modèle soigneusement considéré. La conception, et même le code lui-même, ont la communicativité 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 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 des é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 créées égales"

et

Utilisation du modèle de modèle de domaine

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

sous "Scénarios de non utilisation du modèle de domaine", qui touche au 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é résumée via une interface que vous appelez afin de 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 "accès aux données", vous permettant d'écrire toujours 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 appelant doit supposer ce qui a été chargé et ce qui ne l'a pas été.

Supposons 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 des systèmes réels. 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 aspect du code à la fois, autant que possible. La technique d'interface supprime le mode de chargement de ces données, mais pas le nombre de données chargées, QUAND elles sont chargées et O WH elles sont chargées.

Conclusion: séparation assez faible!

Chargement paresseux

Les données sont chargées à la demande. Les appels à 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, OERE et COMMENT" de l'accès aux données est caché au développeur en se concentrant 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 êtes confronté à 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 paresseux peut entraîner des performances globales moins bonnes et l'implémentation du chargement paresseux peut être délicate.

Interface de rôle/Récupération désireuse

Chaque cas d'utilisation est rendu explicite via une Role Interface 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;
    }

}

Votre agrégat peut alors ressembler à:

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

        etc...
    }
}

Le BillOrderFetchingStrategy est utilisé pour construire 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égrégation d'interface . 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 complet") et des stratégies de chargement spéciales peuvent être mises en œuvre si nécessaire.

Inconvénients: le développeur doit toujours 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 vous retrouver à modifier le code de récupération personnalisé pour modifier les règles métier. Ce n'est pas une séparation parfaite des préoccupations, mais cela finira par être plus maintenable et c'est mieux que la première option. La stratégie de récupération encapsule les données COMMENT, QUAND et OERE est chargé. Il a une meilleure séparation des préoccupations, sans perdre en flexibilité comme l'approche taille unique pour tous les chargements paresseux.

12
tomtg

J'ai trouvé que ce blog avait de très bons arguments contre l'encapsulation des référentiels à l'intérieur des entités:

http://thinkbeforecoding.com/post/2009/03/04/How-not-to-inject-services-in-entities

11
ahaaman

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.

Donc (au risque d'écrire quelque chose avec quoi je ne suis pas d'accord dans un an) voici mes découvertes à ce jour.

Tout d'abord, nous aimons un modèle de domaine riche, ce qui nous donne un haut découverte (de ce que nous pouvons faire avec un agrégat) et lisibilité ( appels de méthode expressive).

// 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 le changement affecte chaque ligne qui instancie l'entité!
  • Ces les services ne font pas partie du modèle, mais l'injection par le constructeur suggère qu'ils le sont.
  • 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.
  • Cela peut être confus pourquoi l'entité ne peut pas exister sans ces dépendances. (Un service de notes de crédit, dites-vous? Je ne vais même rien faire avec des 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 - qui sur elles peuvent ressembler à très dépendances non naturelles.

Comment, alors, pouvons-nous faire cela? Ma conclusion à ce jour est que dépendances de méthode et 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() requiert désormais un service qui est responsable de la création des notes de crédit. Il utilise double répartition, entièrement déchargement du travail au service responsable, tandis que maintien de la découvrabilité de l'entité Invoice.

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

Pour ce dernier, pour faciliter les choses sur le code client, nous pourrions plutôt nous connecter via un IInvoiceService. Après tout, l'enregistrement des factures semble assez intrinsèque à une facture. Un tel IInvoiceService unique permet d'éviter la nécessité de toutes sortes de mini-services pour diverses opérations. L'inconvénient est qu'il devient obscur ce que fera exactement ce service . Il pourrait même commencer à ressembler à une double répartition, alors que la plupart du travail se fait encore dans SetStatus() lui-même.

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

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

Je constate que je n'ai pas adressé les référentiels de manière expresse. 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 n'est nécessaire que dans une ou deux méthodes.

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

En fait, cela fournit une alternative à la toujours ennuyeuse charges paresseuses.

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

Pour les vraies charges paresseuses basées sur les propriétés, je utilise actuellement l'injection de constructeur, mais d'une manière ignorante de la persistance.

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 Invoice à partir de la base de données peut avoir un accès gratuit à une fonction qui chargera les notes de crédit correspondantes et injectera cette fonction dans le Invoice.

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

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

(Un ILazy<out T> Personnalisé pourrait nous débarrasser du vilain cast en IEnumerable, mais cela compliquerait la discussion.)

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

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

9
Timo

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 pourraient ê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
user1502505

vernon Vaughn propose simplement une solution:

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

1

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

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

Je pense que séparer l'accès aux données (alias "Référentiel") de votre logique métier, est l'une de ces choses, qui ont été réinventées plusieurs fois, à travers le livre Domain Driven Design, cela fait beaucoup de "bruit".

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

Séparation des données, dans une couche Repository (alias Data Access layer), peut être considérée comme une bonne technique de programmation, pas seulement une règle, à suivre.

Comme de nombreuses méthodologies, vous souhaiterez peut-être commencer, en l'absence de mise en œuvre, 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
umlcat

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 existe une tentation de créer des dépendances cycliques par commodité de la navigation d'objet 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 identifiants au lieu d'instances racine, (existe-t-il un nom pour ce modèle?) Suggère une alternative qui pourrait guider dans 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 se réfère à Well, par commodité (parfois en boucle d'é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 existe maintenant des cas où l'échantillon ne doit 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 essaie d'implémenter de nouvelles fonctionnalités. Prend du temps à pénétrer.

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

0
Edvard Englund

Est-ce que cela vient du livre d'Eric Evans Domain Driven Design, ou est-ce venu d'ailleurs?

Ce sont des vieux trucs. Le livre d'Eric vient de le faire 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 à des contextes multiples vaguement liés. Ils conduisent à l'ambiguïté (l'Amérique en Amérique du Sud/Amérique du Nord signifie Amérique du Sud/Amérique du Nord), l'ambiguïté conduit à une cartographie constante des informations chaque fois que l'esprit "les touche" et cela se résume en 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 être chargé de 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 classique OO 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 selon lequel dans DDD, Les entités ne sont pas censées parler du tout à la couche d'accès aux données (c'est-à-dire qu'elles ne sont pas censées contenir des références aux objets du référentiel)

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

0
Arnis Lapsa