Wie genau sollte ein CQRS-Befehl validiert und in ein Domänenobjekt umgewandelt werden?

22

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 Carverwendet zwei Komponenten:

  • Transmission,
  • Engine.

Sowohl Transmissionund EngineWertobjekte dargestellt werden als Supertypen und haben gemäß Untertypen Automaticund ManualGetriebe, oder Petrolund ElectricMotoren sind.

In diesem Bereich lebt für sich allein eine erfolgreich erstellt Transmission, sei es Automaticoder Manual, oder jeder Typ eines Engineist völlig in Ordnung. Das CarAggregat führt jedoch einige neue Regeln ein, die nur anwendbar sind, wenn Transmissionund EngineObjekte im selben Kontext verwendet werden. Nämlich:

  • Wenn ein Auto einen ElectricMotor verwendet , ist der einzig zulässige Getriebetyp Automatic.
  • Wenn ein Auto einen PetrolMotor verwendet, kann er beide Typen haben Transmission.

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 CreateCarBefehl 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 CreateUserBefehls.

Der Befehl enthält einen Idder 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 UserRepositoryals 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 UserAggregat wirklich vorhanden sein sollten und dass der Befehlsvalidierer nur die Eindeutigkeit prüfen sollte. Wenn die Validierung erfolgreich ist, sollte ich das UserAggregat im erstellen CreateUserCommandHandlerund 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 UserRepositorymit einem UserObjekt 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.

Andy
quelle
brauche ein paar Beispielcodes, denke ich. Wie sehen Ihre Befehlsobjekte aus und wo erstellen Sie sie?
Ewan
@Ewan Ich werde später heute oder morgen Codebeispiele hinzufügen. Abfahrt in wenigen Minuten.
Andy
Als PHP-Programmierer schlage ich vor, einen Blick auf meine CQRS + ES-Implementierung zu
werfen
@ConstantinGALBENU Sollten wir Greg Youngs Interpretation von CQRS für richtig halten (was wir wahrscheinlich sollten), ist Ihr Verständnis von CQRS falsch - oder zumindest Ihre PHP-Implementierung. Befehle dürfen nicht direkt von Aggregaten verarbeitet werden. Befehle müssen von Befehlshandlern verarbeitet werden, die möglicherweise Änderungen an Aggregaten hervorrufen, die dann Ereignisse erzeugen, die für Statusreplikationen verwendet werden.
Andy
Ich denke nicht, dass unsere Interpretationen unterschiedlich sind. Sie müssen sich nur mehr mit DDD beschäftigen (auf der taktischen Ebene von Aggregates) oder Ihre Augen weiter öffnen. Es gibt mindestens zwei Arten der Implementierung von CQRS. Ich benutze einen von ihnen. Meine Implementierung ähnelt eher dem Actor-Modell und macht die Anwendungsebene sehr dünn, was immer gut ist. Ich habe festgestellt, dass es in diesen App-Diensten viele Code-Duplikate gibt, und habe beschlossen, sie durch ein zu ersetzen CommandDispatcher.
Constantin Galbenu

Antworten:

22

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).

Die eigentliche Frage lautet also: Wo genau liegt die Logik?

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:

  • die Struktur des Befehlsobjekts selbst; Der Konstruktor des Befehls enthält einige erforderliche Felder, die vorhanden sein müssen, damit der Befehl erstellt werden kann. Dies ist die erste und schnellste Validierung. Dies ist offensichtlich im Befehl enthalten.
  • Feldvalidierung auf niedriger Ebene, wie die Nicht-Leere einiger Felder (wie der Benutzername) oder das Format (eine gültige E-Mail-Adresse). Diese Art der Validierung sollte im Befehl selbst im Konstruktor enthalten sein. Es gibt einen anderen Stil, eine isValidMethode 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.
  • separate command validatorsKlassen, 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 validatorsAbhä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.
  • die aggregierten Methoden, die die Befehle empfangen und verarbeiten. Dies ist die letzte (Art) Validierung, die durchgeführt wird. Das Aggregat extrahiert die Daten aus dem Befehl und verwendet eine Kerngeschäftslogik, die es akzeptiert (es führt Änderungen an seinem Status durch) oder lehnt sie ab. Diese Logik wird stark konsistent überprüft. Dies ist die letzte Verteidigungslinie. In Ihrem Beispiel sollte die Regel When a car uses Electric engine the only allowed transmission type is Automatichier überprüft werden.

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 innerhalb der Befehlsvalidierung selbst vorhanden ist, könnte ein anderer Programmierer diese Validierung vollständig überspringen und die Speichermethode im UserRepository mit einem Benutzerobjekt direkt aufrufen, was aufgrund der E-Mail zu einem schwerwiegenden Datenbankfehler führen könnte könnte zu lang gewesen sein.

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, CommandDispatchersodass 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.

