DDD - die Regel, dass Entitäten nicht direkt auf Repositorys zugreifen können

183

Im Domain - Driven Design, es scheint zu sein , viel von Vereinbarung , dass Unternehmen sollten keinen Zugang Repositorys direkt.

Kam dies aus dem Buch " Eric Evans Domain Driven Design " oder von woanders?

Wo gibt es einige gute Erklärungen für die Gründe dafür?

Bearbeiten: Zur Verdeutlichung: Ich spreche nicht über die klassische OO-Praxis, den Datenzugriff in eine separate Ebene von der Geschäftslogik zu trennen - ich spreche über die spezifische Anordnung, wonach Entitäten in DDD nicht mit den Daten sprechen sollen Zugriffsebene überhaupt (dh sie dürfen keine Verweise auf Repository-Objekte enthalten)

Update: Ich habe BacceSR das Kopfgeld gegeben, weil seine Antwort am nächsten schien, aber ich bin immer noch ziemlich im Dunkeln darüber. Wenn es ein so wichtiges Prinzip ist, sollte es doch irgendwo online gute Artikel darüber geben?

Update: März 2013, die positiven Stimmen zu dieser Frage deuten darauf hin, dass großes Interesse daran besteht, und obwohl es viele Antworten gab, denke ich immer noch, dass es Raum für mehr gibt, wenn die Leute Ideen dazu haben.

codeulike
quelle
Werfen Sie einen Blick auf meine Frage stackoverflow.com/q/8269784/235715 . Sie zeigt eine Situation, in der es schwierig ist, Logik zu erfassen, ohne dass Entity Zugriff auf das Repository hat. Obwohl ich denke, dass Entitäten keinen Zugriff auf Repositorys haben sollten, gibt es eine Lösung für meine Situation, in der Code ohne Repository-Referenz neu geschrieben werden kann, aber derzeit fällt mir keine ein.
Alex Burtsev
Ich weiß nicht, woher es kam. Meine Gedanken: Ich denke, dass dieses Missverständnis von Menschen kommt, die nicht verstehen, worum es bei DDD geht. Dieser Ansatz dient nicht zur Implementierung von Software, sondern zum Entwerfen (Domain .. Design). Früher hatten wir Architekten und Implementierer, aber jetzt gibt es nur noch Softwareentwickler. DDD ist für Architekten gedacht. Und wenn ein Architekt Software entwirft, benötigt er ein Werkzeug oder Muster, um einen Speicher oder eine Datenbank für Entwickler darzustellen, die das vorbereitete Design implementieren. Das Design selbst (aus geschäftlicher Sicht) hat oder benötigt jedoch kein Repository.
Berhalak

Antworten:

47

Hier herrscht ein bisschen Verwirrung. Repositorys greifen auf aggregierte Roots zu. Aggregierte Wurzeln sind Entitäten. Der Grund dafür ist die Trennung von Bedenken und eine gute Schichtung. Dies ist bei kleinen Projekten nicht sinnvoll. Wenn Sie jedoch in einem großen Team arbeiten, möchten Sie sagen: "Sie greifen über das Produkt-Repository auf ein Produkt zu. Das Produkt ist ein aggregierter Stamm für eine Sammlung von Entitäten, einschließlich des ProductCatalog-Objekts. Wenn Sie den ProductCatalog aktualisieren möchten, müssen Sie das ProductRepository durchlaufen. "

Auf diese Weise haben Sie eine sehr, sehr klare Trennung in Bezug auf die Geschäftslogik und wo die Dinge aktualisiert werden. Sie haben kein Kind, das alleine unterwegs ist und das gesamte Programm schreibt, das all diese komplizierten Dinge mit dem Produktkatalog macht, und wenn es darum geht, es in das vorgelagerte Projekt zu integrieren, sitzen Sie da und schauen es sich an und realisieren es alles muss weggeworfen werden. Wenn Mitarbeiter dem Team beitreten, neue Funktionen hinzufügen, wissen sie, wohin sie gehen und wie sie das Programm strukturieren müssen.

Aber warte! Repository bezieht sich auch auf die Persistenzschicht, wie im Repository-Muster. In einer besseren Welt hätten das Repository von Eric Evans und das Repository-Muster getrennte Namen, da sie sich häufig überschneiden. Um das Repository-Muster zu erhalten, haben Sie einen Kontrast zu anderen Arten des Datenzugriffs, mit einem Servicebus oder einem Ereignismodellsystem. Wenn Sie diese Ebene erreichen, bleibt die Repository-Definition von Eric Evans normalerweise nebenbei und Sie sprechen über einen begrenzten Kontext. Jeder begrenzte Kontext ist im Wesentlichen eine eigene Anwendung. Möglicherweise verfügen Sie über ein ausgeklügeltes Genehmigungssystem, um Inhalte in den Produktkatalog aufzunehmen. In Ihrem ursprünglichen Design war das Produkt das Herzstück, aber in diesem begrenzten Kontext ist der Produktkatalog. Sie können weiterhin über einen Servicebus auf Produktinformationen zugreifen und das Produkt aktualisieren.

