Ich habe CQRS 1 von Poor-Man schon seit einiger Zeit angepasst, weil ich die Flexibilität liebe, granulare Daten in einem Datenspeicher zu haben, die großartige Analysemöglichkeiten bieten und damit den Geschäftswert steigern, und bei Bedarf auch Lesevorgänge, die denormalisierte Daten enthalten, um die Leistung zu steigern .
Leider hatte ich von Anfang an Probleme damit, genau die richtige Geschäftslogik für diese Art von Architektur zu finden.
Soweit ich weiß, ist ein Befehl ein Mittel, um Absichten mitzuteilen, und ist nicht an eine Domäne gebunden. Es handelt sich im Grunde genommen um Daten (dumm - wenn Sie möchten), die Objekte übertragen. Auf diese Weise können Befehle problemlos zwischen verschiedenen Technologien übertragen werden. Gleiches gilt für Ereignisse als Reaktion auf erfolgreich abgeschlossene Ereignisse.
In einer typischen DDD-Anwendung befindet sich die Geschäftslogik in Entitäten, Wertobjekten und aggregierten Wurzeln. Sie sind sowohl reich an Daten als auch an Verhalten. Ein Befehl ist jedoch kein Domänenobjekt. Daher sollte er nicht auf die Darstellung von Daten in einer Domäne beschränkt sein, da diese dadurch zu stark belastet werden.
Die eigentliche Frage lautet also: Wo genau liegt die Logik?
Ich habe herausgefunden, dass ich diesem Kampf am häufigsten gegenüberstehe, wenn ich versuche, ein ziemlich kompliziertes Aggregat zu konstruieren, das einige Regeln für Kombinationen seiner Werte festlegt. Wenn ich Domänenobjekte modelliere, folge ich gerne dem Fail-Fast- Paradigma und weiß, dass ein Objekt in einem gültigen Zustand ist, wenn es eine Methode erreicht.
Angenommen, ein Aggregat Car
verwendet zwei Komponenten:
Transmission
,Engine
.
Sowohl Transmission
und Engine
Wertobjekte dargestellt werden als Supertypen und haben gemäß Untertypen Automatic
und Manual
Getriebe, oder Petrol
und Electric
Motoren sind.
In diesem Bereich lebt für sich allein eine erfolgreich erstellt Transmission
, sei es Automatic
oder Manual
, oder jeder Typ eines Engine
ist völlig in Ordnung. Das Car
Aggregat führt jedoch einige neue Regeln ein, die nur anwendbar sind, wenn Transmission
und Engine
Objekte im selben Kontext verwendet werden. Nämlich:
- Wenn ein Auto einen
Electric
Motor verwendet , ist der einzig zulässige GetriebetypAutomatic
. - Wenn ein Auto einen
Petrol
Motor verwendet, kann er beide Typen habenTransmission
.
Ich könnte diese Verletzung der Komponentenkombination auf der Ebene des Erstellens eines Befehls feststellen, aber wie ich zuvor ausgeführt habe, sollte dies meines Wissens nicht erfolgen, da der Befehl dann Geschäftslogik enthalten würde, die auf die Domänenebene beschränkt sein sollte.
Eine der Optionen besteht darin, diese Geschäftslogiküberprüfung auf die Befehlsüberprüfung selbst zu verschieben, aber dies scheint auch nicht richtig zu sein. Es fühlt sich so an, als würde ich den Befehl dekonstruieren, seine mit Gettern abgerufenen Eigenschaften überprüfen und sie im Validator vergleichen und die Ergebnisse überprüfen. Das schreit für mich wie ein Verstoß gegen das Gesetz von Demeter .
Wenn Sie die erwähnte Validierungsoption verwerfen, weil sie nicht praktikabel erscheint, sollten Sie den Befehl verwenden und das Aggregat daraus erstellen. Aber wo sollte diese Logik existieren? Sollte es sich innerhalb des Befehlshandlers befinden, der für die Verarbeitung eines konkreten Befehls verantwortlich ist? Oder sollte es vielleicht in der Befehlsüberprüfung sein (ich mag diesen Ansatz auch nicht)?
Ich verwende derzeit einen Befehl und erstelle daraus ein Aggregat im zuständigen Befehlshandler. Aber wenn ich das mache, sollte ich eine Befehlsüberprüfung haben, würde sie überhaupt nichts enthalten, denn sollte der CreateCar
Befehl existieren, würde sie Komponenten enthalten, von denen ich weiß, dass sie in getrennten Fällen gültig sind, aber das Aggregat könnte etwas anderes sagen.
Stellen wir uns ein anderes Szenario vor, in dem verschiedene Validierungsprozesse gemischt werden - Erstellen eines neuen Benutzers mithilfe eines CreateUser
Befehls.
Der Befehl enthält einen Id
der erstellten Benutzer und deren Email
.
Das System gibt die folgenden Regeln für die E-Mail-Adresse des Benutzers an:
- muss einzigartig sein,
- darf nicht leer sein,
- darf höchstens 100 Zeichen enthalten (maximale Länge einer db-Spalte).
In diesem Fall ist es zwar eine Geschäftsregel, eine eindeutige E-Mail zu haben, aber das Einchecken in einem Aggregat macht wenig Sinn, da ich den gesamten Satz aktueller E-Mails im System in einen Speicher laden und die E-Mail im Befehl überprüfen müsste gegen das Aggregat ( Eeeek! Etwas, etwas, Leistung.). Aus diesem Grund würde ich diese Prüfung in die Befehlsüberprüfung verschieben, die UserRepository
als Abhängigkeit verwendet und das Repository verwendet, um zu prüfen, ob ein Benutzer mit der im Befehl enthaltenen E-Mail bereits vorhanden ist.
Wenn es darum geht, ist es plötzlich sinnvoll, die beiden anderen E-Mail-Regeln auch in die Befehlsüberprüfung einzutragen. Ich habe jedoch das Gefühl, dass die Regeln in einem User
Aggregat wirklich vorhanden sein sollten und dass der Befehlsvalidierer nur die Eindeutigkeit prüfen sollte. Wenn die Validierung erfolgreich ist, sollte ich das User
Aggregat im erstellen CreateUserCommandHandler
und an ein Repository übergeben, um es zu speichern.
Ich fühle mich so, weil die Speichermethode des Repository wahrscheinlich ein Aggregat akzeptiert, das sicherstellt, dass alle Invarianten erfüllt sind, sobald das Aggregat übergeben wurde. Wenn die Logik (z. B. die Nicht-Leere) nur in der Befehlsvalidierung selbst vorhanden ist, könnte ein anderer Programmierer diese Validierung vollständig überspringen und die Speichermethode UserRepository
mit einem User
Objekt direkt aufrufen, was zu einem schwerwiegenden Datenbankfehler führen könnte, da die E-Mail möglicherweise einen hat zu lange her.
Wie gehen Sie persönlich mit diesen komplexen Validierungen und Transformationen um? Ich bin größtenteils zufrieden mit meiner Lösung, aber ich muss bestätigen, dass meine Ideen und Ansätze nicht völlig dumm sind, um mit den Entscheidungen zufrieden zu sein. Ich bin ganz offen für ganz andere Ansätze. Wenn Sie etwas haben, das Sie persönlich ausprobiert haben und das sehr gut für Sie funktioniert hat, würde ich gerne Ihre Lösung sehen.
1 Als PHP-Entwickler, der für die Erstellung von RESTful-Systemen verantwortlich ist, weicht meine Interpretation von CQRS ein wenig vom Standardansatz der asynchronen Befehlsverarbeitung ab , z. B. dass manchmal Ergebnisse von Befehlen zurückgegeben werden, weil Befehle synchron verarbeitet werden müssen.
CommandDispatcher
.Antworten:
Die folgende Antwort bezieht sich auf den CQRS-Stil, der von cqrs.nu unterstützt wird und in dem Befehle direkt auf den Aggregaten ankommen. In diesem Architekturstil werden die Anwendungsdienste durch eine Infrastrukturkomponente (den CommandDispatcher ) ersetzt, die das Aggregat identifiziert, lädt, den Befehl sendet und das Aggregat dann beibehält (als eine Reihe von Ereignissen, wenn Event-Sourcing verwendet wird).
Es gibt mehrere Arten von (Validierungs-) Logik. Die allgemeine Idee ist, die Logik so früh wie möglich auszuführen - scheitern Sie schnell, wenn Sie möchten. Die Situationen sind also wie folgt:
isValid
Methode zu haben, aber dies erscheint mir sinnlos, da sich jemand daran erinnern müsste, diese Methode aufzurufen, wenn tatsächlich eine erfolgreiche Befehlsinstanziierung ausreichen sollte.command validators
Klassen, die für die Validierung eines Befehls verantwortlich sind. Ich benutze diese Art der Validierung, wenn ich Informationen aus mehreren Aggregaten oder externen Quellen überprüfen muss. Sie können dies verwenden, um die Eindeutigkeit eines Benutzernamens zu überprüfen.Command validators
Abhängigkeiten können injiziert werden, z. B. Repositorys. Beachten Sie, dass diese Validierung letztendlich mit dem Aggregat übereinstimmt (dh wenn der Benutzer erstellt wird, kann in der Zwischenzeit ein anderer Benutzer mit demselben Benutzernamen erstellt werden)! Versuchen Sie auch nicht, hier eine Logik einzufügen, die sich innerhalb des Aggregats befinden sollte! Befehlsvalidatoren unterscheiden sich von den Sagas / Process-Managern, die Befehle basierend auf Ereignissen generieren.When a car uses Electric engine the only allowed transmission type is Automatic
hier überprüft werden.Mit den oben genannten Techniken kann niemand ungültige Befehle erstellen oder die Logik innerhalb der Aggregate umgehen. Befehlsvalidatoren werden automatisch von geladen und aufgerufen,
CommandDispatcher
sodass niemand einen Befehl direkt an das Aggregat senden kann. Man könnte eine Methode für das Aggregat aufrufen, die einen Befehl übergibt, aber die Änderungen nicht beibehalten, so dass dies sinnlos / harmlos wäre.Ich bin auch ein PHP-Programmierer und gebe nichts von meinen Kommandohandlern zurück (aggregierte Methoden im Formular
handleSomeCommand
). Ich gebe jedoch ziemlich oft Informationen an den Client / Browser zurückHTTP response
, z. B. die ID des neu erstellten Aggregatstamms oder etwas aus einem Lesemodell , aber ich gebe nie (wirklich nie ) etwas von meinen Aggregatbefehlsmethoden zurück. Die einfache Tatsache, dass der Befehl akzeptiert (und verarbeitet wurde - es handelt sich um eine synchrone PHP-Verarbeitung, oder ?!) ist ausreichend.Wir geben etwas an den Browser zurück (und machen immer noch CQRS nach dem Buch), weil CQRS keine High-Level-Architektur ist .
Ein Beispiel für die Funktionsweise von Befehlsüberprüfungen:
quelle
EmailAddress
Wertobjekt haben, das sich selbst validiert.EmailAddress
, um Doppelarbeit zu reduzieren. Noch wichtiger ist jedoch, dass Sie damit auch die Logik von Ihrem Befehl in Ihre Domäne verschieben. Es ist erwähnenswert, dass dies zu weit gehen kann. Häufig haben ähnliche Wissensbestandteile (Wertobjekte) unterschiedliche Validierungsanforderungen, je nachdem, von wem sie verwendet werden.EmailAddress
Dies ist ein praktisches Beispiel, da für die gesamte Konzeption dieses Werts globale Validierungsanforderungen gelten.UserCanPlaceOrdersOnlyIfHeIsNotLockedValidator
. Sie können sehen, dass dies eine separate Domäne der Bestellungen ist, sodass sie nicht vom OrderAggregate selbst validiert werden kann.Eine grundlegende Prämisse von DDD ist, dass Domänenmodelle sich selbst validieren. Dies ist ein kritisches Konzept, da es Ihre Domain zur verantwortlichen Partei für die Durchsetzung Ihrer Geschäftsregeln macht. Es behält auch Ihr Domain-Modell als Entwicklungsschwerpunkt bei.
Ein CQRS-System (wie Sie richtig hervorheben) ist ein Implementierungsdetail, das eine generische Unterdomäne darstellt, die ihren eigenen Kohäsionsmechanismus implementiert. Ihr Modell sollte in keiner Weise von einem Teil der CQRS-Infrastruktur abhängig sein, um sich gemäß Ihren Geschäftsregeln zu verhalten. Das Ziel von DDD ist es, das Verhalten zu modellieren eines Systems so dass das Ergebnis eine nützliche Abstraktion der funktionalen Anforderungen Ihrer Kerngeschäftsdomäne ist. Wenn Sie einen Teil dieses Verhaltens aus Ihrem Modell entfernen, obwohl dies verlockend ist, wird die Integrität und der Zusammenhalt Ihres Modells beeinträchtigt (und es wird weniger nützlich).
Indem Sie Ihr Beispiel einfach um einen
ChangeEmail
Befehl erweitern, können wir perfekt veranschaulichen, warum Sie keine Geschäftslogik in Ihrer Befehlsinfrastruktur haben möchten, da Sie Ihre Regeln duplizieren müssten:Da wir nun sicher sein können, dass unsere Logik in unserer Domäne sein muss, wollen wir uns mit dem Thema "Wo" befassen. Die ersten beiden Regeln können leicht auf unser
User
Aggregat angewendet werden , aber die letzte Regel ist etwas differenzierter. Eine, bei der weitere Kenntnisse erforderlich sind, um einen tieferen Einblick zu gewinnen. An der Oberfläche mag es so aussehen, als ob diese Regel für a giltUser
, aber das ist wirklich nicht der Fall. Die "Einzigartigkeit" einer E-Mail gilt für eine Sammlung vonUsers
(je nach Umfang).Ah ha! In Anbetracht dessen wird deutlich, dass Ihre
UserRepository
(Ihre In-Memory-Sammlung vonUsers
) möglicherweise ein besserer Kandidat für die Durchsetzung dieser Invariante ist. Die "save" -Methode ist wahrscheinlich der sinnvollste Ort, um die Prüfung einzuschließen (wo Sie eineUserEmailAlreadyExists
Ausnahme auslösen können ). AlternativUserService
könnte eine Domain dafür verantwortlich gemacht werden, neueUsers
Attribute zu erstellen und diese zu aktualisieren.Ein schneller Ausfall ist ein guter Ansatz, der jedoch nur ausgeführt werden kann, wenn und wo er zum Rest des Modells passt. Es kann sehr verlockend sein, die Parameter einer Anwendungsdienstmethode (oder eines Befehls) vor der weiteren Verarbeitung zu überprüfen, um Fehler abzufangen, wenn Sie (der Entwickler) wissen, dass der Aufruf irgendwo im Prozess fehlschlägt. Auf diese Weise haben Sie jedoch Kenntnisse auf eine Weise dupliziert (und durchgesickert), die wahrscheinlich mehr als eine Aktualisierung des Codes erforderlich macht, wenn sich die Geschäftsregeln ändern.
quelle