Samstag, 26. November 2016

Domain Driven Design, CQRS, Event Sourcing, Hexagonale Architektur & mehr

Der Weg zur richtigen Software-Architektur kann manchmal lang sein. Oder sogar sehr lang. Insbesondere, wenn man nicht gerade hauptberuflich Software-Architekt ist und dabei quasi jeden Tag bei seinen Kunden die Entwicklung komplexer Software betreibt.

Wie kann Oregami nun den Weg zu einer guten Software-Architektur finden? Wer unser Projekt von Beginn an beobachtet hat, der weiß, dass wir schon mehrmals die bis dato ausgewählte Technik "über den Haufen geworfen" haben. Anfangs hatte ich eine klassische Schichtenarchitektur im Sinn und machte mir hauptsächlich Gedanken darüber, mit welchem Java-Framework wir unsere fachlichen Objekte speichern könnten und welche Datenbank-Software wir einsetzen würden. Bis zum heutigen Tag hat sich in meinem Kopf allerdings viel getan.

Während ich bei meinem ersten Web-Projekt (kultpower.de) vor ca. 15 Jahren noch mit PHP3 einfach drauflos programmierte, habe ich für die Spieledatenbank Oregami heutzutage ganz andere Ansprüche - nämlich viel höhere. Aus diesem Grund recherchiere ich in meiner Freizeit so viel wie möglich, wie man komplexe Software vernünftig programmiert. Wer sich z.B. die Liste der Personen anschaut, denen ich bei Twitter folge, wird die eine oder andere erfahrene IT-Persönlichkeit entdecken. Auf diese Weise konnte ich in den letzten Jahren viel erfahren über Dinge wie Spring BootHATEOASResponsive Web Design und Mobile FirstJSON Web TokensRoca-Style und vieles mehr.

Meine neuesten "Errungenschaften" beeinflussen - mal wieder - mein bisheriges technisches Gesamtbild der Oregami-Web-Anwendung. Im Sommer befasste ich mich endlich mal mit dem Thema "Domain Driven Design" (DDD), welches in Wikipedia beschrieben wird als "eine Herangehensweise an die Modellierung komplexer objektorientierter Software. Die Modellierung der Software wird dabei maßgeblich von den umzusetzenden Fachlichkeiten der Anwendungsdomäne beeinflusst." Bei dieser groben Umschreibung mag man sich vielleicht denken "Ja und?", aber beim Lesen des Buchs "Implementing Domain Driven Design" kamen mir gleich mehrere Erleuchtungen. Viele der dort behandelten Aspekte passen wie die Faust auf's Auge zu Problematiken, die mich bei der Entwicklung für Oregami bisher viel beschäftigt haben. Es geht sogar so weit, dass ich - bevor ich das Buch oder DDD überhaupt kannte - mir bereits selbst ähnliche Lösungsansätze überlegt hatte. Natürlich nur in Ansätzen, aber wenn man dann in so einem Buch liest, wie etwas "richtig" gemacht werden sollte, und seine eigenen Gedanken wieder erkennt, ist das schonmal nicht verkehrt.

Worum geht es nun konkret beim Domain Driven Design? Ich versuche mal, die für mich entscheidenden Punkte zusammenzufassen:
(Man möge mir den Mischmasch aus deutschen und englischen Begriffen verzeihen.)
  • Domänen-Experten arbeiten sehr eng mit den Entwicklern zusammen, um in einer gemeinsamen Sprache die Fachlichkeiten zu beschreiben.
  • Man erstellt gerade nicht ein gemeinsames, alles umfassendes Modell für sämtliche Fachlichkeiten der Anwendung, sondern man unterteilt die Fachlichkeit nach bestimmten Regeln in mehrere Teilmodelle ("Sub-Domains", "Bounded Contexts").
  • Man unterscheidet zwischen "echten Entitäten" und "Value objects". Man verwendet möglichst an vielen Stellen die einfacher zu beherrschenden "Value objects" anstelle von Entitäten, das schafft viele technische Vorteile.
  • Innerhalb eines Teilmodells gibt es nur genau ein Entität-Netzwerk ("Aggregate") mit einer Haupt-Entität, auch "Aggregate Root" genannt. Jede Transaktion, die Daten ändert, muss sich immer auf genau so ein Entität-Netzwerk beziehen. Es dürfen nicht mehrere Netzwerke in einer Transaktion angelegt oder geändert werden. 
  • Beziehungen zwischen zwei Teilmodellen dürfen nur die Haupt-Entität des jeweils anderen Aggregates referenzieren. Diese Haupt-Entität ist auch verantwortlich für alle Regeln (oft wird hier von Invarianten gesprochen), die das Teilmodell betreffen. Es darf keine Regeln geben, die über mehrere Teilmodelle hinweg gelten.