Zurück zu Ihrer ursprünglichen Frage. Wenn Sie von einer Entität aus auf ein Repository zugreifen, bedeutet dies, dass die Entität wirklich keine Geschäftsentität ist, sondern wahrscheinlich etwas, das in einer Serviceschicht vorhanden sein sollte. Dies liegt daran, dass Entitäten Geschäftsobjekte sind und sich darum bemühen sollten, einer DSL (domänenspezifischen Sprache) so ähnlich wie möglich zu sein. Nur Geschäftsinformationen in dieser Ebene. Wenn Sie ein Leistungsproblem beheben, sollten Sie sich anderswo umsehen, da nur Geschäftsinformationen hier sein sollten. Wenn hier plötzlich Anwendungsprobleme auftreten, ist es sehr schwierig, eine Anwendung zu erweitern und zu warten. Dies ist das Herzstück von DDD: die Erstellung wartbarer Software.

Antwort auf Kommentar 1 : Richtig, gute Frage. Daher erfolgt nicht jede Validierung in der Domänenschicht. Sharp hat ein Attribut "DomainSignature", das macht, was Sie wollen. Es ist persistenzbewusst, aber als Attribut bleibt die Domänenschicht sauber. Es stellt sicher, dass Sie keine doppelte Entität mit demselben Namen haben, in Ihrem Beispiel.

Aber lassen Sie uns über kompliziertere Validierungsregeln sprechen. Angenommen, Sie sind Amazon.com. Haben Sie jemals etwas mit einer abgelaufenen Kreditkarte bestellt? Ich habe, wo ich die Karte nicht aktualisiert und etwas gekauft habe. Es nimmt die Bestellung an und die Benutzeroberfläche informiert mich, dass alles pfirsichfarben ist. Ungefähr 15 Minuten später erhalte ich eine E-Mail mit dem Hinweis, dass ein Problem mit meiner Bestellung vorliegt und meine Kreditkarte ungültig ist. Was hier passiert, ist, dass es im Idealfall eine Regex-Validierung in der Domänenschicht gibt. Ist das eine korrekte Kreditkartennummer? Wenn ja, behalten Sie die Bestellung bei. Es gibt jedoch eine zusätzliche Validierung auf der Ebene der Anwendungsaufgaben, bei der ein externer Dienst abgefragt wird, um festzustellen, ob die Zahlung mit der Kreditkarte erfolgen kann. Wenn nicht, versenden Sie nichts, setzen Sie die Bestellung aus und warten Sie auf den Kunden.

Haben Sie keine Angst, Validierungsobjekte auf der Serviceebene zu erstellen, die auf Repositorys zugreifen können . Halten Sie es einfach aus der Domänenschicht heraus.

Kertose
quelle
15
Vielen Dank. Aber ich sollte mich bemühen, so viel Geschäftslogik wie möglich in die Entitäten (und die damit verbundenen Fabriken und Spezifikationen usw.) zu bringen, oder? Aber wenn keiner von ihnen Daten über Repositories abrufen darf, wie soll ich dann eine (einigermaßen komplizierte) Geschäftslogik schreiben? Beispiel: Chatroom-Benutzer dürfen ihren Namen nicht in einen Namen ändern, der bereits von einer anderen Person verwendet wurde. Ich möchte, dass diese Regel von der ChatUser-Entität integriert wird, aber es ist nicht sehr einfach, wenn Sie das Repository von dort aus nicht aufrufen können. Also was soll ich tun?
Codeulike
Meine Antwort war größer als das Kommentarfeld erlauben würde, siehe die Bearbeitung.
Kertose
6
Ihr Unternehmen sollte wissen, wie es sich vor Schaden schützen kann. Dazu gehört, dass sichergestellt wird, dass kein ungültiger Zustand erreicht wird. Was Sie mit dem Chatroom-Benutzer beschreiben, ist eine Geschäftslogik, die sich ZUSÄTZLICH zu der Logik befindet, die die Entität benötigt, um sich selbst gültig zu halten. Geschäftslogik wie das, was Sie wirklich wollen, gehört zu einem Chatroom-Dienst, nicht zur ChatUser-Entität.
Alec
9
Danke Alec. Das ist eine klare Art, es auszudrücken. Mir scheint jedoch, dass Evans 'domänenorientierte goldene Regel "Alle Geschäftslogik sollte in die Domänenschicht gehen" im Widerspruch zur Regel "Entitäten sollten nicht auf Repositorys zugreifen" steht. Ich kann damit leben, wenn ich verstehe, warum das so ist, aber ich kann online keine gute Erklärung dafür finden, warum Entitäten nicht auf Repositorys zugreifen sollten. Evans scheint es nicht explizit zu erwähnen. Wo ist es hergekommen? Wenn Sie eine Antwort veröffentlichen können, die auf eine gute Literatur verweist, können Sie sich möglicherweise ein 50-Punkte-Kopfgeld
sichern :)
4
"Das macht auf klein keinen Sinn" Dies ist ein großer Fehler, den Teams machen ... es ist ein kleines Projekt, als solches kann ich dies und das tun ... hör auf so zu denken. Viele der kleinen Projekte, mit denen wir arbeiten, werden aufgrund der geschäftlichen Anforderungen groß. Wenn Sie etwas verdorren, das klein oder groß ist, machen Sie es richtig.
MeTitus
35

