Ist die Isolierung von Domänen- / Persistenzmodellen normalerweise so umständlich?

12

Ich beschäftige mich mit den Konzepten für Domain-Driven Design (DDD) und fand einige Prinzipien seltsam, insbesondere in Bezug auf die Isolierung von Domain- und Persistenzmodellen. Hier ist mein Grundverständnis:

  1. Ein Dienst auf der Anwendungsschicht (der einen Funktionssatz bereitstellt) fordert Domänenobjekte von einem Repository an, das er zur Ausführung seiner Funktion benötigt.
  2. Die konkrete Implementierung dieses Repositorys ruft Daten aus dem Speicher ab, für den es implementiert wurde
  3. Der Dienst weist das Domänenobjekt, das die Geschäftslogik kapselt, an, bestimmte Aufgaben auszuführen, die seinen Status ändern.
  4. Der Dienst weist das Repository an, das geänderte Domänenobjekt beizubehalten.
  5. Das Repository muss das Domänenobjekt wieder der entsprechenden Darstellung im Speicher zuordnen.

Flussillustration

Angesichts der obigen Annahmen erscheint Folgendes unangenehm:

Ad 2.:.

Das Domänenmodell scheint das gesamte Domänenobjekt (einschließlich aller Felder und Referenzen) zu laden, auch wenn sie für die angeforderte Funktion nicht benötigt werden. Das vollständige Laden ist möglicherweise nicht einmal möglich, wenn auf andere Domänenobjekte verwiesen wird, es sei denn, Sie laden auch diese Domänenobjekte und alle Objekte, auf die sie nacheinander verweisen, und so weiter und so fort. Das verzögerte Laden kommt in den Sinn, was jedoch bedeutet, dass Sie anfangen, Ihre Domänenobjekte abzufragen, für die in erster Linie das Repository verantwortlich sein sollte.

Angesichts dieses Problems scheint die "richtige" Art des Ladens von Domänenobjekten eine dedizierte Ladefunktion für jeden Anwendungsfall zu haben. Diese dedizierten Funktionen würden dann nur die Daten laden, die für den Anwendungsfall erforderlich sind, für den sie entwickelt wurden. Hier kommt die Unbeholfenheit ins Spiel: Erstens müsste ich für jede Implementierung des Repositorys eine beträchtliche Anzahl von Ladefunktionen aufrechterhalten, und Domänenobjekte würden in unvollständigen Zuständen enden, die nullin ihren Feldern enthalten sind. Letzteres sollte technisch gesehen kein Problem sein, da ein Wert, der nicht geladen wurde, von der Funktionalität, die ihn ohnehin angefordert hat, nicht benötigt werden sollte. Trotzdem ist es umständlich und eine potenzielle Gefahr.

Ad 3.:.

Wie würde ein Domänenobjekt die Eindeutigkeitsbeschränkungen bei der Erstellung überprüfen, wenn es keine Vorstellung vom Repository hat? Wenn ich beispielsweise eine neue Usermit einer eindeutigen Sozialversicherungsnummer (die angegeben wird) erstellen möchte, tritt der früheste Konflikt auf, wenn das Repository aufgefordert wird, das Objekt zu speichern, nur wenn in der Datenbank eine Eindeutigkeitsbeschränkung definiert ist. Andernfalls könnte ich nach einem Usermit der angegebenen Sozialversicherung suchen und einen Fehler melden, falls vorhanden, bevor ich einen neuen erstelle. Aber dann würden die Einschränkungsprüfungen im Dienst und nicht in dem Domänenobjekt leben, zu dem sie gehören. Ich habe gerade festgestellt, dass die Domänenobjekte sehr gut (injizierte) Repositorys zur Validierung verwenden dürfen.

Ad 5.:

Ich empfinde die Zuordnung von Domänenobjekten zu einem Speicher-Backend als arbeitsintensiven Prozess im Vergleich dazu, dass die Domänenobjekte die zugrunde liegenden Daten direkt ändern. Es ist natürlich eine wesentliche Voraussetzung, die konkrete Speicherimplementierung vom Domänencode zu entkoppeln. Aber ist es tatsächlich so teuer?

Sie haben anscheinend die Möglichkeit, ORM-Tools zu verwenden, um das Mapping für Sie durchzuführen. In diesen Fällen müssen Sie das Domänenmodell jedoch häufig gemäß den Einschränkungen des ORM entwerfen oder sogar eine Abhängigkeit von der Domäne zur Infrastrukturschicht einführen (z. B. mithilfe von ORM-Anmerkungen in den Domänenobjekten). Ich habe auch gelesen, dass ORMs einen erheblichen Rechenaufwand verursachen.

