it-swarm.com.de

DDD - Die Regel, nach der Entitäten nicht direkt auf Repositorys zugreifen können

In Domain Driven Design scheint es viele von Vereinbarung zu geben, dass Entitäten nicht direkt auf Repositorys zugreifen sollten.

Kam dies von Eric Evans Domain Driven Design Buch, oder kam es von woanders?

Wo gibt es einige gute Erklärungen für die Argumentation dahinter?

bearbeiten: Zur Verdeutlichung: Ich spreche nicht von der klassischen OO Praxis, den Datenzugriff von der Geschäftslogik in eine separate Ebene zu trennen soll überhaupt mit der Datenzugriffsebene sprechen (dh sie sollen 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. Wenn es ein so wichtiges Prinzip ist, sollte es doch irgendwo online gute Artikel darüber geben?

update: März 2013, die Upvotes zu der Frage lassen darauf schließen, dass großes Interesse besteht. Auch wenn es viele Antworten gibt, denke ich, dass es noch Platz für mehr gibt, wenn die Leute Ideen dazu haben.

160
codeulike

Hier herrscht eine gewisse 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 zu einem großen Team gehören, möchten Sie sagen: "Sie greifen über das Produkt-Repository auf ein Produkt zu. 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 durchgehen. "

Auf diese Weise haben Sie eine sehr, sehr klare Trennung in der Geschäftslogik und wo die Dinge aktualisiert werden. Sie haben kein Kind, das alleine unterwegs ist und dieses gesamte Programm schreibt, das all diese komplizierten Dinge mit dem Produktkatalog zu tun hat, 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. Es bedeutet auch, dass Mitarbeiter, die dem Team beitreten, neue Funktionen hinzufügen, wissen, wohin sie gehen müssen 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 würden das Repository von Eric Evans und das Repository-Muster unterschiedliche Namen haben, da sie sich häufig überlappen. Um das Repository-Muster zu erhalten, haben Sie einen Kontrast zu anderen Arten des Datenzugriffs, beispielsweise mit einem Servicebus oder einem Ereignismodellsystem. In der Regel bleibt die Definition des Eric Evans-Repository auf der Strecke und Sie fangen an, über einen begrenzten Kontext zu sprechen. Jeder begrenzte Kontext ist im Wesentlichen seine eigene Anwendung. Möglicherweise verfügen Sie über ein ausgeklügeltes Genehmigungssystem, um Dinge in den Produktkatalog aufzunehmen. In Ihrem ursprünglichen Design stand das Produkt im Mittelpunkt, in diesem begrenzten Kontext ist es jedoch der Produktkatalog. Sie können weiterhin über einen Servicebus auf Produktinformationen zugreifen und Produkte aktualisieren, müssen sich jedoch darüber im Klaren sein, dass ein Produktkatalog außerhalb des begrenzten Kontexts möglicherweise etwas völlig anderes bedeutet.

Zurück zu deiner ursprünglichen Frage. Wenn Sie von einer Entität aus auf ein Repository zugreifen, bedeutet dies, dass es sich bei der Entität nicht um eine Geschäftsentität handelt, sondern wahrscheinlich um eine Entität, die 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. Haben Sie nur Geschäftsinformationen in dieser Schicht. Wenn Sie ein Leistungsproblem beheben, sollten Sie sich anderswo umsehen, da hier nur Geschäftsinformationen angezeigt werden sollten. Wenn plötzlich Anwendungsprobleme auftreten, ist es sehr schwierig, eine Anwendung zu erweitern und zu warten. Dies ist das eigentliche Herzstück von DDD: die Erstellung wartungsfähiger Software.

Antwort auf Kommentar 1: Richtig, gute Frage. Daher findet nicht alle Validierung in der Domänenschicht statt. Sharp hat ein Attribut "DomainSignature", das genau das tut, was Sie wollen. Es ist persistenzbewusst, aber ein Attribut zu sein, hält die Domänenschicht sauber. Es stellt sicher, dass Sie keine doppelte Entität mit demselben Namen in Ihrem Beispiel haben.

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. Etwa 15 Minuten später erhalte ich eine E-Mail mit dem Hinweis, dass bei meiner Bestellung ein Problem vorliegt. Meine Kreditkarte ist ungültig. Was hier passiert, ist, dass es im Idealfall eine Regex-Validierung in der Domänenschicht gibt. Ist das eine korrekte Kreditkartennummer? Wenn ja, die Bestellung fortsetzen. Es gibt jedoch eine zusätzliche Validierung auf der Ebene der Anwendungstasks, bei der ein externer Dienst abgefragt wird, um festzustellen, ob die Zahlung mit der Kreditkarte möglich ist. Wenn nicht, versenden Sie nichts, setzen Sie die Bestellung aus und warten Sie auf den Kunden. Dies sollte alles in einer Serviceschicht erfolgen.

Haben Sie keine Angst davor, Validierungsobjekte auf der Serviceebene zu erstellen, die auf Repositorys zugreifen können . Halten Sie es einfach aus der Domain-Schicht heraus.

44
kertosis

Zuerst war ich der Überzeugung, einigen meiner Entitäten Zugriff auf Repositories 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 Domain wollen, daher können wir Repository-Aufrufe tätigen, bevor wir Aggregate-Verhalten erstellen oder aufrufen. Dies hilft auch dabei, das Problem des inkonsistenten Speicherzustands und das Erfordernis des verzögerten Ladens zu vermeiden (siehe dieses Artikel ). Der Geruch ist, dass Sie keine Instanz Ihrer Entität mehr im Arbeitsspeicher erstellen können, ohne sich Gedanken über den Datenzugriff zu machen.
  2. CQS (Command Query Separation) kann dazu beitragen, die Notwendigkeit zu verringern, das Repository für Dinge in unseren Entitäten aufrufen zu müssen.
  3. Wir können ein Spezifikation verwenden, um die Anforderungen der Domänenlogik zu kapseln und zu kommunizieren und diese stattdessen an das Repository weiterzuleiten (ein Service 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 seine eigene 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 Bestellspezifikation" und "Filterreihenfolge von tbl_order, wobei placement_at weniger als 30 Minuten vor sysdate liegt" (siehe dies Antwort ).
  4. Dies erschwert das Nachdenken ü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 wird die Gefahr vermieden, einer Entität bidirektionalen Zugriff auf den globalen Status zu gewähren (über das Repository und die Domänendienste). Sie möchten auch nicht Ihre Transaktionsgrenze überschreiten.

Vernon Vaughn in dem roten Buch Implementing Domain-Driven Design bezieht sich an zwei mir bekannten Stellen auf dieses Problem (Anmerkung: Dieses Buch wird von Evans voll befürwortet 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 verwendet und ein anderes Aggregat, 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 zu vermeiden, wenn dies möglich ist.

Vernon, Vaughn (06.02.2013). Implementieren von domänengesteuertem Design (Kindle Location 6089). Pearson Ausbildung. Kindle Edition.

Und in Kapitel 10 über Aggregate im Abschnitt mit dem Titel "Modellnavigation" sagt er (kurz nachdem er die Verwendung globaler eindeutiger IDs zum Verweisen auf andere Aggregatwurzeln empfohlen hat):

Identitätsbezug verhindert die Navigation durch das Modell nicht vollständig. Einige verwenden ein Repository (12) aus einem Aggregat zum Nachschlagen. Diese Technik wird als "Disconnected Domain Model" bezeichnet und ist eigentlich eine Form des verzögerten Ladens. Es wird jedoch ein anderer Ansatz empfohlen: 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 noch eine andere Lösung, wie ein Domänendienst in einer Aggregatbefehlsmethode zusammen mit double-dispatch 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 in dem wohlverdienten Geld und lesen Sie das Buch.)

Ich hatte dann einige Diskussion mit dem immer gnädigen Marco Pivetta @ Ocramius , der mir ein bisschen Code zeigte, wie ich eine Spezifikation aus der Domain herausholte und diese verwendete:

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

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 Nilsons und Eric Evans erwähnt. Ich denke, es ist auch an Beispielen zu erkennen, wie man das Reposistory-Muster verwendet.

ABER lasst 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 zu wissen, wie sie andere Entitäten lesen oder schreiben soll. Sicher, Sie können Entität A wahrscheinlich nur um eine Repository-Schnittstelle zum Lesen von Entitäten B erweitern. Das Risiko besteht jedoch darin, dass Sie wissen, wie Sie B beibehalten können. Führt Entität A auch eine Validierung für B durch, bevor Sie B in db beibehalten?

Wie Sie sehen, kann Entität A stärker in den Lebenszyklus von Entität B einbezogen werden, was dem Modell mehr Komplexität verleihen kann.

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

Aber ich bin mir 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 fängt meiner Meinung nach mit vielen Nachteilen an. Es muss ein ganz besonderes Szenario mit Profis sein, die die Nachteile ausgleichen.

27
Magnus Backeus

Warum Datenzugriff trennen?

Die ersten beiden Seiten des Kapitels "Model Driven Design" geben meiner Ansicht nach eine Rechtfertigung dafür, warum Sie technische Implementierungsdetails von der Implementierung des Domänenmodells abstrahieren möchten.

  • Sie möchten eine enge Verbindung zwischen dem Domain-Modell und dem Code herstellen
  • Die Trennung der technischen Belange hilft zu beweisen, dass das Modell für die Implementierung praktisch ist
  • Sie möchten, dass die allgegenwärtige Sprache bis zum 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.

Soweit ich das Buch verstehe, heißt es, dass dieses "Analysemodell" ohne Berücksichtigung der Softwareimplementierung entworfen werden kann. Sobald Entwickler versuchen, das von der Geschäftsseite verstandene Modell zu implementieren, bilden sie aus Gründen der Notwendigkeit ihre eigenen Abstraktionen, was zu einer Wand 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 zu dieser Kluft führen.

Sie können also davon ausgehen, dass das Üben einer Trennung von Bedenken wie z. B. der Persistenz dazu beitragen kann, ein abweichendes Analysemodell vor diesem Design zu schützen. Wenn es notwendig erscheint, Dinge wie Persistenz in das Modell einzuführen, ist dies eine rote Fahne. Vielleicht ist das Modell für die Implementierung nicht praktisch.

Zitat:

"Das einzelne Modell verringert die Fehlerwahrscheinlichkeit, da das Design nun 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, wenn Sie am Ende mehr Codezeilen haben, die sich mit Dingen wie dem Datenbankzugriff befassen, verlieren Sie diese Kommunikationsfähigkeit.

Wenn Sie auf eine Datenbank zugreifen müssen, um beispielsweise die Eindeutigkeit zu überprüfen, 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

klicken Sie unter "Szenarien für die Nichtverwendung des Domänenmodells" auf das gleiche Thema.

So trennen Sie den Datenzugriff

Laden von Daten über eine Schnittstelle

Die "Datenzugriffsebene" 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 Benutzeroberfläche trennt den Installationscode für den Datenzugriff, sodass Sie weiterhin Tests schreiben können. Der Datenzugriff kann von Fall zu Fall abgewickelt werden und bietet eine bessere Leistung als eine generische Strategie.

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

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

Ich habe diese Methode auf realen Systemen ausprobiert. Am Ende ändern Sie den Umfang dessen, was ständig geladen wird, um Leistungsprobleme zu beheben. Am Ende schauen Sie hinter die Benutzeroberfläche und sehen sich den Datenzugriffscode an, um zu sehen, was geladen wird und was nicht.

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

Fazit: Ziemlich geringer Abstand!

Faules Laden

Daten werden bei Bedarf geladen. Aufrufe zum Laden von Daten werden im Objektdiagramm selbst ausgeblendet. Wenn Sie auf eine Eigenschaft zugreifen, wird möglicherweise eine SQL-Abfrage ausgeführt, 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 die genaue Menge sein, die der Code erfordert.

Nachteile: Wenn Sie mit einem Leistungsproblem konfrontiert sind, ist es schwierig, das Problem zu beheben, wenn Sie eine allgemeine Lösung mit dem Titel "Einheitsgröße" haben. Ein verzögertes Laden kann insgesamt zu einer schlechteren Leistung führen, und das Implementieren eines verzögerten Ladens kann schwierig sein.

Rollenschnittstelle/Eager Fetching

Jeder Anwendungsfall wird durch ein Rolleninterface explizit dargestellt, das von der Aggregatklasse implementiert wird, sodass Datenladestrategien pro Anwendungsfall behandelt werden können.

Die Abrufstrategie könnte 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 so 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, wodurch eine optimale Leistung erzielt wird. Entspricht dem Prinzip der Schnittstellentrennung . Keine komplexen Code-Anforderungen. Aggregate-Unit-Tests müssen nicht die Ladestrategie imitieren. In den meisten Fällen kann eine generische Ladestrategie verwendet werden (z. B. eine "Lade alle" -Strategie), und bei Bedarf können spezielle Ladestrategien implementiert werden.

Nachteile: Der Entwickler muss die Abrufstrategie nach einer Änderung des Domain-Codes noch anpassen/überprüfen.

Beim Abrufstrategieansatz werden Sie möglicherweise immer noch den benutzerdefinierten Abrufcode ändern, um die Geschäftsregeln zu ändern. Es ist keine perfekte Trennung von Bedenken, aber es wird am Ende wartbarer und ist besser als die erste Option. Die Abrufstrategie kapselt das WIE, WANN und WO Daten geladen werden. Es hat eine bessere Trennung von Bedenken, ohne an Flexibilität zu verlieren, wie die Einheitsgröße für alle langsamen Ladevorgänge.

12
tomtg

Ich fand, dass dieser Blog ziemlich gute Argumente gegen das Einkapseln von Repositories in Entities hat:

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

11
ahaaman

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), hier sind meine bisherigen Entdeckungen.

Zuallererst mögen wir ein Rich Domain Model, das uns hohe Auffindbarkeit (von dem, was wir mit einem Aggregat machen können) und Lesbarkeit ( ausdrucksstarke Methodenaufrufe).

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

Wir möchten dies erreichen, ohne dem Konstruktor einer Entität Services hinzuzufügen, weil:

  • Die Einführung eines neuen Verhaltens (das einen neuen Dienst verwendet) kann zu einer Konstruktoränderung führen, dh die Änderung wirkt sich auf jede Zeile aus, die die Entität instanziiert!
  • Diese Services sind nicht Teil des Modells, aber Konstruktor-Injection würde darauf hindeuten, 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? Mit Gutschriften mache ich gar nichts ...)
  • Es würde es schwer machen, zu instanziieren, also schwer zu testen.
  • Das Problem breitet sich leicht aus, da andere Entitäten, die dieses enthalten, die gleichen Abhängigkeiten erhalten - was auf ihnen sehr nnatürliche Abhängigkeiten aussehen kann.