Zuerst war ich überzeugt, einigen meiner Entitäten Zugriff auf Repositorys zu gewähren (dh verzögertes Laden ohne ORM). Später kam ich zu dem Schluss, dass ich nicht sollte und dass ich alternative Wege finden könnte:

  1. Wir sollten unsere Absichten in einer Anfrage kennen und wissen, was wir von der Domäne erwarten. Daher können wir Repository-Aufrufe durchführen, bevor wir das Aggregatverhalten erstellen oder aufrufen. Dies hilft auch, das Problem eines inkonsistenten In-Memory-Status und die Notwendigkeit eines verzögerten Ladens zu vermeiden (siehe diesen Artikel ). Der Geruch ist, dass Sie keine In-Memory-Instanz Ihrer Entität mehr erstellen können, ohne sich um den Datenzugriff sorgen zu müssen.
  2. CQS (Command Query Separation) kann dazu beitragen, dass Sie das Repository nicht mehr für Dinge in unseren Entitäten aufrufen müssen.
  3. Wir können eine Spezifikation verwenden , um Domänenlogikanforderungen zu kapseln und zu kommunizieren und diese stattdessen an das Repository zu übergeben (ein Dienst kann diese Dinge für uns orchestrieren). Die Spezifikation kann von der Entität stammen, die für die Aufrechterhaltung dieser Invariante verantwortlich ist. Das Repository interpretiert Teile der Spezifikation in seiner eigenen Abfrageimplementierung und wendet Regeln aus der Spezifikation auf Abfrageergebnisse an. Dies zielt darauf ab, die Domänenlogik in der Domänenschicht zu halten. Es dient auch der allgegenwärtigen Sprache und Kommunikation besser. Stellen Sie sich vor, Sie sagen "überfällige Auftragsspezifikation" oder "Filterbestellung von tbl_order, wobei place_at weniger als 30 Minuten vor sysdate liegt" (siehe diese Antwort ).
  4. Dies erschwert das Denken über das Verhalten von Unternehmen, da das Prinzip der Einzelverantwortung verletzt wird. Wenn Sie Speicher- / Persistenzprobleme lösen müssen, wissen Sie, wohin Sie gehen müssen und wohin Sie nicht gehen müssen.
  5. Es vermeidet die Gefahr, einer Entität bidirektionalen Zugriff auf den globalen Status zu gewähren (über das Repository und die Domänendienste). Sie möchten auch Ihre Transaktionsgrenze nicht überschreiten.

Vernon Vaughn im roten Buch Implementing Domain-Driven Design bezieht sich an zwei mir bekannten Stellen auf dieses Problem (Hinweis: Dieses Buch wird von Evans vollständig unterstützt, wie Sie im Vorwort lesen können). In Kapitel 7 zu Diensten verwendet er einen Domänendienst und eine Spezifikation, um die Notwendigkeit zu umgehen, dass ein Aggregat ein Repository und ein anderes Aggregat verwendet, um festzustellen, ob ein Benutzer authentifiziert ist. Er wird mit den Worten zitiert:

Als Faustregel sollten wir versuchen, die Verwendung von Repositorys (12) innerhalb von Aggregaten nach Möglichkeit zu vermeiden.

Vernon, Vaughn (06.02.2013). Implementieren eines domänengesteuerten Designs (Kindle Location 6089). Pearson Ausbildung. Kindle Edition.

Und in Kapitel 10 über Aggregate sagt er im Abschnitt "Modellnavigation" (kurz nachdem er die Verwendung globaler eindeutiger IDs für die Referenzierung anderer Aggregatwurzeln empfohlen hat):

Die Referenz nach Identität verhindert die Navigation durch das Modell nicht vollständig. Einige verwenden ein Repository (12) aus einem Aggregat zur Suche. Diese Technik wird als Disconnected Domain Model bezeichnet und ist eigentlich eine Form des verzögerten Ladens. Es gibt jedoch einen anderen empfohlenen Ansatz: Verwenden Sie ein Repository oder einen Domänendienst (7), um abhängige Objekte zu suchen, bevor Sie das Aggregatverhalten aufrufen. Ein Client-Anwendungsdienst kann dies steuern und dann an das Aggregat senden:

Er zeigt ein Beispiel dafür im 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));
   } 
   ...
}     

Er erwähnt auch eine weitere Lösung, wie ein Domänendienst in einer Aggregate-Befehlsmethode zusammen mit dem Doppelversand verwendet werden kann . (Ich kann nicht genug empfehlen, wie nützlich es ist, sein Buch zu lesen. Nachdem Sie es satt haben, endlos im Internet zu stöbern, stöbern Sie über das wohlverdiente Geld und lesen Sie das Buch.)

Ich hatte dann einige Diskussionen mit dem immer liebenswürdigen Marco Pivetta @Ocramius, der mir ein bisschen Code zeigte, wie man eine Spezifikation aus der Domain herausholt und diese verwendet:

1) Dies wird nicht empfohlen:

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

2) In einem Domain-Service ist dies gut:

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); 
}
Programmierer
quelle
1
Frage: Es wird uns immer beigebracht, kein Objekt in einem ungültigen oder inkonsistenten Zustand zu erstellen. Wenn Sie Benutzer aus dem Repository laden und dann aufrufen, getFriends()bevor Sie etwas anderes tun, wird es leer oder verzögert geladen. Wenn leer, dann liegt dieses Objekt in einem ungültigen Zustand. Irgendwelche Gedanken dazu?
Jimbo
Das Repository ruft die Domäne auf, um eine Instanz neu zu erstellen. Sie erhalten keine Benutzerinstanz, ohne die Domäne zu durchlaufen. Das Problem, mit dem sich diese Antwort befasst, ist umgekehrt. Wenn die Domäne auf das Repository verweist, sollte dies vermieden werden.
Programmierer
27

Das ist eine sehr gute Frage. Ich freue mich auf eine Diskussion darüber. Aber ich denke, es wird in mehreren DDD-Büchern und Jimmy Nilssons und Eric Evans erwähnt. Ich denke, es ist auch anhand von Beispielen sichtbar, wie das Reposistory-Muster verwendet wird.

