Mehr Adaptivität durch DDD – Ein Überblick
Domain Driven Design umfasst ein sehr weites Feld. Es ist kaum möglich, alle Feinheiten und Details in einem Artikel zusammenzufassen. Deshalb wollen wir hier primär einige Grundgedanken und zentrale Ideen skizzieren, die einen Eindruck vermitteln, wie DDD zur Verbesserung von Adaptivitätseigenschaften beitragen kann.
Zum allgemeinen Verständnis von DDD
- Der wesentliche Grundgedanke bei DDD steckt bereits im Namen: „Domain Driven Design“ könnte man übersetzen als „Durch die Fachlichkeit getriebenes Design“. Die Strukturen, Konzepte und Zusammenhänge der Fachdomäne wollen wir passend abbilden. Sie geben uns somit gewissermaßen „natürliche“ Leitlinien und Orientierungspunkte mit, an denen wir Designentscheidungen ausrichten können und in der Folge die Strukturen in unserer Software ableiten.
- DDD bezieht sich im Kern immer auf ein Model getriebenes Vorgehen. Das bedeutet, wir versuchen Fachkonzepte zu modellieren, um so mehr Klarheit und Einsichten zu erlangen und sie dann passend zu implementieren. Dabei ist wichtig zu verstehen, dass die Modellierung und Implementierung idealerweise durchaus iterativ angepasst und verfeinert wird (kein Big UpFront Design „BUF“). Ebenso sollen die Modelle helfen, bei schwierigen Entscheidungen alternative Entwürfe zu vergleichen.
- Unserer Meinung nach ist DDD als Werkzeugkasten zu verstehen, der methodische Ansätze und Techniken bietet, bei welchen man sich bedienen kann, wenn man das jeweilige Werkzeug für passend hält.
DDD legt viel Wert auf klare und unmissverständliche Beschreibungen der Fachkonzepte. Das Ziel ist es, eine speziell auf den jeweiligen Kontext (Bounded Context) bezogene Sprache zu etablieren, die sowohl von Technikern als auch dem zugehörigen Fachbereich gesprochen und verstanden wird (Ubiquitous Language).
DDD Prinzipien für verbesserte Adaptivität
Es gibt zwei wiederkehrende Prinzipien, welche sich durch alle Werkzeuge von DDD ziehen und gleichzeitig die Adaptivitätseigenschaften in Software-Strukturen verbessern:
- Verständlichkeit: Bei DDD werden implizite Konzepte und Zusammenhänge möglichst explizit in den Modellen und dann auch im Code abgebildet. Wenn man Software anpassen oder erweitern möchte, dann ist die Grundlage dafür, dass man die Ausgangssituation und zugehörige Zusammenhänge möglichst klar erfasst hat. Dadurch lassen sich unerwünschte Seiteneffekte besser ausschließen und man weiß genauer, was alles anzupassen ist. Über die Verankerung der Ubiquitous Language in der Kommunikation bei den handelnden Personen und direkt im Code werden Übersetzungsverluste und Missverständnisse stark reduziert. Software-Entwicklung ist Teamarbeit. Der größte Aufwand entsteht nicht dadurch, dass man mehr Zeilen Code schreiben muss. Vielmehr ist die größte Hürde, dass die zugrunde liegenden Fachzusammenhänge über mehrere Köpfe hinweg klar erfasst und in Code transferiert werden müssen.
- Grenzen setzen: Seit jeher ist Modularisierung ein zentrales Mittel in der Software-Entwicklung, um Komplexität kontrollierbar zu machen. Grenzen klar zu definieren erlaubt uns Abhängigkeiten zwischen Modulen klar zu definieren (Schnittstellen) und das Innere im Rahmen dieser Abhängigkeiten als „Black Box“ zu betrachten (Kapselung). So lassen sich die betroffenen Bereiche von Änderungen, Anpassungen und Erweiterungen schneller identifizieren und man muss sich nicht immer mit allen Details auseinandersetzen, wenn man die entsprechenden Dinge umsetzen will. Darüber hinaus werden auch hierüber unerwünschte Nebeneffekte besser unterbunden und vieles mehr. DDD bietet Methoden und Patterns, um Grenzen besser zu erkennen und sie zu klarer definieren. Die Patterns beinhalten Optionen und Vorschläge, wie sie besser in der Architektur und im Code umgesetzt werden können. Die Gestaltung von Abhängigkeiten ist von zentraler Bedeutung für gute Adaptivitätseigenschaften in Software-Lösungen. Das vielfach im DDD Kontext erwähnte Anti-Pattern „Big Ball of Mud“ erläutert auch warum. DDD hilft auf unterschiedlichen Ebenen des Designs (strategisches und taktisches Design) beim passenden Schnitt von Modulen und Komponenten und deren Grenzen.
Unterstützung für adaptives Design in allen Bereichen
Domain Driven Design lässt sich in drei große Bereiche ordnen:
- Domain Discovery: Grundlage für eine zielgerichtete Implementierung ist, dass die Problemstellung und die Zusammenhänge in einer Domäne gut verstanden werden. DDD bietet hierzu bekannte kollaborative methodische Ansätze, wie beispielsweise „Event Storming“ oder „Domain Story Telling“. Dieser übergreifende Wissensaufbau bildet ebenso die Grundlagen für die Verständlichkeit im Domänen getriebenen Design.
- Strategic Design: Eine Domäne muss im Design zerlegt werden. Das betrifft die oben besagte Grenzdefinition auf oberer Ebene. In diesem Bereich hat DDD im Zuge des Microservice Hypes wieder viel Wahrnehmung erfahren. Insbesondere als Hilfestellung bei Fragen wie beispielsweise „Wie schneide ich meine Microservices idealerweise?“. Die dort nutzbaren Patterns bringen aber insbesondere auch unabhängig vom verfolgten Architekturstil wertvolle Einsichten. Das strategische Design unterstützt dabei einen fachlichen und an den Organisationsstrukturen orientierten Schnitt zu erreichen. Dadurch wird erleichtert, Abhängigkeiten explizit zu formulieren und bestenfalls zu minimieren und so in der Folge die Adaptivitätseigenschaften der Lösung zu verbessern.
- Tactical Design: Im Kern geht es bei taktischem DDD um einen guten objektorientierten Programmierstil. DDD bietet hierzu einige Patterns, die dabei helfen zentrale Konzepte voneinander abzugrenzen (z.B. Aggregates). Das ultimative Ziel dabei ist, die Konzepte der Domäne möglichst passend im Code abzubilden (Zitat Vaughn Vernon: „the code is the model, and the model is the code“, siehe hier). Das hebt die Code-Qualität, indem Code Duplikationen vermieden und Zusammenhänge tendenziell besser passend logisch gebündelt werden und einiges mehr. Zudem werden implizite Konzepte explizit ausgedrückt, was die Verständlichkeit verbessert. All dies steigert langfristig die Adaptivitätseigenschaften im Ergebnis auf Code-Ebene.
Ein letzter Rat
Zum Abschluss noch eine Sache. DDD in vollem Umfang ist nicht einfach und die Vielfalt an Aspekten und Patterns schreckt anfangs oft ab. Besonders, wenn man mit dieser Welt bisher noch nicht viel zu tun hatte. Aus unserer Erfahrung sollte man DDD eher begreifen wie eine Reise, auf die man sich einlassen muss. Man kann auch erst mal klein anfangen und sich ein ausgewähltes Werkzeug greifen, das vielleicht zu einer konkreten Problemstellung Lösungsansätze verspricht. Es hilft zum Einstieg auch erst einmal zu versuchen eine Problemstellung über ein Modell abzubilden und besser zu verstehen (ohne direkt alle Patterns versuchen perfekt einzusetzen). Man gelangt oft auch so zu wertvollen Einsichten, bevor man an die Implementierung geht.
Bounded Context: Der Bounded Context definiert den Einsatzbereich eines Domänenmodells. Es umfasst die Geschäftslogik für eine bestimmte Fachlichkeit. Das Domänenmodell des Bounded Context für einen „Online Shop“ ist in der Abbildung oben dargestellt.
Aggregate: Ein Aggregat ist eine Gruppe von Domänenobjekten, die als eine Einheit behandelt werden. Ein Beispiel dafür ist eine Bestellung (siehe „Order“ im obigen Domänenmodell) und ihre Einzelposten („OrderItem“). Diese sind separate Objekte, aber es ist sinnvoll, die Bestellung (zusammen mit ihren Einzelposten) als ein einziges Aggregat zu behandeln.
Bei einem Aggregat ist eines seiner Komponentenobjekte die Wurzel des Aggregats (AggregateRoot). Alle Verweise von außerhalb des Aggregats sollten nur auf die Aggregatwurzel gehen. Die Wurzel kann somit die Integrität des Aggregats als Ganzes gewährleisten.
Aggregate sind das grundlegende Element für den Zugriff auf die Persistenz über Repositories. Über Repositories werden zur Wahrung der Konsistenz nur ganze Aggregate geladen oder gespeichert. Transaktionen sollten keine Aggregatgrenzen überschreiten.
Anaemic Domain Model (Anti Pattern): Martin Fowler schreibt hierzu:
„Das grundlegende Symptom eines anämischen Domänenmodells ist, dass es auf den ersten Blick wie ein echtes Modell aussieht. Es gibt Objekte, von denen viele nach den Substantiven im Domänenraum benannt sind, und diese Objekte sind mit den reichhaltigen Beziehungen und Strukturen verbunden, die echte Domänenmodelle aufweisen.
Der Haken an der Sache ist, dass es kaum Verhalten für diese Objekte gibt, so dass sie kaum mehr als eine Ansammlung von Gettern und Settern sind. In der Tat sind diese Modelle oft mit Entwurfsregeln versehen, die besagen, dass man keine Domänenlogik in die Domänenobjekte einbauen soll. Stattdessen gibt es eine Reihe von Services, die die gesamte Domänenlogik erfassen, alle Berechnungen durchführen und die Modellobjekte mit den Ergebnissen aktualisieren. Diese Dienste leben oberhalb des Domänenmodells und verwenden das Domänenmodell für Daten.
Das Schlimme an diesem Muster ist, dass es dem Grundgedanken des objektorientierten Designs zuwiderläuft, nämlich Daten und Prozesse miteinander zu verbinden. Das anämische Domänenmodell ist in Wirklichkeit nur ein prozedurales Design, genau die Art von Dingen, die Objektfanatiker wie ich (und Eric [Evans]) seit unseren frühen Tagen in Smalltalk bekämpft haben. Noch schlimmer ist, dass viele Leute denken, dass anämische Objekte echte Objekte sind, und damit völlig verfehlen, worum es bei objektorientiertem Design überhaupt geht.“
Wenn man versucht, die Architektur eines Softwaresystems zu dokumentieren und feststellt, dass es schwierig ist, Teilsysteme oder Verantwortlichkeiten abzugrenzen, ist es möglich, dass das System keine erkennbare Architektur hat und somit ein Big Ball of Mud ist.
Änderungen und Erweiterungen sind in einem Big Ball of Mud oft nur sehr schwer und langsam umsetzbar. Es dauert durch die fehlende Struktur sehr lange alle Stellen zu finden, die für ein konsistentes Verhalten angepasst werden müssen. Die Fehlerquote durch unerwünschte Nebeneffekte ist tendenziell sehr hoch.