Wie können wir das dann tun? Meine Schlussfolgerung bisher ist, dass Methodenabhängigkeiten und Doppelversand eine anständige Lösung bieten.

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 zuständig ist. Es verwendet Doppelversand, vollständig Auslagerung der Arbeit an den zuständigen Dienst, während Auffindbarkeit aufrechterhalten von der Entität Invoice.

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

Um den Client-Code zu vereinfachen, können wir uns stattdessen über ein IInvoiceService anmelden. Schließlich scheint die Rechnungserfassung einer Rechnung ziemlich eigen zu sein. Ein solches einzelnes IInvoiceService vermeidet die Notwendigkeit aller Arten von Minidiensten für verschiedene Operationen. Der Nachteil ist, dass es dunkel wird, was genau dieser Dienst tun wird . Es könnte sogar anfangen wie doppelter Versand auszusehen , während der größte Teil der Arbeit noch in SetStatus() selbst erledigt wird.

Wir könnten den Parameter immer noch "Logger" nennen, in der Hoffnung, unsere Absicht zu enthüllen. Scheint allerdings etwas schwach.

Stattdessen würde ich nach einem IInvoiceLogger fragen (wie wir es bereits im Codebeispiel tun) und IInvoiceService diese Schnittstelle implementieren lassen. Der Client-Code kann einfach seine einzelnen IInvoiceService für alle Invoice Methoden verwenden, die nach einem solchen speziellen, rechnungsinternen "Mini-Service" fragen, während die Methodensignaturen immer noch deutlich machen, was passiert sie fragen nach.

