Eine typische Implementierung eines DDD-Repository sieht nicht besonders gut aus, zum Beispiel eine save()
Methode:
package com.example.domain;
public class Product { /* public attributes for brevity */
public String name;
public Double price;
}
public interface ProductRepo {
void save(Product product);
}
Infrastrukturteil:
package com.example.infrastructure;
// imports...
public class JdbcProductRepo implements ProductRepo {
private JdbcTemplate = ...
public void save(Product product) {
JdbcTemplate.update("INSERT INTO product (name, price) VALUES (?, ?)",
product.name, product.price);
}
}
Eine solche Schnittstelle erwartet Product
, dass es sich zumindest bei Gettern um ein anämisches Modell handelt.
Andererseits sagt OOP, dass ein Product
Objekt wissen sollte, wie es sich selbst retten kann.
package com.example.domain;
public class Product {
private String name;
private Double price;
void save() {
// save the product
// ???
}
}
Die Sache ist, wenn der Product
weiß, wie er sich selbst speichert, bedeutet dies, dass der Infrastrukturcode nicht vom Domänencode getrennt ist.
Vielleicht können wir die Speicherung an ein anderes Objekt delegieren:
package com.example.domain;
public class Product {
private String name;
private Double price;
void save(Storage storage) {
storage
.with("name", this.name)
.with("price", this.price)
.save();
}
}
public interface Storage {
Storage with(String name, Object value);
void save();
}
Infrastrukturteil:
package com.example.infrastructure;
// imports...
public class JdbcProductRepo implements ProductRepo {
public void save(Product product) {
product.save(new JdbcStorage());
}
}
class JdbcStorage implements Storage {
private final JdbcTemplate = ...
private final Map<String, Object> attrs = new HashMap<>();
private final String tableName;
public JdbcStorage(String tableName) {
this.tableName = tableName;
}
public Storage with(String name, Object value) {
attrs.put(name, value);
}
public void save() {
JdbcTemplate.update("INSERT INTO " + tableName + " (name, price) VALUES (?, ?)",
attrs.get("name"), attrs.get("price"));
}
}
Was ist der beste Ansatz, um dies zu erreichen? Ist es möglich, ein objektorientiertes Repository zu implementieren?
Antworten:
Sie schrieben
und in einem Kommentar.
Dies ist ein weit verbreitetes Missverständnis.
Product
Ist ein Domain-Objekt, so sollte es für die Domain- Operationen verantwortlich sein, die ein einzelnes Produktobjekt betreffen , nicht weniger, nicht mehr - also definitiv nicht für alle Operationen. In der Regel wird Persistenz nicht als Domänenoperation angesehen. Im Gegenteil, in Unternehmensanwendungen ist es nicht ungewöhnlich, Persistenz-Ignoranz im Domänenmodell zu erreichen (zumindest bis zu einem gewissen Grad), und die Persistenzmechanik in einer separaten Repository-Klasse zu halten, ist eine beliebte Lösung dafür. "DDD" ist eine Technik, die auf diese Art von Anwendungen abzielt.Was könnte also eine sinnvolle Domainoperation für eine sein
Product
? Dies hängt tatsächlich vom Domänenkontext des Anwendungssystems ab. Wenn es sich bei dem System um ein kleines System handelt, das nur CRUD-Operationen unterstützt,Product
kann es durchaus vorkommen , dass ein System wie in Ihrem Beispiel "anämisch" bleibt. Für solche Anwendungen ist es möglicherweise umstritten, ob sich das Einordnen der Datenbankoperationen in eine separate Repo-Klasse oder die Verwendung von DDD überhaupt lohnt.Sobald Ihre Anwendung jedoch reale Geschäftsvorgänge unterstützt, z. B. den Kauf oder Verkauf von Produkten, die Vorratshaltung und -verwaltung oder die Berechnung von Steuern für diese Vorgänge, werden häufig Vorgänge erkannt, die sinnvoll in eine
Product
Klasse eingeordnet werden können . Beispielsweise könnte es eine Operation geben,CalcTotalPrice(int noOfItems)
die den Preis für n Artikel eines bestimmten Produkts unter Berücksichtigung von Mengenrabatten berechnet.Kurz gesagt, wenn Sie Klassen entwerfen, müssen Sie über Ihren Kontext nachdenken, in welcher der fünf Welten von Joel Spolsky Sie sich befinden, und wenn das System genügend Domänenlogik enthält, ist DDD von Vorteil . Wenn die Antwort Ja lautet, ist es ziemlich unwahrscheinlich, dass Sie ein anämisches Modell erhalten, nur weil Sie die Persistenzmechanik außerhalb der Domänenklassen halten.
quelle
Account.transfer(amount)
sollte die Übertragung bestehen bleiben. Wie es das macht, liegt in der Verantwortung des Objekts, nicht in der einer externen Entität. Das Anzeigen des Objekts hingegen ist in der Regel eine Domain-Operation! Die Anforderungen beschreiben in der Regel sehr detailliert, wie das Zeug aussehen soll. Es ist Teil der Sprache unter Projektmitgliedern, Unternehmen oder auf andere Weise.Account.transfer
für beinhaltet normalerweise zwei Kontenobjekte und eine Arbeitseinheit. Die Transaktions-Persistenz-Operation könnte dann Teil der letzteren sein (zusammen mit Aufrufen verwandter Repos), sodass sie von der "Transfer" -Methode ausgeschlossen bleibt. Auf diese WeiseAccount
kann Beharrlichkeit ignorant bleiben. Ich sage nicht, dass dies unbedingt besser ist als Ihre vermeintliche Lösung, aber Ihre ist auch nur einer von mehreren möglichen Ansätzen.Praxis übertrumpft Theorie.
Die Erfahrung lehrt uns, dass Product.Save () zu vielen Problemen führt. Um diese Probleme zu umgehen, haben wir das Repository-Muster erfunden.
Sicher, es verstößt gegen die OOP-Regel, die Produktdaten zu verbergen. Aber es funktioniert gut.
Es ist viel schwieriger, einheitliche Regeln zu erstellen, die alles abdecken, als allgemeine Regeln zu erstellen, die Ausnahmen enthalten.
quelle
Es ist hilfreich, sich vor Augen zu halten, dass es keine Spannung zwischen diesen beiden Ideen geben soll - Wertobjekte, Aggregate, Repositorys sind eine Reihe von Mustern, die verwendet werden, was einige für richtig halten.
Nicht so. Objekte kapseln ihre eigenen Datenstrukturen. Ihre In-Memory-Darstellung eines Produkts ist dafür verantwortlich, dass das Produktverhalten angezeigt wird (unabhängig davon, was es ist). aber der persistente Speicher ist dort (hinter dem Repository) und hat seine eigene Arbeit zu erledigen.
Es muss eine Möglichkeit geben, Daten zwischen der In-Memory-Darstellung der Datenbank und ihrem dauerhaften Andenken zu kopieren . An der Grenze tendieren die Dinge dazu, ziemlich primitiv zu werden.
Grundsätzlich sind reine Schreibdatenbanken nicht besonders nützlich, und ihre Entsprechungen im Arbeitsspeicher sind nicht nützlicher als die "dauerhafte" Sortierung. Es hat keinen Sinn, Informationen in ein
Product
Objekt einzufügen, wenn Sie diese Informationen niemals löschen wollen. Sie werden nicht unbedingt "Getter" verwenden - Sie versuchen nicht, die Produktdatenstruktur freizugeben, und Sie sollten auf keinen Fall einen veränderlichen Zugriff auf die interne Darstellung des Produkts freigeben.Das funktioniert auf jeden Fall - Ihr persistenter Speicher wird effektiv zum Rückruf. Ich würde das Interface wahrscheinlich einfacher machen:
Es wird gehen zwischen dem in Speicherdarstellung und der Speichermechanismus zu koppeln, da die Informationen von hier nach dort zu kommen brauchen (und wieder zurück). Das Ändern der Informationen, die geteilt werden sollen, wirkt sich auf beide Enden der Konversation aus. Also können wir das genauso gut explizit machen, wo wir können.
Dieser Ansatz - die Weitergabe von Daten über Rückrufe - spielte eine wichtige Rolle bei der Entwicklung von Mocks in TDD .
Beachten Sie, dass die Weitergabe der Informationen an den Rückruf dieselben Einschränkungen aufweist wie die Rückgabe der Informationen aus einer Abfrage. Sie sollten keine veränderlichen Kopien Ihrer Datenstrukturen weitergeben.
Dieser Ansatz steht ein wenig im Widerspruch zu dem, was Evans im Blue Book beschrieben hat, in dem das Zurückgeben von Daten über eine Abfrage die übliche Vorgehensweise war und Domänenobjekte speziell entwickelt wurden, um das Einmischen von "Persistenzbedenken" zu vermeiden.
Beachten Sie Folgendes: Das Blaue Buch wurde vor fünfzehn Jahren geschrieben, als Java 1.4 die Erde durchstreifte. Insbesondere ist das Buch älter als Java- Generika - wir haben jetzt viel mehr Techniken zur Verfügung, als Evans seine Ideen entwickelte.
quelle
Storage
Benutzeroberfläche auf die gleiche Weise wie Sie entworfen, dann habe ich über hohe Kopplung nachgedacht und sie geändert. Aber Sie haben Recht, es gibt ohnehin eine unvermeidliche Kopplung, warum also nicht expliziter.Sehr gute Beobachtungen, da stimme ich Ihnen voll und ganz zu. Hier ist eine Rede von mir (Korrektur: nur Folien) zu genau diesem Thema: Objektorientiertes domänengetriebenes Design .
Kurze Antwort: nein. In Ihrer Anwendung sollte sich kein Objekt befinden , das rein technisch ist und keine Domain-Relevanz hat. Das entspricht dem Implementieren des Protokollierungsframeworks in einer Buchhaltungsanwendung.
Ihr
Storage
Schnittstellenbeispiel ist ausgezeichnet, vorausgesetzt, dasStorage
wird dann als ein externes Framework betrachtet, auch wenn Sie es schreiben.Außerdem sollte
save()
in einem Objekt nur erlaubt sein, wenn das Teil der Domain ist (die "Sprache"). Zum Beispiel sollte ich nicht aufgefordert werden, einAccount
nach dem Anruf explizit "zu speichern"transfer(amount)
. Ich sollte zu Recht damit rechnen, dass die Geschäftsfunktiontransfer()
meine Übertragung fortsetzt.Alles in allem finde ich die Ideen von DDD gut. Verwenden der allgegenwärtigen Sprache, Trainieren der Domäne mit Konversation, begrenzten Kontexten usw. Die Bausteine müssen jedoch grundlegend überarbeitet werden, um mit der Objektorientierung kompatibel zu sein. Einzelheiten finden Sie im verknüpften Deck.
quelle
AccountNumber
sollte wissen, dass es als dargestellt werden kannTextField
. Wenn andere (wie ein „View“) das wissen würde, dass das ist.Anhängerkupplung nicht existieren sollten, weil diese Komponente wissen müsste , wasAccountNumber
die Interna, also besteht.Vermeiden Sie es, unnötig Wissen über Felder zu verbreiten. Je mehr Informationen über ein einzelnes Feld vorliegen, desto schwieriger wird es, ein Feld hinzuzufügen oder zu entfernen:
Hier hat das Produkt keine Ahnung, ob Sie in eine Protokolldatei oder eine Datenbank oder beides speichern. Hier hat die Speichermethode keine Ahnung, ob Sie 4 oder 40 Felder haben. Das ist locker gekoppelt. Das ist gut.
Dies ist natürlich nur ein Beispiel dafür, wie Sie dieses Ziel erreichen können. Wenn Sie keine Zeichenfolge erstellen und analysieren möchten, die als DTO verwendet werden soll, können Sie auch eine Auflistung verwenden.
LinkedHashMap
ist ein alter Favorit von mir, da er die Ordnung beibehält und toString () in einer Protokolldatei gut aussieht.Wie auch immer du es tust, bitte verbreite keine Kenntnisse über Felder in der Umgebung. Dies ist eine Form der Kopplung, die die Leute oft ignorieren, bis es zu spät ist. Ich möchte, dass möglichst wenige Dinge statisch wissen, über wie viele Felder mein Objekt verfügt. Auf diese Weise erfordert das Hinzufügen eines Felds nicht viele Änderungen an vielen Stellen.
quelle
Map
, Sie schlagen aString
oder a vorList
. Aber, wie @VoiceOfUnreason in seiner Antwort erwähnte, ist die Kopplung immer noch da, nur nicht explizit. Es ist immer noch nicht erforderlich, die Datenstruktur des Produkts zu kennen, um es sowohl in einer Datenbank als auch in einer Protokolldatei zu speichern, zumindest wenn es als Objekt zurückgelesen wird.Storage
ist ein Teil der Domain (ebenso wie die Repository-Schnittstelle) und macht eine solche Persistenz-API. Bei Änderungen ist es besser, die Clients in der Kompilierungszeit zu informieren, da sie ohnehin reagieren müssen, um nicht in der Laufzeit kaputt zu gehen.Es gibt eine Alternative zu den bereits erwähnten Mustern. Das Memento-Muster eignet sich hervorragend zum Einkapseln des internen Zustands eines Domänenobjekts. Das Erinnerungsobjekt stellt eine Momentaufnahme des öffentlichen Zustands des Domänenobjekts dar. Das Domänenobjekt weiß, wie dieser öffentliche Zustand aus seinem internen Zustand erstellt wird und umgekehrt. Ein Endlager arbeitet dann nur mit der öffentlichen Vertretung des Staates. Damit ist die interne Implementierung von jeglichen Persistenzspezifikationen entkoppelt und muss lediglich den öffentlichen Auftrag aufrechterhalten. Auch Ihr Domain-Objekt muss keine Getter aussetzen, die es tatsächlich ein bisschen anämisch machen würden.
Für mehr zu diesem Thema empfehle ich das großartige Buch: "Patterns, Principles and Practices of Domain-Driven Design" von Scott Millett und Nick Tune
quelle