Bei weiteren Recherchen stieß ich auf die sog. "Hexagonale Software-Architektur". Alistair Cockburn schreibt darüber diesen zentralen Satz, der perfekt beschreibt, was man mit so einer Architektur erreichen möchte: "Sorge dafür, dass die Anwendung für menschliche Benutzer, Programme, automatisierte Tests und Batch-Skripte gleichermaßen gut benutzbar ist, und dass sie völlig isoliert von irgendwelchen Laufzeit-Einrichtungen und -Datenbanken entwickelt und getestet werden kann." (Übersetzung des Autors) Wow. Genauso sollte Software sein - universell einsetzbar, gut erweiterbar und gut testbar. Auch unter den Namen "Ports and Adapters" oder "Onion Architecture" wird beschrieben, wie ausgehend vom zentralen Programmcode für die eigentliche Dömane ("innen im Modell") über eine Service-Schicht hinüber zur Infrastruktur-Schicht keinerlei Abhängigkeiten von innen nach außen bestehen. Dadurch bleibt der zentrale Code völlig unabhängig von der Infrastruktur, wodurch man zum einen viel besser testen (Infrastruktur ist über "Ports" austauschbar, für Tests z.B. "in memory") und zum anderen Schnittstellen nach außen für "Clients" (also Software, die auf unsere Domäne zugreift) flexibel über sog. Adapter hinzufügen kann (z.B. ein Zugriff über "ReST", einer über Queues usw.).

Oft werden im Zusammenhang mit DDD und der Hexagonalen Architektur die Konzepte CQRS und Event Sourcing genannt. Fangen wir mit dem Event Sourcing an, welches beschreibt, dass der Zustand von Business-Objekten durch eine Abfolge von Events definiert wird. In der Praxis bedeutet das eine Abkehr von der traditionellen (oft relationalen) Speicherung von Daten, bei der jede Änderung direkt über Updates in der Datenbank gesichert wird. Stattdessen wird jede Änderung eines Zustandes durch ein Ereignis (Event) ausgelöst. Diese Ereignisse werden vom System gespeichert, und alle Anfragen nach dem aktuellen Zustand werden durch das erneute "Abspielen" der gespeicherten Events beantwortet. Aber was genau soll das bringen? Nichts weniger als bessere Skalierbarkeit und eine viel bessere Nachvollziehbarkeit aller Änderungen. Und letzteres ist genau das, was wir bei Oregami für ein System, in dem jede Änderung bzw. Eintragung neuer Daten zunächst kontrolliert und erst dann freigegeben werden soll, brauchen. Zu diesem Themengebiet passt dann auch das Konzept von CQRS (Command and Query Responsibility Segregation), bei dem man lesende Zugriffe (Queries) von schreibenden Zugriffen (Commands) trennt - auch hier ist die Skalierbarkeit ein sehr wichtiges Thema. Ich sehe es gewissermaßen vor mir: Die Benutzer von Oregami senden bei der Eingabe von Daten Commands, deren Zustandsänderungen als Events gespeichert werden. Nach einer Kontrolle der Eingaben durch so etwas wie einen Moderator werden die Events (oder nur einige davon) "freigegeben", was zu einer dauerhaften Änderung führt und dann zukünftige lesenden Abfragen entsprechend bedient. Das wiederum bringt mich dann dazu, wie denn die Oberfläche unserer Anwendung dafür aussehen muss. Anders als in meinen bisherigen Prototypen, in denen ich eher den CRUD-Ansatz verfolgt habe, klingt das hier alles eher nach Task based UIs. Dabei ist die Oberfläche auf das Auswählen und Absenden von vorgegebenen Befehlen (Commands) ausgerichtet, was zu den oben beschriebenen Ansätzen sehr gut passt. Beispiele für Oregami-Commands könnten sein: "Neues Spiel anlegen", "Release zu Spiel hinzufügen", "Screenshot hinzufügen" usw.

Doch was fangen wir jetzt mit all diesen Erkenntnissen an?

Ich kann mir folgendes vorstellen:
  1. Ich fange zum wiederholten Mal ein neues Git-Repository an: Oregami-DDD oder so ähnlich.
  2. Wir identifizieren unseren ersten Bounded Context, den wir nach den Regeln des DDD umsetzen wollen.
  3. Die notwendigen Entitäten und Value Objects werden erstellt.
  4. Use Cases werden erarbeitet und gemäß den Prinzipien einer hexagonalen Architektur umgesetzt.
  5. Änderungen am Zustand der Anwendung werden dabei über Commands und Events verarbeitet.
  6. Die Events müssen in der Anwendung einsehbar sein, denn darauf muss ja irgendwann die "Moderation von Dateneingaben" erfolgen.
  7. Später werden weitere Bounded Contexts nach dem gleichen Muster entwickelt. Spannend wird dabei die Kommunikation zwischen den BCs und die Integration mehrerer BCs für die Oberfläche. Und vielleicht benötigen wir ein spezielles, getrenntes Read-Model, das wird sich zeigen.