Ich stelle fest, dass ich nicht Repositorys explizit angesprochen habe. Nun, der Logger ist oder benutzt 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 lästigen faulen Lasten.

pdate: Ich habe den folgenden Text aus historischen Gründen hinterlassen, schlage aber vor, faulen Lasten zu 100% aus dem Weg zu gehen.

Für echte, eigenschaftsbasierte verzögerte Ladevorgänge verwende ich derzeit die Konstruktorinjektion, jedoch auf persistenzunabhängige 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));
    }
}

Einerseits kann ein Repository, das ein Invoice aus der Datenbank lädt, freien Zugriff auf eine Funktion haben, die die entsprechenden Gutschriften lädt und diese Funktion in das Invoice einfügt.

Andererseits übergibt Code, der ein aktuelles neues Invoice erstellt, 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 von IEnumerable befreien, 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!

9
Timo

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

Gründe, die mir einfallen, sind:

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

vernon Vaughn gibt einfach eine Lösung:

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

Ich habe gelernt, objektorientierte Programmierung zu programmieren, bevor all diese unterschiedlichen Layer-Summen auftauchen und meine ersten Objekte/Klassen DID direkt der Datenbank zugeordnet werden.

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

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

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

Trennen der Daten in eine Repository Ebene (a.k.a. Data Access layer), kann als eine gute Programmiertechnik angesehen werden, nicht nur als eine Regel, der zu folgen ist.

