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 Boot, HATEOAS, Responsive Web Design und Mobile First, JSON Web Tokens, Roca-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:
- Ich fange zum wiederholten Mal ein neues Git-Repository an: Oregami-DDD oder so ähnlich.
- Wir identifizieren unseren ersten Bounded Context, den wir nach den Regeln des DDD umsetzen wollen.
- Die notwendigen Entitäten und Value Objects werden erstellt.
- Use Cases werden erarbeitet und gemäß den Prinzipien einer hexagonalen Architektur umgesetzt.
- Änderungen am Zustand der Anwendung werden dabei über Commands und Events verarbeitet.
- Die Events müssen in der Anwendung einsehbar sein, denn darauf muss ja irgendwann die "Moderation von Dateneingaben" erfolgen.
- 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:
- Vaughn Vernon: Effective Aggregate Design (PDFs: Part 1, Part 2, Part 3)
- Paul Rayner: Aggregates & Entities in Domain-Driven Design
- Alistair Cockburn: Hexagonal Architecture
- Slideset by Jeppe Cramon: Agile, Architecture, DDD and CQRS
- Martin Fowler: Event Sourcing
- Torben Fojuth: Domain-Driven Design im Hexagon (German)
- Kyle Cordes: Task Based User Interfaces
- Mehdi Khalili: ORM anti-patterns - Part 5: Generic update methods