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
.