Wie bei vielen Methoden möchten Sie möglicherweise Ihr Programm starten, indem Sie es 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 anderen Arbeit geleistet hat, alle zusammen den Verdienst ;-)

1
umlcat

Um Carolina Lilientahl zu zitieren: "Muster sollten Zyklen verhindern" https://www.youtube.com/watch?v=eJjadzMRQAk , wobei sie sich auf zyklische Abhängigkeiten zwischen Klassen bezieht. Im Fall von Repositorys in Aggregaten besteht die Versuchung, zyklische Abhängigkeiten zu erstellen, da die Objektnavigation nur aus Gründen der Zweckmäßigkeit möglich ist. Das oben von prograhammer erwähnte Muster, das von Vernon Vaughn empfohlen wurde, wobei andere Aggregate durch IDs anstelle von Stamminstanzen referenziert werden (gibt es einen Namen für dieses Muster?), Schlägt eine Alternative vor, die möglicherweise zu anderen Lösungen führt.

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

(Time0): Zwei Klassen, Sample und Well, verweisen aufeinander (zyklische Abhängigkeit). "Well" bezieht sich auf "Sample" und "Sample" bezieht sich aus Bequemlichkeitsgründen auf "Well" (manchmal werden Proben geloopt, manchmal werden alle Wells in einer Platte geloopt). Ich konnte mir keine Fälle vorstellen, in denen Sample nicht auf den Brunnen zurückgreifen würde, in dem es platziert ist.

