Vorbedingungen
Es sei angemerkt, dass man mit den Vorteilen der SOLID Prinzipien [1] vertraut sein sollte, um den ganzen Sinn von CQRS zu verstehen. Es entstanden im letzten Artikel viele Diskussionen über grundsätzliche Aspekte wie SRP oder OCL, weil diese Prinzipien nicht angewandt oder verstanden werden. Ebenso sollte man schon mal etwas über den Sinn von Layering und Modularität von Software gehört haben.
Zusammenfassung CQRS
Um noch mal die entscheidenden Punkte, die für einen Einsatz von CQRS
sprechen, zu erläutern, hier eine kurze Zusammenfassung:
In CQRS geht es um eine weitere Auftrennung von Verantwortlichkeiten bzw. Belangen in einer Software, über Methoden und Klassen bis hin zu den Schichten der Applikation selbst.
In einer Businessanwendung geht es vor allem darum, dass Anweisungen
zuverlässig und korrekt verarbeitet werden. Das Ergebnis sollte ein jederzeit gültiger
und erwarteter Zustand sein, der alle Regeln der Geschäftslogik einhält. Dabei spielen Faktoren wie u.a. Transaktionssicherheit, Konsistenz oder
Nebenläufigkeit eine Rolle.
Commands spiegeln, wie gesagt, den tatsächlichen Umfang und Wert der Software wider.
Was sie machen, ist der Funktionsumfang, also die Erfüllung gestellter Aufgaben.
Und wie gut sie es machen, die Qualität.
Die Summe aller Commands spiegelt den gesamten Funktionsumfang und die Fähigkeiten einer Domain wider.
Beim Anzeigen, Reporten und Überprüfen der Änderungen wiederum entstehen ganz anderen Anforderungen an die Software, z.B. Performance oder Skalierbarkeit.
Abfragen müssen nicht zwangsläufig aktuell oder konsistent sein. Dies ist übrigens auch aus der inzwischen verbreiteten NoSQL-Technologien bekannt: Eventual Consistency [2].
Da jede Art von Abfrage den Zustand zur Zeit der Abfrage selbst widerspiegelt aber nicht unbedingt den augenblicklichen Stand, liefert sie sehr oft sowieso nur „veraltete“ Daten.
Ein fehlerhaftes Reporting lässt sich immer wieder neu aufbauen.
Aber ein inkonsistenter Zustand der Domain und der Daten nur sehr schwer.
Umgekehrt kann man sagen, dass die meisten Anwendungen
wenige Befehle verarbeiten müssen, diese aber mit höchster Präzision. Hingegen müssen
sie aber ein Vielfaches an Querys abarbeiten und deren Ergebnisse schnell ausliefern.
Allein diese kurzen Erläuterungen zeigen schon, dass es einerseits komplett verschiedene Anforderungen an die Software gibt es aber auch komplett unterschiedliche Implementierungen geben sollte. Denn für so unterschiedliche Belange wird es kaum ein System geben, dass in der Lage ist, beide Belange optimal darzustellen.
CQRS in der Praxis: Implementierung
mit DomainEventing
Es gibt viele Möglichkeiten, CQRS zu implementieren. Einen
davon möchte ich hier vorstellen: Domain Eventing mit Commands und Events.
Wenn man den Wert von
CQRS erkannt hat und mit der Umsetzung beginnt, ist der nächste, fast
zwangsläufig logische, Schritt die Einführung einer Messagingarchitektur auf
Domainebene.
Es geht darum, dass das komplette Verhalten einer Domain über Commands gesteuert wird.Dabei entstehen Zustandsänderungen, wie Erzeugung von neuen Objekten oder beliebige andere Änderungen des Zustands.
Wem die Begriffe Repository und Aggregate nicht bekannt
sind: das sind Bausteine, die in Domain Driven Design [3] verwendet werden. Eine
Repository ist im Prinzip ein DAO und eine Aggregat nichts anderes als ein einzelnes
Objekt oder ein Objektgraph.
Das Wichtigste dabei ist: eine Repository stellt genau ein Aggregat zur Verfügung, das in der Lage ist, einen Command vollständig abzuarbeiten.
Das Wichtigste dabei ist: eine Repository stellt genau ein Aggregat zur Verfügung, das in der Lage ist, einen Command vollständig abzuarbeiten.
Die einzelnen Bausteine sind:
Command
Ein Command ist ein Befehl, der eine Statusänderung
durchführen soll.
Er ist immer im Imperativ benannt: CreateCustomer, AddContactPerson,
LockCustomer.
Technisch ist ein Command ein simples DTO, dass nur die
Daten bereithält, die zur Ausführung des Befehls gebraucht werden.
Ein Command wird an einen CommandBus (der mit mindestens einer Repository ausgestattet wird) geschickt, dieser sucht den entsprechenden Handler und übergibt diesem das Command.
Ein Command wird an einen CommandBus (der mit mindestens einer Repository ausgestattet wird) geschickt, dieser sucht den entsprechenden Handler und übergibt diesem das Command.
Werden diese Commands auch mitgeloggt erhält man damit eine komplette
Historie aller Vorgänge und Zustandsänderungen einer Domain.
CommandHandler
CommandHandler
Führt das entsprechende Kommando aus, in dem er über die Repository ein Aggregat holt und den Befehl an dieses delegiert, oder (über eine Factory) ein Aggregat erstellt und es über Repository/Unit of Work persistiert.
Event
Events sind Messages, die eine Zustandsänderung beschreiben.
Es sind geschehene Ereignisse, die aus
der Ausführung von Commands resultieren und deshalb immer in der Vergangenheitsform
benannt (CustomerLocked, MoneyTransferred) und nicht mehr änderbar (!) sind.
Sie werden zuerst im EventBus registriert (i.d.R. aus einem Service heraus) und an der Stelle erzeugt, an der das Command als abgearbeitet bezeichnet werden kann. Events werden an den EventBus geschickt (raise).
EventHandler
Wenn die Sicherstellung der Konsistenz eines Aggregates exakt bestimmt werden kann, wird an dieser Stelle dafür gesorgt, dass alle bisher gesammelten Events im EventBus veröffentlicht werden (publish).
Dies kann im Aggregat selbst, in unserem Fall jedoch oft erst in der Repository
sichergestellt werden (nach erfolgreichem save()). Der EventBus sucht alle
Events, die die Interfaces der registrierten Events implementieren und führt
diese aus. So können mehrere EventHandler nach einem bestimmt Event die weitere Arbeit
übernehmen.
CommandBus, EventBus
Das können vollwertige Messaging Lösungen (z.B. ActiveMQ)[4] sein, wenn es um verteilte Anwendungen oder asynchrone Prozesse geht. In der Regel sind es aber simple Container, die nur für eine Transaktion (Thread) aufgebaut werden und Commands und Events abhandeln und verwalten.
CommandBus, EventBus
Das können vollwertige Messaging Lösungen (z.B. ActiveMQ)[4] sein, wenn es um verteilte Anwendungen oder asynchrone Prozesse geht. In der Regel sind es aber simple Container, die nur für eine Transaktion (Thread) aufgebaut werden und Commands und Events abhandeln und verwalten.
Wichtig dabei sind folgende Dinge:
- Ein Command wird immer vollständig oder gar nicht abgearbeitet.
- Ein Command darf genau einmal abgearbeitet werden.
- Für jedes Command gibt es exakt EINEN CommandHandler.
- Commands und Events müssen serialisierbar sein.
- Für einen Event kann es mehrere EventHandler geben.
Vorteile DomainEventing
Dieser Mechanismus hat gleich mehrere Vorteile:
Commands und CommandHandlern geben präzise die Intention des
Befehls wider. CommandHandler sind quasi als Microservices anzusehen, durch den
Fokus auf die Erledigung genau einer Aufgabe sind sie zudem besonders gut
testbar.
Die Testbarkeit wird dadurch erhöht, dass man Testfälle so
definieren kann:
- Ausgangssituation (given)
- Befehl, der ausgeführt werden soll (when)
- Erwartetes Resultat bzw. eingetretenes Ereignis (then)
Man braucht also nicht mehr auf Werte zu testen, um zu
sehen, ob eine Zustandsänderung erfolgt ist (wovon sowieso stark abzuraten ist,
da es sehr anfällig ist und dem Prinzip der Kapselung widerspricht), sondern
braucht nur zu schauen, ob ein Event erzeugt wurde.
Beispiel „Kunden sperren“:
public
function testLockCustomerRaisesEvent ()
{
EventBus::clearAll();
EventBus::register('CustomerLocked');
$cmd = new LockCustomer(array(‘customerId’ => 1);
$cmdBus = new CommandBus($this->customerRepository);
$cmdBus->dispatch($cmd);
$event = EventBus::getLastPublished();
$this->assertType('CustomerLocked', $event);
}
|
Die Hauptaufgabe der Eventhandler ist es, weitere Prozesse
innerhalb eines Threads anzustoßen. Damit sie in der Domain Sinn machen, dürfen
sie z.B. nur innerhalb des Threads
weitere Commands erzeugen. Dies sollte man aber so weit wie möglich vermeiden.
Alle außerhalb liegenden (z.b. asynchrone oder auf anderen Systemen operierende) werden wie gehabt über bestehende Messaginglösungen abgearbeitet.
Alle außerhalb liegenden (z.b. asynchrone oder auf anderen Systemen operierende) werden wie gehabt über bestehende Messaginglösungen abgearbeitet.
Eine der häufigsten Aufgaben von EventHandlern ist die
Denormalisierung der Aggregate in die entsprechenden Datenhaltungen für die
Lesevorgänge zu erledigen.
Nachteile DomainEventing
Hm, gute Frage. Am ehesten noch die erhöhte Anzahl von Interfaces
und Klassen.
Das ist aber nicht wirklich ein Nachteil. Denn viele kleine
Klassen haben im Gegensatz zu wenigen aber zu großen Klassen sowieso Vorteile.
Fazit aus der Praxis
Bei einer aktuellen Anwendung, mit der ich mich als Teil
eines Teams gerade beschäftige, handelt
es sich um ein großes und komplexes System, dass aus knapp 1.000.000 Codezeilen, ca. 220 Datenbanktabellen und etwa 250 GB Daten besteht.
Wie es viele von uns (leider) kennen, gibt es kaum oder nur wenige Tests. Und wenn, dann meist nur Integrationstests. Also eine typische Legacy-Anwendung (mit Legacy meine ich hier nicht Fremdsysteme, sondern nicht getestete Software – was das gleiche ist.)
Wie es viele von uns (leider) kennen, gibt es kaum oder nur wenige Tests. Und wenn, dann meist nur Integrationstests. Also eine typische Legacy-Anwendung (mit Legacy meine ich hier nicht Fremdsysteme, sondern nicht getestete Software – was das gleiche ist.)
Dabei zeigte sich sehr schnell, dass es nur eine relativ
kleine Anzahl von Commands und Events im Verhältnis zur Größe des Sourcecodes
gibt.
Und es zeigte sich ebenso, wie man schrittweise Legacy Code umbauen kann, in dem man die Anforderungen schrittweise in Commands umsetzte und diese erst mal nur über Wrapper bestehende Services benutzen und später dann ausgetauscht werden.
Aber durch die oben erwähnte ideale Testbarkeit konnte man das sogar bei einem bisher kaum mit Tests versehenem System relativ einfach tun.
Und es zeigte sich ebenso, wie man schrittweise Legacy Code umbauen kann, in dem man die Anforderungen schrittweise in Commands umsetzte und diese erst mal nur über Wrapper bestehende Services benutzen und später dann ausgetauscht werden.
Aber durch die oben erwähnte ideale Testbarkeit konnte man das sogar bei einem bisher kaum mit Tests versehenem System relativ einfach tun.
Es wurde bei so einem System schnell klar, dass dies das
eigentliche Killerfeature von DomainEventing ist.