ABER lassen Sie uns diskutieren. Ich denke, ein sehr berechtigter Gedanke ist, warum eine Entität wissen sollte, wie sie eine andere Entität bestehen kann. Wichtig bei DDD ist, dass jede Entität die Verantwortung hat, ihre eigene "Wissenssphäre" zu verwalten, und nichts darüber wissen sollte, wie man andere Entitäten liest oder schreibt. Sicher, Sie können Entität A wahrscheinlich nur eine Repository-Schnittstelle zum Lesen von Entitäten B hinzufügen. Das Risiko besteht jedoch darin, dass Sie Kenntnisse darüber offenlegen, wie B beibehalten werden soll. Wird Entität A auch eine Validierung für B durchführen, bevor B in db beibehalten wird?

Wie Sie sehen, kann Entität A stärker in den Lebenszyklus von Entität B einbezogen werden, was das Modell komplexer machen kann.

Ich denke (ohne Beispiel), dass Unit-Tests komplexer sein werden.

Ich bin mir jedoch sicher, dass es immer Szenarien geben wird, in denen Sie versucht sind, Repositorys über Entitäten zu verwenden. Sie müssen sich jedes Szenario ansehen, um ein gültiges Urteil zu fällen. Vor-und Nachteile. Aber die Repository-Entity-Lösung beginnt meiner Meinung nach mit vielen Nachteilen. Es muss ein ganz besonderes Szenario mit Vorteilen sein, die die Nachteile ausgleichen ....

Magnus Backeus
quelle
1
Guter Punkt. Das Domain-Modell der alten Schule würde wahrscheinlich Entity B dafür verantwortlich machen, sich selbst zu validieren, bevor es sich selbst bestehen lässt, denke ich. Sind Sie sicher, dass Evans Entitäten erwähnt, die keine Repositorys verwenden? Ich bin in der Mitte des Buches und es hat es noch nicht erwähnt ...
Codeulike
Nun, ich habe das Buch vor einigen Jahren gelesen (gut 3 ...) und mein Gedächtnis versagt mir. Ich kann mich nicht erinnern, ob er es genau formuliert hat, aber ich glaube, er hat dies anhand von Beispielen veranschaulicht. Eine Community-Interpretation seines Frachtbeispiels (aus seinem Buch) finden Sie auch unter dddsamplenet.codeplex.com . Laden Sie das Code-Projekt herunter (siehe Vanilla-Projekt - es ist das Beispiel aus dem Buch). Sie werden feststellen, dass Repositorys nur in der Anwendungsschicht für den Zugriff auf Domänenentitäten verwendet werden.
Magnus Backeus
1
Wenn Sie das DDD SmartCA-Beispiel aus dem Buch p2p.wrox.com/… herunterladen , sehen Sie einen anderen Ansatz (obwohl dies ein RIA-Windows-Client ist), bei dem Repositorys in Diensten verwendet werden (hier nichts Seltsames), Dienste jedoch in Entites verwendet werden. Dies ist etwas, was ich nicht tun würde, ABER ich bin ein Webb-App-Typ. In Anbetracht des Szenarios für die SmartCA-App, in dem Sie offline arbeiten müssen, sieht das ddd-Design möglicherweise anders aus.
Magnus Backeus
Das SmartCA-Beispiel klingt interessant. In welchem ​​Kapitel befindet es sich? (Die Code-Downloads sind nach Kapiteln geordnet)
Codeulike
1
@codeulike Ich entwerfe und implementiere derzeit ein Framework mit ddd-Konzepten. Manchmal muss bei der Validierung auf die Datenbank zugegriffen und diese abgefragt werden (Beispiel: Abfrage nach einer mehrspaltigen eindeutigen Indexprüfung). Diesbezüglich und aufgrund der Tatsache, dass Abfragen in die Repository-Schicht geschrieben werden sollten, stellt sich heraus, dass Domänenentitäten Verweise auf haben müssen ihre Repository-Schnittstellen in der Domänenmodellschicht, um die Validierung vollständig in der Domänenmodellschicht zu platzieren. Ist es also endlich in Ordnung, dass Domänenentitäten Zugriff auf Repositorys haben?
Karamafrooz
13

Warum den Datenzugriff trennen?

Aus dem Buch denke ich, dass die ersten beiden Seiten des Kapitels Model Driven Design eine Rechtfertigung dafür liefern, warum Sie technische Implementierungsdetails aus der Implementierung des Domänenmodells abstrahieren möchten.

  • Sie möchten eine enge Verbindung zwischen dem Domänenmodell und dem Code aufrechterhalten
  • Die Trennung technischer Belange hilft zu beweisen, dass das Modell für die Implementierung praktisch ist
  • Sie möchten, dass die allgegenwärtige Sprache das Design des Systems durchdringt

Dies scheint alles zu dem Zweck zu sein, ein separates "Analysemodell" zu vermeiden, das von der tatsächlichen Implementierung des Systems getrennt wird.

Nach meinem Verständnis des Buches kann dieses "Analysemodell" ohne Berücksichtigung der Softwareimplementierung entworfen werden. Sobald Entwickler versuchen, das von der Unternehmensseite verstandene Modell zu implementieren, bilden sie aufgrund der Notwendigkeit ihre eigenen Abstraktionen, was zu einer Mauer in der Kommunikation und im Verständnis führt.