Hier noch für den interessierten Leser ein paar grundlegende oder weiterführende Links zu den oben genannten Themen:
Es liegen also auch weiterhin interessante Zeiten vor uns: Stay tuned! 

Kommentare:

  1. Hi Sebastian,

    seit einigen Jahren verfolge ich nun schon die Entwicklung dieses Projekts und finde es immer wieder interessant - Mir geht es als Software-Entwickler genau so (und so sollte es wohl auch sein). Die gesamte Thematik verändert sich fortwährend, was das Ganze auch so spannend macht.

    Allerdings sind mir hier ein paar (wenige :-) ) Dinge aufgefallen:

    Beim Event-Sourcing werden zum Lesen nicht alle Events jedes Mal erneut durchgespielt. Viel mehr wird ein Read-Model aufgebaut (Was die Verbindung zu CQRS mehr oder weniger notwendig macht), welches dann für Abfragen genutzt wird. In der Tat ist aber das Read-Model (bzw. die Read-Models) durch ein "Replay" der Events beliebig oft erneut wieder aufbaubar (Siehe Dein Punkt 7).

    Allerdings ist Event-Sourcing auch mit ein paar Fallstricken verbunden: Man muss als relativ "neue" Technologie aufpassen, was man tut (Und auch die Teammitglieder, da vielen dieses Konzept nicht bekannt ist). Wenn man Busniessregeln später ändert, muss man auf schon existierende Events achten (z. B. durch Versionierung der Eventtypen). Wie handhabt man asynchrones Processing der Events. Größerer initialer Aufwand (Commands, Events, Handling usw.) - Es sind schon einige Sachen :-)

    > Die Benutzer von Oregami senden bei der Eingabe von Daten Commands, deren Zustandsänderungen als Events gespeichert werden. Nach einer Kontrolle der Eingaben durch so etwas wie einen Moderator werden die Events (oder nur einige davon) "freigegeben", was zu einer dauerhaften Änderung führt und dann zukünftige lesenden Abfragen entsprechend bedient.

    Dies ist auf der Ebene der Business-Logik, nicht derer von Event Sourcing angesiedelt.Ein Beispiel aus der Buchhaltung:
    1. Mitarbeiter erzeugt bei einem Geschäftsessen Kosten
    2. Er reicht die Kosten ein
    3. Jemand prüft, ob das gerechtfertigt ist
    4. Die Kosten werden gebucht.
    5. Die Buchung fließt in den Geschäftsbericht ein

    (Nur um sicherzugehen, dass der springende Punkt ankommt: Das Buchen entspricht der Persistierung und kann nicht so ohne weiteres rückgängig gemacht werden.)

    Nur die Punkte 4 und 5 haben mit Event-Sourcing zu tun. Der Rest ist Busines-Logik und hat damit nichts zu tun. Analog zu Deinem Beispiel würde es hingegen so aussehen:

    1. Mitarbeiter erzeugt bei einem Geschäftsessen Kosten
    2. Er reicht die Kosten ein
    3. Die Kosten werden gebucht.
    4. Die Buchung fließt in den Geschäftsbericht ein
    5. Jemand prüft, ob das gerechtfertigt ist

    Stattdessen ist es eher so, dass Du bei Deinem Beispiel folgenden Use-Case hast:
    1. User tut etwas
    2. Validierung
    3. Persistiere UserDidSomething
    4. Ändere ggf. das Read-Model
    5. Admin schaltet die Aktion frei
    6. Validierung
    7. Persistiere SomethingConfirmed
    8. Ändere ggf. das Read-Model

    An Punkt 3 und 7 sieht man, dass es sich hier um zwei getrennte Vorgänge und nicht um einen handelt.


    Aus meiner Erfahrung heraus würde ich auch erst mal nur mit einem kleinen Subset der Bounded Contextes beginnen, da Du garantiert auch hier auf Dinge stoßen wirst, die Dir umso doller auf die Füße fallen, je größer Deine Anfangsbasis ist. Immer an das MVP denken und nicht in Schönheit sterben :-)

    Sehr unterhaltsamer Blog übrigens!

    AntwortenLöschen
  2. Danke für den ausführlichen Kommentar!
    Muss darüber gut nachdenken 😎

    AntwortenLöschen