Als PHP-Entwickler, der für die Erstellung von REST-fähigen 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.

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ück HTTP 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:

Befehlspfad durch Befehlsvalidatoren auf dem Weg zum Aggregat

Constantin Galbenu
quelle
In Bezug auf Ihre Validierungsstrategie ist der zweite Punkt ein wichtiger Punkt für mich, an dem die Logik häufig dupliziert wird. Sicher möchte man, dass das Benutzeraggregat auch eine nicht leere und wohlgeformte E-Mail validiert. Nein? Dies wird deutlich, wenn wir einen ChangeEmail-Befehl einführen.
King-Side-Slide
@ king-side-slide nicht, wenn Sie ein EmailAddressWertobjekt haben, das sich selbst validiert.
Constantin Galbenu
Das ist völlig richtig. Man könnte ein kapseln 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. EmailAddressDies ist ein praktisches Beispiel, da für die gesamte Konzeption dieses Werts globale Validierungsanforderungen gelten.
King-Side-Slide
Ebenso erscheint die Idee eines "Befehlsvalidators" unnötig. Das Ziel ist nicht zu verhindern, dass ungültige Befehle erstellt und gesendet werden. Das Ziel ist es, zu verhindern, dass sie ausgeführt werden. Beispielsweise kann ich beliebige Daten mit einer URL übergeben. Wenn es ungültig ist, lehnt das System meine Anfrage ab. Der Befehl wird noch erstellt und versendet. Benötigt ein Befehl mehrere Aggregate zur Validierung (dh eine Sammlung von Benutzern zur Überprüfung der E-Mail-Eindeutigkeit), ist ein Domain-Service besser geeignet. Objekte wie "x validator" sind oft ein Zeichen für ein anämisches Modell, bei dem die Daten vom Verhalten getrennt werden.
King-Side-Slide
1
@ king-side-slide Ein konkretes Beispiel ist UserCanPlaceOrdersOnlyIfHeIsNotLockedValidator. Sie können sehen, dass dies eine separate Domäne der Bestellungen ist, sodass sie nicht vom OrderAggregate selbst validiert werden kann.
Constantin Galbenu
6

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 ChangeEmailBefehl erweitern, können wir perfekt veranschaulichen, warum Sie keine Geschäftslogik in Ihrer Befehlsinfrastruktur haben möchten, da Sie Ihre Regeln duplizieren müssten:

  • E-Mail kann nicht leer sein
  • E-Mail darf nicht länger als 100 Zeichen sein
  • E-Mail muss eindeutig sein

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 UserAggregat 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 gilt User, aber das ist wirklich nicht der Fall. Die "Einzigartigkeit" einer E-Mail gilt für eine Sammlung von Users(je nach Umfang).

Ah ha! In Anbetracht dessen wird deutlich, dass Ihre UserRepository(Ihre In-Memory-Sammlung von Users) 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 eine UserEmailAlreadyExistsAusnahme auslösen können ). Alternativ UserServicekönnte eine Domain dafür verantwortlich gemacht werden, neue UsersAttribute 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.

König-Seite-Rutsche
quelle
2
Ich stimme dem zu. Meine bisherige Lektüre (ohne CQRS) zeigt mir, dass die Validierung immer im Domänenmodell erfolgen sollte, um die Invarianten zu schützen. Jetzt lese ich CQRS und muss die Validierung in die Command-Objekte einfügen. Dies scheint nicht intuitiv zu sein. Kennen Sie Beispiele auf GitHub, bei denen die Validierung im Domain-Modell anstelle des Befehls abgelegt wird? +1.
w0051977