(Zeitpunkt 1): Ein Jahr später werden viele Anwendungsfälle implementiert .... und es gibt Fälle, in denen die Probe nicht mehr auf den Brunnen zurückgreifen sollte, in dem sie platziert ist. In einem Arbeitsschritt 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 seltsames Verhalten auf, wenn jemand versucht, neue Funktionen zu implementieren. Es braucht Zeit, um einzudringen.

Mir hat auch dies geholfen Artikel , das oben über negative Aspekte des faulen Ladens erwähnt wurde.

0
Edvard Englund

Entstammt das einem Buch von Eric Evans Domain Driven Design oder stammt es von einer anderen Stelle?

Es ist altes Zeug. Eric`s Buch hat es nur ein bisschen lauter gemacht.

Wo gibt es einige gute Erklärungen für die Argumentation dahinter?

Die Vernunft ist einfach - der menschliche Geist wird schwach, wenn er mit vage verwandten multiplen Kontexten konfrontiert ist. 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", was sich in schlechter Produktivität und Fehlern niederschlägt.

Die Geschäftslogik sollte so klar wie möglich wiedergegeben werden. Fremdschlüssel, Normalisierung und objektrelationale Zuordnung stammen aus einem völlig anderen Bereich - diese Dinge sind technisch und computerbezogen.

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

bearbeiten: Zur Verdeutlichung: Ich spreche nicht von der klassischen OO Praxis, den Datenzugriff von der Geschäftslogik in eine separate Ebene zu trennen soll überhaupt mit der Datenzugriffsebene sprechen (dh sie sollen keine Verweise auf Repository-Objekte enthalten)

Der Grund ist immer noch derselbe, den ich oben erwähnt habe. Hier ist es nur ein Schritt weiter. Warum sollten Entitäten teilweise unwissend sein, wenn sie (zumindest in der Nähe von) vollständig sein können? Weniger domänenunabhängige Bedenken hat unser Modell - mehr Raum zum Atmen, den unser Geist bekommt, wenn er es neu interpretieren muss.

0
Arnis Lapsa