Wie können Sie bei NoSQL-Datenbanken, für die kaum ORM-ähnliche Konzepte existieren, nachverfolgen, welche Eigenschaften sich in den Domänenmodellen geändert haben save()?

Bearbeiten : Damit ein Repository auf den Status des Domänenobjekts zugreifen kann (dh auf den Wert jedes Felds), muss das Domänenobjekt seinen internen Status anzeigen, der die Kapselung unterbricht.

Im Algemeinen:

  • Wohin würde die Transaktionslogik gehen? Dies ist sicherlich persistenzspezifisch. Einige Speicherinfrastrukturen unterstützen möglicherweise überhaupt keine Transaktionen (z. B. In-Memory-Mock-Repositorys).
  • Muss ich bei Massenvorgängen, bei denen mehrere Objekte geändert werden, jedes Objekt einzeln laden, ändern und speichern, um die gekapselte Validierungslogik des Objekts zu durchlaufen? Dies steht im Gegensatz zur Ausführung einer einzelnen Abfrage direkt in der Datenbank.

Ich würde mich über eine Klarstellung zu diesem Thema freuen. Sind meine Annahmen richtig? Wenn nicht, wie können diese Probleme richtig angegangen werden?

Doppel M.
quelle
1
Gute Punkte und Fragen, die interessieren mich auch. Eine Randnotiz: Wenn Sie das Aggregat ordnungsgemäß modellieren, bedeutet dies, dass sich die Aggregatinstanz zu einem bestimmten Zeitpunkt in einem gültigen Zustand befinden muss, ist dies der Hauptpunkt des Aggregats (und es wird kein Aggregat als Kompositionscontainer verwendet). Das bedeutet auch, dass das Repository selbst zum Wiederherstellen des Aggregats aus den DB-Daten normalerweise einen bestimmten Konstruktor und eine Reihe von Mutationsoperationen verwenden muss, und ich sehe nicht, wie ein ORM automatisch wissen kann, wie diese Operationen ausgeführt werden .
Dusan
2
Noch enttäuschender ist, dass diese Fragen wie Ihre ziemlich oft gestellt werden, aber meines Wissens gibt es NULL Beispiele für die Implementierung der Aggregate und Repositories, die in dem Buch enthalten sind
Dusan

Antworten:

5

Ihr Grundverständnis ist korrekt und die Architektur, die Sie skizzieren, ist gut und funktioniert gut.

Wenn Sie zwischen den Zeilen lesen, scheinen Sie von einem datenbankzentrierteren aktiven Programmierstil für Datensätze zu stammen? Um zu einer funktionierenden Implementierung zu gelangen, müssten Sie dies tun

1: Domänenobjekte müssen nicht das gesamte Objektdiagramm enthalten. Zum Beispiel könnte ich haben:

public class Customer
{
    public string AddressId {get;set;}
    public string Name {get;set;}
}

public class Address
{
    public string Id {get;set;}
    public string HouseNumber {get;set;
}

Adresse und Kunde müssen nur dann Teil desselben Aggregats sein, wenn Sie eine Logik wie "Der Kundenname kann nur mit demselben Buchstaben wie der Hausname beginnen" haben. Sie vermeiden zu Recht verzögertes Laden und Lite-Versionen von Objekten.

2: Eindeutigkeitsbeschränkungen sind im Allgemeinen der Bereich des Repositorys und nicht das Domänenobjekt. Fügen Sie keine Repositorys in Domänenobjekte ein. Dies ist eine Rückkehr zum aktiven Datensatz. Es handelt sich lediglich um einen Fehler, wenn der Dienst versucht, ihn zu speichern.

Die Geschäftsregel lautet nicht "Es können niemals zwei Instanzen von Benutzern mit derselben SocialSecurityNumber gleichzeitig existieren".

Es ist so, dass sie nicht im selben Repository existieren können.

3: Es ist nicht schwer, Repositorys zu schreiben, anstatt einzelne Methoden zur Aktualisierung von Eigenschaften. In der Tat werden Sie feststellen, dass Sie so oder so ziemlich den gleichen Code haben. Es ist nur die Klasse, in die du es steckst.

ORMs sind heutzutage einfach und unterliegen keinen zusätzlichen Einschränkungen für Ihren Code. Ich persönlich bevorzuge es jedoch, die SQL einfach von Hand zu drehen. Es ist nicht so schwer, Sie haben nie Probleme mit ORM-Funktionen und können bei Bedarf optimieren.

Es ist wirklich nicht erforderlich zu verfolgen, welche Eigenschaften sich beim Speichern geändert haben. Halten Sie Ihre Domain-Objekte klein und überschreiben Sie einfach die alte Version.

Allgemeine Fragen