In der anderen Richtung können Entwickler, die zu viele technische Probleme in das Domänenmodell einbringen, ebenfalls diese Kluft verursachen.

Sie könnten also in Betracht ziehen, dass das Üben der Trennung von Bedenken wie z. B. Persistenz dazu beitragen kann, sich vor diesen unterschiedlichen Entwurfs- und Analysemodellen zu schützen. Wenn es notwendig erscheint, Dinge wie Persistenz in das Modell einzuführen, ist dies eine rote Fahne. Möglicherweise ist das Modell für die Implementierung nicht praktisch.

Zitat:

"Das einzelne Modell verringert die Fehlerwahrscheinlichkeit, da das Design jetzt ein direktes Ergebnis des sorgfältig überlegten Modells ist. Das Design und sogar der Code selbst haben die Kommunikationsfähigkeit eines Modells."

So wie ich das interpretiere, verlieren Sie diese Kommunikationsfähigkeit, wenn Sie mehr Codezeilen haben, die sich mit Dingen wie dem Datenbankzugriff befassen.

Wenn Sie beispielsweise auf die Überprüfung der Eindeutigkeit zugreifen müssen, schauen Sie sich Folgendes an:

Udi Dahan: Die größten Fehler, die Teams bei der Anwendung von DDD machen

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

unter "Alle Regeln sind nicht gleich"

und

Verwenden des Domänenmodellmusters

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

unter "Szenarien für die Nichtverwendung des Domänenmodells", in denen dasselbe Thema behandelt wird.

So trennen Sie den Datenzugriff

Laden von Daten über eine Schnittstelle

Die "Datenzugriffsschicht" wurde über eine Schnittstelle abstrahiert, die Sie aufrufen, um die erforderlichen Daten abzurufen:

var orderLines = OrderRepository.GetOrderLines(orderId);

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

Vorteile: Die Schnittstelle trennt den Installationscode "Datenzugriff", sodass Sie weiterhin Tests schreiben können. Der Datenzugriff kann von Fall zu Fall abgewickelt werden, was eine bessere Leistung als eine generische Strategie ermöglicht.

Nachteile: Der aufrufende Code muss davon ausgehen, was geladen wurde und was nicht.

Angenommen, GetOrderLines gibt aus Leistungsgründen OrderLine-Objekte mit einer ProductInfo-Null-Eigenschaft zurück. Der Entwickler muss den Code hinter der Schnittstelle genau kennen.

Ich habe diese Methode auf realen Systemen ausprobiert. Am Ende ändern Sie ständig den Umfang der geladenen Daten, um Leistungsprobleme zu beheben. Am Ende spähen Sie hinter die Benutzeroberfläche, um den Datenzugriffscode zu überprüfen und festzustellen, was geladen wird und was nicht.

Die Trennung von Bedenken sollte es dem Entwickler nun ermöglichen, sich so weit wie möglich auf einen Aspekt des Codes zu konzentrieren. Die Schnittstellentechnik entfernt das WIE werden diese Daten geladen, aber nicht WIE VIEL Daten werden geladen, WENN sie geladen werden und WO sie geladen werden.

Fazit: Ziemlich geringe Trennung!

Faules Laden

Daten werden bei Bedarf geladen. Aufrufe zum Laden von Daten sind im Objektdiagramm selbst ausgeblendet. Wenn Sie auf eine Eigenschaft zugreifen, kann eine SQL-Abfrage ausgeführt werden, bevor das Ergebnis zurückgegeben wird.

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

Vorteile: Das "WANN, WO und WIE" des Datenzugriffs ist dem Entwickler verborgen, der sich auf die Domänenlogik konzentriert. Das Aggregat enthält keinen Code, der sich mit dem Laden von Daten befasst. Die geladene Datenmenge kann genau der Menge entsprechen, die der Code benötigt.

Nachteile: Wenn Sie von einem Leistungsproblem betroffen sind, ist es schwierig, es zu beheben, wenn Sie eine generische "Einheitslösung" haben. Das verzögerte Laden kann insgesamt zu einer schlechteren Leistung führen, und das Implementieren des verzögerten Ladens kann schwierig sein.

Rollenschnittstelle / Eifriges Abrufen

Jeder Anwendungsfall wird über eine von der Aggregatklasse implementierte Rollenschnittstelle explizit angegeben , sodass Datenladestrategien pro Anwendungsfall verarbeitet werden können.

Die Abrufstrategie kann folgendermaßen aussehen:

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

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

}
   

Dann kann Ihr Aggregat wie folgt aussehen:

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

        etc...
    }
}

Die BillOrderFetchingStrategy wird verwendet, um das Aggregat zu erstellen, und dann erledigt das Aggregat seine Arbeit.

Vorteile: Ermöglicht benutzerdefinierten Code pro Anwendungsfall und ermöglicht so eine optimale Leistung. Entspricht dem Prinzip der Schnittstellentrennung . Keine komplexen Code-Anforderungen. Aggregat-Unit-Tests müssen die Ladestrategie nicht nachahmen. In den meisten Fällen kann eine generische Ladestrategie verwendet werden (z. B. eine "Alle laden" -Strategie), und bei Bedarf können spezielle Ladestrategien implementiert werden.

Nachteile: Der Entwickler muss die Abrufstrategie nach dem Ändern des Domänencodes noch anpassen / überprüfen.

