Freitag, 4. November 2011

Teile und Herrsche! – CQRS, Teil 2

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.

In der Praxis sieht das beispielweise so aus:



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.


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.
Werden diese Commands auch mitgeloggt erhält man damit eine komplette Historie aller Vorgänge und Zustandsänderungen einer Domain. 

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. 
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:
  1. Ausgangssituation (given)
  2. Befehl, der ausgeführt werden soll (when)
  3. 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.
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.)
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.
Es wurde bei so einem System schnell klar, dass dies das eigentliche Killerfeature von DomainEventing ist.

Keine Kommentare:

Kommentar veröffentlichen