  1. Die Transaktionslogik wird im Repository abgelegt. Aber Sie sollten nicht viel davon haben. Sicher, Sie benötigen einige, wenn Sie untergeordnete Tabellen haben, in die Sie die untergeordneten Objekte des Aggregats einfügen, aber diese werden vollständig in der SaveMyObject-Repository-Methode gekapselt.

  2. Massenaktualisierungen. Ja, Sie sollten jedes Objekt einzeln ändern und dann einfach eine SaveMyObjects-Methode (Listenobjekte) zu Ihrem Repository hinzufügen, um die Massenaktualisierung durchzuführen.

    Sie möchten, dass das Domänenobjekt oder der Domänendienst die Logik enthält. Nicht die Datenbank. Das bedeutet, dass Sie nicht einfach "Kundensatzname = x aktualisieren, wobei y" aktualisieren, da nach allem, was Sie über das Kundenobjekt wissen, oder CustomerUpdateService 20 andere Dinge ausführt, wenn Sie den Namen ändern.

Ewan
quelle
Gute Antwort. Sie haben absolut Recht, ich bin an einen aktiven Datensatzcodierungsstil gewöhnt, weshalb das Repository-Muster auf den ersten Blick seltsam erscheint. Widersprechen die "schlanken" Domänenobjekte ( AddressIdstatt Address) nicht den OO-Prinzipien?
Double M
Nein, Sie haben noch ein Adressobjekt, es ist einfach kein Kind des Kunden
Ewan
Anzeigenobjektzuordnung ohne Änderungsverfolgung softwareengineering.stackexchange.com/questions/380274/…
Double M
2

Kurze Antwort: Ihr Verständnis ist richtig und die Fragen, die Sie haben, weisen auf gültige Probleme hin, für die die Lösungen weder einfach noch allgemein akzeptiert sind.

Punkt 2.: (Laden der vollständigen Objektgraphen)

Ich bin nicht der erste, der darauf hinweist, dass ORMs nicht immer eine gute Lösung sind. Das Hauptproblem besteht darin, dass ORMs nichts über den tatsächlichen Anwendungsfall wissen und daher keine Ahnung haben, was sie laden oder wie sie optimieren sollen. Das ist ein Problem.

Wie Sie sagten, besteht die offensichtliche Lösung darin, Persistenzmethoden für jeden Anwendungsfall zu haben. Wenn Sie dafür jedoch immer noch ein ORM verwenden, werden Sie vom ORM gezwungen, alles in Datenobjekte zu packen. Was, abgesehen davon, dass es nicht wirklich objektorientiert ist, für einige Anwendungsfälle nicht das beste Design ist.

Was ist, wenn ich nur einige Datensätze in großen Mengen aktualisieren möchte? Warum sollte ich für alle Datensätze eine Objektdarstellung benötigen? Etc.

Die Lösung dafür besteht darin, ein ORM nicht für Anwendungsfälle zu verwenden, für die es nicht geeignet ist. Implementieren Sie einen Anwendungsfall "natürlich" wie er ist, der manchmal weder eine zusätzliche "Abstraktion" der Daten selbst (Datenobjekte) noch eine Abstraktion über die "Tabellen" (Repositorys) erfordert.

Halb gefüllte Datenobjekte oder das Ersetzen von Objektreferenzen durch "IDs" sind bestenfalls Problemumgehungen, keine guten Designs, wie Sie betont haben.

Punkt 3.: (Überprüfung der Einschränkungen)

Wenn die Persistenz nicht abstrahiert wird, kann jeder Anwendungsfall offensichtlich leicht überprüfen, welche Einschränkung er wünscht. Die Anforderung, dass Objekte das "Repository" nicht kennen, ist völlig künstlich und kein Problem der Technologie.

Punkt 5.: (ORMs)

Es ist natürlich eine wesentliche Voraussetzung, die konkrete Speicherimplementierung vom Domänencode zu entkoppeln. Aber ist es tatsächlich so teuer?

Nein, das tut es nicht. Es gibt viele andere Möglichkeiten, um Ausdauer zu haben. Das Problem ist, dass das ORM immer als "die" zu verwendende Lösung angesehen wird (zumindest für relationale Datenbanken). Der Versuch, vorzuschlagen, es für einige Anwendungsfälle in einem Projekt nicht zu verwenden, ist zwecklos und abhängig vom ORM selbst manchmal sogar unmöglich, da Caches und späte Ausführung manchmal von diesen Tools verwendet werden.

Allgemeine Frage 1.: (Transaktionen)

Ich glaube nicht, dass es eine einzige Lösung gibt. Wenn Ihr Design objektorientiert ist, gibt es für jeden Anwendungsfall eine "Top" -Methode. Die Transaktion sollte da sein.

Jede andere Einschränkung ist völlig künstlich.

Allgemeine Frage 2: (Massenoperationen)

Mit einem ORM sind Sie (für die meisten mir bekannten ORMs) gezwungen, einzelne Objekte zu durchlaufen. Dies ist völlig unnötig und wäre wahrscheinlich nicht Ihr Design, wenn Ihre Hand nicht vom ORM gebunden würde.

Die Anforderung, "Logik" von SQL zu trennen, kommt von den ORMs. Das müssen sie sagen, weil sie es nicht unterstützen können. Es ist nicht von Natur aus "schlecht".

Zusammenfassung

Ich denke, mein Punkt ist, dass ORMs nicht immer das beste Werkzeug für ein Projekt sind, und selbst wenn dies der Fall ist, ist es höchst unwahrscheinlich, dass es für alle Anwendungsfälle in einem Projekt das beste ist.

Ebenso ist die Datenobjekt-Repository-Abstraktion von DDD nicht immer die beste. Ich würde sogar so weit gehen zu sagen, dass sie selten das optimale Design sind.

Das lässt uns keine einheitliche Lösung, daher müssten wir über Lösungen für jeden Anwendungsfall einzeln nachdenken, was keine gute Nachricht ist und unsere Arbeit offensichtlich schwieriger macht :)