Beim Ansatz der Abrufstrategie ändern Sie möglicherweise immer noch den benutzerdefinierten Abrufcode, um die Geschäftsregeln zu ändern. Es ist keine perfekte Trennung von Bedenken, wird aber am Ende wartbarer und ist besser als die erste Option. Die Abrufstrategie kapselt die Daten WIE, WANN und WO. Es hat eine bessere Trennung von Bedenken, ohne an Flexibilität zu verlieren, wie die Einheitsgröße für alle Lazy-Loading-Ansätze.

ttg
quelle
Danke, ich werde die Links überprüfen. Aber verwechseln Sie in Ihrer Antwort "Trennung von Bedenken" mit "überhaupt keinen Zugang dazu"? Sicherlich würden die meisten Menschen zustimmen, dass die Persistenzschicht von der Schicht, in der sich die Entitäten befinden, getrennt gehalten werden sollte. Dies unterscheidet sich jedoch von der Aussage: „Die Entitäten sollten nicht einmal in der Lage sein, die Persistenzschicht selbst durch eine sehr allgemeine Implementierungsunabhängigkeit zu sehen Schnittstelle'.
Codeulike
Laden von Daten über eine Schnittstelle oder nicht, Sie müssen weiterhin Daten laden, während Sie Geschäftsregeln implementieren. Ich stimme zu, dass viele Leute diese Trennung von Bedenken immer noch nennen, vielleicht wäre das Prinzip der Einzelverantwortung ein besserer Begriff gewesen.
ttg
1
Sie sind sich nicht ganz sicher, wie Sie Ihren letzten Kommentar analysieren sollen, aber ich denke, Sie schlagen vor, dass Daten während der Verarbeitung von Geschäftsregeln nicht geladen werden sollten? Ich sehe, das würde die Regeln "reiner" machen. Viele Arten von Geschäftsregeln müssen sich jedoch auf andere Daten beziehen. Schlagen Sie vor, dass diese im Voraus von einem separaten Objekt geladen werden sollten?
Codeulike
@codeulike: Ich habe meine Antwort aktualisiert. Sie können weiterhin Daten während der Geschäftsregeln laden, wenn Sie dies unbedingt benötigen. Dies erfordert jedoch nicht das Hinzufügen von Datenzugriffscodezeilen zu Ihrem Domänenmodell (z. B. verzögertes Laden). In den von mir entworfenen Domänenmodellen werden Daten im Allgemeinen nur im Voraus geladen, wie Sie gesagt haben. Ich habe festgestellt, dass das Ausführen von Geschäftsregeln normalerweise keine übermäßige Datenmenge erfordert.
ttg
12

Was für eine ausgezeichnete Frage. Ich bin auf dem gleichen Weg der Entdeckung, und die meisten Antworten im Internet scheinen so viele Probleme zu bringen, wie sie Lösungen bringen.

Also (auf die Gefahr hin, etwas zu schreiben, mit dem ich in einem Jahr nicht einverstanden bin) sind hier meine bisherigen Entdeckungen.

Zuallererst mögen wir ein reichhaltiges Domänenmodell , das uns eine hohe Auffindbarkeit (was wir mit einem Aggregat tun können) und Lesbarkeit (ausdrucksstarke Methodenaufrufe) bietet .

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

Wir möchten dies erreichen, ohne Services in den Konstruktor einer Entität einzufügen, weil:

  • Die Einführung eines neuen Verhaltens (das einen neuen Dienst verwendet) kann zu einer Konstruktoränderung führen, was bedeutet, dass die Änderung jede Zeile betrifft, die die Entität instanziiert !
  • Diese Dienste sind nicht Teil des Modells , aber die Konstruktorinjektion würde darauf hinweisen, dass dies der Fall ist.
  • Oft ist ein Dienst (sogar seine Schnittstelle) eher ein Implementierungsdetail als ein Teil der Domäne. Das Domänenmodell hätte eine nach außen gerichtete Abhängigkeit .
  • Es kann verwirrend sein, warum die Entität ohne diese Abhängigkeiten nicht existieren kann. (Ein Gutschriftservice, sagen Sie? Ich werde nicht einmal etwas mit Gutschriften anfangen ...)
  • Es würde es schwierig machen, zu instanziieren und somit schwer zu testen .
  • Das Problem breitet sich leicht aus, da andere Entitäten, die diese enthalten, dieselben Abhängigkeiten erhalten würden - was auf sie wie sehr unnatürliche Abhängigkeiten aussehen könnte .

Wie können wir das dann tun? Meine bisherige Schlussfolgerung ist, dass Methodenabhängigkeiten und doppelter Versand eine anständige Lösung darstellen.

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()erfordert jetzt einen Dienst, der für die Erstellung von Gutschriften verantwortlich ist. Es verwendet den doppelten Versand , wodurch die Arbeit vollständig an den zuständigen Dienst ausgelagert wird , während die Erkennbarkeit für die InvoiceEntität erhalten bleibt.

SetStatus()hat jetzt eine einfache Abhängigkeit von einem Logger, der offensichtlich einen Teil der Arbeit ausführen wird .

Für letztere können wir uns stattdessen über ein anmelden, um den Client-Code zu vereinfachen IInvoiceService. Schließlich scheint die Rechnungsprotokollierung einer Rechnung ziemlich eigen zu sein. Solch eine einzige IInvoiceServicehilft, die Notwendigkeit aller Arten von Minidiensten für verschiedene Operationen zu vermeiden. Der Nachteil ist, dass unklar wird, was genau dieser Dienst tun wird . Es könnte sogar so aussehen, als würde es doppelt versandt, während der größte Teil der Arbeit wirklich noch in sich SetStatus()selbst erledigt wird .