Robert Bräutigam
quelle
Sehr interessante Punkte, danke, dass Sie meine Annahmen bestätigt haben. Sie sagten, es gibt viele andere Möglichkeiten, um Ausdauer zu haben. Können Sie ein performantes Entwurfsmuster für Graphendatenbanken (kein ORM) empfehlen, das weiterhin PI liefert?
Double M
1
Ich würde tatsächlich fragen, ob Sie überhaupt Isolation brauchen (und welche). Das Isolieren pro Technologie (dh Datenbank, Benutzeroberfläche usw.) bringt fast automatisch die "Unbeholfenheit" mit sich, die Sie vermeiden möchten, zum Vorteil eines etwas einfacheren Austauschs der Datenbanktechnologie. Die Kosten stellen jedoch eine schwierigere Änderung der Geschäftslogik dar, da sich diese über die Schichten erstreckt. Oder Sie können Geschäftsfunktionen aufteilen, was das Ändern von Datenbanken erschweren, aber das Ändern der Logik erleichtern würde. Welches willst du wirklich?
Robert Bräutigam
1
Sie können die beste Leistung erzielen, wenn Sie nur die Domäne (dh Geschäftsfunktionen) modellieren und die Datenbank nicht abstrahieren (ob relational oder grafisch keine Rolle spielt). Da die Datenbank nicht vom Anwendungsfall abstrahiert ist, kann der Anwendungsfall die optimalsten Abfragen / Aktualisierungen implementieren, die er wünscht, und muss kein umständliches Objektmodell durchlaufen, um das zu erreichen, was er will.
Robert Bräutigam
Nun, das Hauptziel ist es, die Bedenken hinsichtlich der Persistenz von der Geschäftslogik fernzuhalten, um sauberen Code zu haben, der leicht zu verstehen, zu erweitern und zu testen ist. Die Möglichkeit, DB-Technologien auszutauschen, ist nur ein Bonus. Ich kann sehen, dass es offensichtlich Reibungen zwischen Effizienz und Ignoranz gibt, die bei Grafik-DBs aufgrund der leistungsstarken Abfragen, die Sie verwenden können (aber nicht dürfen), stärker zu sein scheinen.
Double M
1
Als Java Enterprise-Entwickler kann ich Ihnen sagen, dass wir in den letzten zwei Jahrzehnten versucht haben, die Persistenz von der Logik zu trennen. Es funktioniert nicht. Erstens wurde eine Trennung nie wirklich erreicht. Noch heute gibt es in angeblich "geschäftlichen" Objekten alle möglichen datenbankbezogenen Dinge, wobei die wichtigste die Datenbank-ID (und viele Datenbankanmerkungen) ist. Zweitens wird, wie Sie sagten, manchmal Geschäftslogik in der Datenbank so oder so ausgeführt. Drittens ist dies der Grund, warum wir über bestimmte Datenbanken verfügen, um eine Logik auslagern zu können, die am besten dort ausgeführt wird, wo sich die Daten befinden.
Robert Bräutigam