Wir könnten den Parameter 'logger' noch nennen, in der Hoffnung, unsere Absicht preiszugeben. Scheint allerdings etwas schwach.

Stattdessen würde ich nach einem fragen IInvoiceLogger(wie wir es bereits im Codebeispiel tun) und IInvoiceServicediese Schnittstelle implementieren lassen. Der Client-Code kann einfach seine Single IInvoiceServicefür alle InvoiceMethoden verwenden, die nach einem solchen ganz bestimmten, rechnungsbezogenen "Mini-Service" fragen, während die Methodensignaturen immer noch deutlich machen, wonach sie fragen.

Ich stelle fest, dass ich Repositories nicht ausdrücklich angesprochen habe. Nun, der Logger ist oder verwendet ein Repository, aber lassen Sie mich auch ein expliziteres Beispiel geben. Wir können den gleichen Ansatz verwenden, wenn das Repository nur in ein oder zwei Methoden benötigt wird.

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

In der Tat bietet dies eine Alternative zu den immer störenden faulen Lasten .

Update: Ich habe den folgenden Text aus historischen Gründen verlassen, empfehle jedoch, faule Lasten zu 100% zu vermeiden.

Für einen echten, eigenschaftsbasierte faul Lasten, ich tun verwenden derzeit Konstruktor Injektion, aber in einer Persistenz-unwissend Art und Weise.

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));
    }
}

Ein Repository, das ein Repository Invoiceaus der Datenbank lädt, kann einerseits freien Zugriff auf eine Funktion haben, die die entsprechenden Gutschriften lädt, und diese Funktion in das Verzeichnis einfügen Invoice.

Auf der anderen Seite übergibt Code, der eine tatsächliche neue erstellt Invoice , lediglich eine Funktion, die eine leere Liste zurückgibt:

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

(Ein Brauch ILazy<out T>könnte uns von der hässlichen Besetzung befreien IEnumerable, aber das würde die Diskussion erschweren.)

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

Ich würde mich freuen, Ihre Meinungen, Vorlieben und Verbesserungen zu hören!

Timo
quelle
3

Für mich scheint dies eine allgemein gute OOD-bezogene Praxis zu sein, anstatt spezifisch für DDD zu sein.

Gründe, an die ich denken kann, sind:

  • Trennung von Bedenken (Entitäten sollten von der Art und Weise, wie sie fortbestehen, getrennt werden, da es mehrere Strategien geben kann, bei denen dieselbe Entität je nach Nutzungsszenario bestehen bleibt.)
  • Entitäten können logischerweise auf einer Ebene unterhalb der Ebene angezeigt werden, auf der Repositorys betrieben werden. Komponenten auf niedrigerer Ebene sollten keine Kenntnisse über Komponenten auf höherer Ebene haben. Daher sollten Einträge keine Kenntnisse über Repositories haben.
user1502505
quelle
2

einfach gibt Vernon Vaughn eine Lösung:

Verwenden Sie ein Repository oder einen Domänendienst, um abhängige Objekte zu suchen, bevor Sie das Aggregatverhalten aufrufen. Ein Clientanwendungsdienst kann dies steuern.

Alireza Rahmani Khalili
quelle
Aber nicht von einer Entität.
Schmied
Aus Vernon Vaughn IDDD-Quelle: public class Calendar erweitert EventSourcedRootEntity {... public CalendarEntry ScheduleCalendarEntry (CalendarIdentityService aCalendarIdentityService,
Teimuraz
Überprüfen Sie seine Zeitung @Teimuraz
Alireza Rahmani Khalili
1

Ich habe gelernt, objektorientierte Programmierung zu codieren, bevor all dieses separate Layer-Buzz auftritt und meine ersten Objekte / Klassen direkt der Datenbank zugeordnet wurden.

Schließlich habe ich eine Zwischenschicht hinzugefügt, da ich auf einen anderen Datenbankserver migrieren musste. Ich habe dasselbe Szenario mehrmals gesehen / gehört.

Ich denke, die Trennung des Datenzugriffs (auch bekannt als "Repository") von Ihrer Geschäftslogik ist eines der Dinge, die mehrmals neu erfunden wurden, obwohl das Domain Driven Design-Buch viel "Lärm" verursacht.

Ich verwende derzeit 3 ​​Ebenen (GUI, Logik, Datenzugriff), wie es viele Entwickler tun, weil es eine gute Technik ist.

Das Aufteilen der Daten in eine RepositoryEbene (auch bekannt als Data AccessEbene) kann als eine gute Programmiertechnik angesehen werden, die nicht nur eine Regel ist.

Wie bei vielen Methoden möchten Sie möglicherweise Ihr Programm starten, NICHT implementieren und schließlich aktualisieren, sobald Sie sie verstanden haben.

Zitat: Die Ilias wurde nicht vollständig von Homer erfunden, Carmina Burana wurde nicht vollständig von Carl Orff erfunden, und in beiden Fällen bekam die Person, die andere zusammen brachte, die Anerkennung ;-)

umlcat
quelle
1
Vielen Dank, aber ich frage nicht nach der Trennung des Datenzugriffs von der Geschäftslogik - das ist eine sehr klare Sache, über die sehr weitgehende Übereinstimmung besteht. Ich frage, warum in DDD-Architekturen wie S # arp die Entitäten nicht einmal mit der Datenzugriffsschicht "sprechen" dürfen. Es ist ein interessantes Arrangement, über das ich nicht viel diskutieren konnte.
Codeulike
0

Kam dies aus dem Buch "Eric Evans Domain Driven Design" oder von woanders?

Es ist altes Zeug. Erics Buch hat es nur ein bisschen mehr summend gemacht.

Wo gibt es einige gute Erklärungen für die Gründe dafür?

Die Vernunft ist einfach - der menschliche Geist wird schwach, wenn er vage verwandten Mehrfachkontexten gegenübersteht. Sie führen zu Mehrdeutigkeiten (Amerika in Süd- / Nordamerika bedeutet Süd- / Nordamerika), Mehrdeutigkeiten führen zu einer ständigen Zuordnung von Informationen, wenn der Verstand sie "berührt", und dies summiert sich zu schlechter Produktivität und Fehlern.

Geschäftslogik sollte so klar wie möglich reflektiert werden. Fremdschlüssel, Normalisierung und objektrelationale Zuordnung stammen aus einer völlig anderen Domäne - diese Dinge sind technisch und computerbezogen.

In Analogie: Wenn Sie lernen, wie man handschriftlich schreibt, sollten Sie nicht mit dem Verständnis belastet sein, wo Stift hergestellt wurde, warum Tinte auf Papier hält, wann Papier erfunden wurde und was andere berühmte chinesische Erfindungen sind.

Bearbeiten: Zur Verdeutlichung: Ich spreche nicht über die klassische OO-Praxis, den Datenzugriff in eine separate Ebene von der Geschäftslogik zu trennen - ich spreche über die spezifische Anordnung, wonach Entitäten in DDD nicht mit den Daten sprechen sollen Zugriffsebene überhaupt (dh sie dürfen keine Verweise auf Repository-Objekte enthalten)

Der Grund ist immer noch der gleiche, den ich oben erwähnt habe. Hier ist es nur einen Schritt weiter. Warum sollten Entitäten teilweise unwissend sein, wenn sie (zumindest nahezu) vollständig sein können? Unser Modell hat weniger domänenunabhängige Bedenken - mehr Raum zum Atmen, wenn unser Geist es neu interpretieren muss.

Arnis Lapsa
quelle
Richtig. Wie implementiert eine völlig persistenzunabhängige Entität Business Logic, wenn sie nicht einmal mit der Persistenzschicht sprechen darf? Was macht es, wenn Werte in beliebigen anderen Entitäten betrachtet werden müssen?
Codeulike
Wenn Ihre Entität Werte in beliebigen anderen Entitäten betrachten muss, haben Sie wahrscheinlich einige Entwurfsprobleme. Vielleicht sollten Sie die Klassen aufteilen, damit sie zusammenhängender sind.
CDAq
0

Um Carolina Lilientahl zu zitieren: "Muster sollten Zyklen verhindern" https://www.youtube.com/watch?v=eJjadzMRQAk , wo sie sich auf zyklische Abhängigkeiten zwischen Klassen bezieht. Bei Repositorys innerhalb von Aggregaten besteht die Versuchung, zyklische Abhängigkeiten aus der Bequemlichkeit der Objektnavigation als einzigem Grund zu erstellen. Das oben von Prograhammer erwähnte Muster, das von Vernon Vaughn empfohlen wurde, wo andere Aggregate durch IDs anstelle von Root-Instanzen referenziert werden (gibt es einen Namen für dieses Muster?), Schlägt eine Alternative vor, die zu anderen Lösungen führen könnte.

Beispiel für eine zyklische Abhängigkeit zwischen Klassen (Geständnis):

(Time0): Zwei Klassen, Sample und Well, beziehen sich aufeinander (zyklische Abhängigkeit). Well bezieht sich auf Probe, und Probe bezieht sich aus Bequemlichkeitsgründen auf Well (manchmal Schleifen von Proben, manchmal Schleifen aller Vertiefungen in einer Platte). Ich konnte mir keine Fälle vorstellen, in denen Sample nicht auf den Brunnen zurückgreifen würde, in dem er platziert ist.

(Zeit 1): Ein Jahr später werden viele Anwendungsfälle implementiert ... und es gibt jetzt Fälle, in denen die Probe nicht auf den Brunnen zurückgreifen sollte, in dem sie platziert ist. Innerhalb eines Arbeitsschritts befinden sich temporäre Platten. Hier bezieht sich eine Vertiefung auf eine Probe, die sich wiederum auf eine Vertiefung auf einer anderen Platte bezieht. Aus diesem Grund tritt manchmal ein seltsames Verhalten auf, wenn jemand versucht, neue Funktionen zu implementieren. Es braucht Zeit, um einzudringen.

Mir hat auch dieser oben erwähnte Artikel über negative Aspekte des verzögerten Ladens geholfen .

Edvard Englund
quelle
-1

In der idealen Welt schlägt DDD vor, dass Entitäten keinen Bezug zu Datenschichten haben sollten. aber wir leben nicht in einer idealen Welt. Domänen müssen möglicherweise für die Geschäftslogik auf andere Domänenobjekte verweisen, von denen sie möglicherweise keine Abhängigkeit haben. Es ist logisch, dass Entitäten schreibgeschützt auf die Repository-Ebene verweisen, um die Werte abzurufen.

vsingh
quelle
Nein, dies führt zu einer unnötigen Kopplung an die Entitäten, verletzt die SRP und die Trennung von Bedenken und macht es schwierig, die Entität von der Persistenz zu deserialisieren (da der Deserialisierungsprozess jetzt auch Dienste / Repositorys einspeisen muss, die die Entität besucht).
Schmied