Domain-Driven Design – Taktische Modellierung: Teil 2
Building Blocks und die "fehlenden Patterns"

Taktisches Design bietet, wie im ersten Artikel dieser Serie geschildert, speziell für die Modellierung innerhalb eines Bounded Context eine Palette von Patterns, mit der Fachkonzepte über ein Zusammenspiel von Bausteinen modelliert und in der Folge auch implementiert werden können. Diese Bausteine unterstützen vor allem eine sinnvolle Kapselung nach Designprinzipien wie “hoher Kohäsion und niedriger Kopplung” und eine gute Aufteilung nach gewissen “typischen” Verantwortlichkeiten wird angestrebt. Dabei bildet, wie der Name “Domain-Driven Design” schon sagt, immer die Fachlichkeit (also die Abbildung der Fachdomäne) den Ausgangspunkt aller Überlegungen. Wir gehen im Rahmen dieses Artikels sowohl auf die klassischen DDD Building Blocks als auch auf einige ergänzende Patterns ("die fehlenden Patterns") ein.
Domain-Driven Design: Objektorientierung
Im Kern geht es bei der Implementierung im Rahmen des taktischen Designs erstmal “nur” um echt objektorientierte Programmierung. Um zu verdeutlichen, was mit einem “echt” objektorientierten Domänenmodell gemeint ist, bedient man sich oft der Gegendarstellung mit Hilfe eines Anti-Patterns. Das wohl bekannteste Anti-Pattern in diesem Zusammenhang beschreibt, was insbesondere bei Business-Software häufig zu beobachten, aber im Sinne des taktischen Designs nicht erwünscht ist: das von Martin Fowler so treffend benannte “Anemic Domain Model”, das sogenannte “blutleere” Domänenmodell [1].
Fowler beschreibt blutleere Domänenobjekte als Behälter von Werten, die lediglich mit Gettern und Settern versehen sind. Diese Objekte bilden dann kaum Verhalten ab und widersprechen der fundamentalen Idee der Objektorientierung, Daten und deren Verarbeitung zusammenzubringen.
Abb. 1 verdeutlicht beispielhaft, wie solche “blutleeren” Objekte aussehen können. Das Beispiel orientiert sich zwar hinsichtlich der Benennung von Klassen und Attributen an der Ubiquitous Language, aber die eigentliche Implementierung benötigt prozedurale Strukturen, in denen die Geschäftslogik nahezu ausschließlich in Services vorzufinden ist, während die blutleeren Domänenobjekte nur reine Daten-Container sind.
Aber Achtung: Fowler beschreibt ebenso, dass es “einfachere” Einsatzfälle gibt, in welchen diese Art der Strukturierung der Geschäftslogik in Services (als sogenanntes “Transaction Script”) mit ergänzenden “dummen” Daten-Containern durchaus sinnvoll ist. Dies ist beispielsweise überall da sinnvoll, wo sich die Geschäftslogik in einem einzigen linearen Fluss von Aktionen abbilden lässt (z. B. ETL-Prozesse [2], bei denen Daten aus mehreren, ggf. unterschiedlich strukturierten Datenquellen in einer Zieldatenbank vereinigt werden). Man darf also nicht zu vorschnell den Schluss ziehen, dass ein anämisch anmutendes Modell immer eine überaus schlechte Lösung ist. Es kommt darauf an!
Domain-Driven Design: Das Aggregate als zentraler Building Block
In besonders komplexen Domänen mit einer Vielzahl an Interaktionen, Zuständen und Einflussfaktoren, die in der Domäne abgebildet werden müssen, führen blutleere Domänenmodelle oft zu schlecht nachvollziehbarem Code, weil sich das Verhalten tendenziell auf mehrere Services verteilt. Es werden in den Geschäftsobjekten häufig viele Eigenschaften mit Settern offengelegt (Stichwort: “mutable state”), was generell zu fragilem Code führt, denn der jeweilige Aufrufer muss in diesem Fall wissen, wie man bei einem blutleeren Modell in valide und konsistente Gesamtzustände kommt. Das führt tendenziell auch zu Code-Wiederholungen.
Neben der schlechteren Lesbarkeit aufgrund der verteilten Logik ergibt sich dadurch häufig über die Zeit hinweg im Rahmen der Evolution des Codes eine schlechtere Konsistenz bzgl. des Programmverhaltens bei Zustandsübergängen. Da sich beispielsweise die Prüfungen für Zustandsübergänge auf mehrere Services verteilen, kann es schneller passieren, dass bei Anpassungen gewisse Teile der Logik vergessen werden und ggf. in der Gesamtbetrachtung dann inkonsistent abgebildet sind, was wiederum dazu führt, dass sich häufiger Fehler in die Software einschleichen.
Das hat auch weitere negative Auswirkungen. Beispielsweise verschlechtert sich auch die Testbarkeit des Codes, da sinnvolle Tests zur Businesslogik in der Folge auf mehrere Stellen verteilt werden müssen. Generell wird hierbei oft eine Vielzahl an objektorientierten Entwurfsprinzipien (oft unbewusst) gebrochen, z. B.:
- “Tell, don’t ask” [2]: Anstelle Objekte nach ihren Daten zu fragen und dann auf diesen Daten zu operieren, sollte man besser dem Objekt direkt “sagen”, was es tun soll.
- “Law of Demeter” [3]: Objekte sollten nur mit Objekten in ihrer unmittelbaren Umgebung kommunizieren.
- “Single Responsibility Principle” [4]: Bringe die Dinge zusammen, die sich aus denselben Gründen ändern. Trenne die Dinge, die sich aus unterschiedlichen Gründen ändern.
Also, wie kann man es besser machen? Und es stellt sich die Frage: Geht das nicht auch ein bisschen einfacher, als ständig zu versuchen, den Code entsprechend der Einhaltung abstrakter Entwurfsprinzipien zu beurteilen? DDD sieht dafür die Unterteilung der Domänenobjekte in Entities und ValueObjects vor. Darauf aufbauend bilden dann Aggregates zentrale Einheiten für die Abbildung von Geschäftsobjekten.
ValueObjects (dt. Wertobjekte)
“Einige Objekte beschreiben oder berechnen eine Eigenschaft eines Dings. Viele Objekte haben keine konzeptionelle Identität. Die Identität von Entities zu verfolgen ist unerlässlich, aber anderen Objekten eine Identität zuzuweisen, kann die Systemleistung beeinträchtigen, mehr Analysetätigkeiten erzwingen und das Modell durcheinander bringen, weil alle Objekte gleich aussehen. Softwaredesign ist ein ständiger Kampf gegen Komplexität. Wir müssen Unterscheidungen treffen, damit eine besondere Behandlung nur dann erfolgt, wenn sie notwendig ist.
Wenn wir diese Kategorie von Objekten jedoch nur als die Abwesenheit von Identität verstehen, haben wir nicht viel zu unserem Werkzeugkasten oder Vokabular hinzugefügt. Tatsächlich haben diese Objekte eigene Eigenschaften und eine eigene Bedeutung für das Modell. Dies sind Objekte, die Dinge beschreiben.
Daher: Wenn es dir nur um die Attribute und die Logik eines Elements des Modells geht, klassifiziere es als Value Object. Lass es die Bedeutung der Attribute ausdrücken, die es enthält, und gib ihm die zugehörige Funktionalität. Behandle das Value Object als unveränderlich. Mache alle Operationen zu Side-effect-free Functions, die nicht von veränderlichem Zustand abhängig sind. Gib dem Value Object keine Identität und vermeide die Komplexität, die zur Wartung von Entities notwendig ist." (Auszug aus der dt. Übersetzung von Eric Evans DDD-Referenz)
Entities (dt. Entitäten)
“Viele Objekte stellen eine Kontinuität und Identität dar und durchlaufen einen Lebenszyklus, obwohl sich ihre Attribute ändern können. Einige Objekte sind nicht in erster Linie über ihre Attribute definiert. Sie stellen eine Identität dar, welche die Zeit und oft verschiedene Darstellungen durchläuft. Manchmal stimmt ein solches Objekt mit einem anderen Objekt überein, obwohl die Attribute unterschiedlich sind. Oder ein Objekt muss von anderen Objekten unterschieden werden, selbst wenn sie die gleichen Attribute haben können. Eine falsche Identität kann zu Datenkorruption führen.
Daher: Wenn ein Objekt durch seine Identität und nicht durch seine Attribute unterschieden wird, dann mache das zu einem wichtigen Teil seiner Definition im Modell. Halte die Klassendefinition einfach und konzentriere dich auf die Kontinuität des Lebenszyklus und die Identität. Definiere eine Möglichkeit zur Unterscheidung jedes Objekts unabhängig von seiner Form oder Geschichte. Achte auf Anforderungen, die einen Abgleich von Objekten über Attribute erfordern. Definiere eine Operation, die garantiert ein eindeutiges Ergebnis für jedes Objekt liefert, möglicherweise durch Ergänzen eines garantiert eindeutigen Attributes. Dieses Attribut zur Identifikation kann von außen kommen, oder es kann ein beliebiger Identifikator sein, der vom und für das System erstellt wird, muss aber zu den Identitätsunterschieden im Modell passen. Das Modell muss definieren, was es bedeutet, das Gleiche zu sein. (auch bekannt als Reference Objects, dt.: Referenzobjekte)”. (Auszug aus der dt. Übersetzung von Eric Evans DDD-Referenz)
Ein Aggregate ist zunächst einmal eine Gruppe von Entities und ValueOjects (s. Infoboxen), die als eine logische Einheit behandelt werden sollen. Jedes Aggregate verfügt dabei über eine spezielle Entity (die Wurzel-Entity bzw. Aggregate Root), über die das Aggregate von außen angesprochen wird. Die einzelnen Objekte im Aggregate sind dabei nicht nur Datencontainer, sondern enthalten auch Methoden, die direkt mit den Daten des Aggregates arbeiten. Dabei werden bestimmte Geschäftsregeln, insbesondere Invarianten, im Rahmen dieser Methoden für entsprechende Interaktion mit der “Außenwelt” innerhalb des Aggregates durchgesetzt. Invarianten sind dabei besonders hilfreich, weil sie Regeln beschreiben, die immer gelten müssen und somit eine gewisse Konstanz und Verlässlichkeit in das Design bringen. Aggregates bilden außerdem eine transaktionale Einheit ab. Das bedeutet, dass konsistente Änderungen an einem Aggregate idealerweise immer innerhalb einer einzigen Transaktion stattfinden sollten.
Die Nutzung von Aggregates ist das zentrale Konzept, mit dem eine gute fachliche Separation of Concerns innerhalb eines BoundedContexts in den Code gebracht werden kann. Für die Interaktion mit einem Aggregate gibt es ergänzende Strukturen wie DomainServices oder DomainEvents (s. u.), die sich auf die transaktionalen Übergänge ausrichten und damit die zentralen Bausteine unseres Designs ergänzen.
Oben ist nun die Implementierung einer “Buchung” zu sehen, die sich mehr nach den DDD-Empfehlungen zu einem reichhaltigen Design richtet. Es werden typisierte ValueObjects wie “Preis” und “Zimmerkategorie” eingesetzt, um Eigenschaften der “Buchung” zu beschreiben. Über den Konstruktor werden grundsätzliche Regeln und Invarianten geprüft, die bei einer “Buchung” zu jeder Zeit eingehalten werden müssen. Darüber hinaus werden zentrale Zustandsübergänge wie die “Stornierung” einer “Buchung” oder die zugrunde liegenden Operationen für den “Check-in” oder “Check-out” direkt in dieser Klasse abgebildet.
Haltung: Implizites explizit machen
Software-Entwicklung ist Teamarbeit, deshalb gibt es nichts Schlimmeres als Code, den nur der ursprüngliche Autor versteht. Oder implizite Regeln zum Verhalten eines Geschäftsobjekts, die nur im Kopf des ursprünglichen Entwicklers bedacht wurden, aber nicht explizit im Code festgehalten sind. Bei der Implementierung von Aggregates wird das Verhalten der Objekte idealerweise immer explizit im Code abgebildet. Über explizite Geschäftsregeln kann nachvollziehbar formuliert und entsprechend geprüft werden, welche Zustandsübergänge zulässig sind. Das AggregateRoot bildet die Schnittstelle eines Aggregates nach außen. Über Methoden des Aggregate Roots werden Zustandsübergänge innerhalb des Aggregates ausgelöst. Diese grundlegende Struktur macht den Code lesbarer und verständlicher.
Explizite Implementierung von Domänenmodellen und KI
An dieser Stelle noch eine unbewiesene, aber aus unserer Sicht schlüssige These bzgl. des Zusammenspiels KI-Code-Assistenten und taktischem Design: Auf LLMs basierende Code-Assistenten arbeiten effizienter und korrekter, wenn der bestehende, durch ein Assistenzsystem anzupassende Code Geschäftsregeln explizit abbildet [5]. Ansonsten muss der Entwickler diese Information immer (potentiell ungenau) über entsprechende Prompts an das Assistenzsystem mitgeben.
Repository, DomainService, DomainEvent und Factories
Bei taktischem Design lassen sich die Bedeutung und Verantwortlichkeiten von Building Blocks wie Repository, DomainService, DomainEvent und Factory am (zugegebenermaßen auf den ersten Blick recht komplizierten) Konzept des Aggregates ableiten:
- Ein Repository abstrahiert im Wesentlichen den Persistenz-Zugriff für Aggregates. Repositories verbergen die technischen Details, wie und wo Aggregates gespeichert werden.
- Ein DomainService wird dann sinnvoll eingesetzt, wenn eine bestimmte Geschäftslogik nicht einem spezifischen Aggregate zugeordnet werden kann, aber dennoch ein zentraler Bestandteil der Domäne ist. DomainServices sind für die Umsetzung von geschäftsrelevanten Operationen zuständig, die mehrere Aggregates betreffen oder eine externe Zusammenarbeit weiterer Services erfordern.
- DomainEvents dienen in erster Linie dazu, Ereignisse innerhalb der Domäne zu kommunizieren, wenn z. B. bestimmte Geschäftsregeln oder Zustandsänderungen eintreten. Sie modellieren Ereignisse, die bedeutend für die Domäne sind und ermöglichen es, verschiedene Teile des Systems lose gekoppelt zu halten. Dies gilt insbesondere für die Fälle, in denen ein Aggregate eine Änderung signalisiert, die von anderen Aggregates verarbeitet werden soll.
- Factories unterstützen dabei, die zentralisierte, konsistente und geschäftsregel-konforme Erstellung von komplexen Objektstrukturen, insbesondere Aggregates, an einem dedizierten Ort abzubilden.
Zusammenfassend kann man nun bis dahin sagen, dass die oben beschriebenen Konzepte dabei helfen, die Domänenlogik in einer Weise zu organisieren, die konsistent, modular und skalierbar bleibt.
Domain-Driven Design: Die fehlenden Patterns
Setzt man taktisches Design in der Praxis ein, gibt es immer wieder mal auch Situationen, für die sich mit der Theorie auf den ersten Blick keine Lösung finden lässt. Einerseits ist das so, weil die Ideen des taktischen Designs durchaus mit einer hohen Einstiegshürde einhergehen. Die entsprechenden Konzepte an sich sind anfangs oft schwer zu greifen und einzuordnen. Auf der anderen Seite gibt es viele Aspekte, die in den einschlägigen Büchern manchmal zwar genannt, aber so beschrieben werden, dass man sie erstmal als unwesentliche Details einordnet. In bestimmten Fällen machen diese Details dann aber den Unterschied aus. Zudem hat sich die Theorie über andere ergänzende Konzepte (bspw. Ports & Adapters oder CQRS) weiterentwickelt und das Zusammenspiel der Konzepte ist nicht immer leicht zugänglich, weil unterschiedliche Autoren ähnliche Konzepte benennen, die dann aber nicht vollumfänglich voneinander abgegrenzt oder im Zusammenspiel erläutert sind.
Im Folgenden beschreiben wir nun mehrere Fragestellungen, über die wir bei der Anwendung von taktischem Design häufiger gestolpert sind. Dies soll als Hilfestellung dienen, wenn man sich ähnliche Fragen stellt. Aus Sicht der häufig zitierten Bücher von Eric Evans und Vaughn Vernon könnte man diese Konzepte als “fehlende Patterns” bezeichnen. Sie werden oft in der ersten Wahrnehmung eines Lesers übersehen oder falsch oder auch überkomplex interpretiert. Oder sie waren zum Zeitpunkt der ursprünglichen Beschreibung so einfach noch nicht bekannt.
Domain-Driven Design: Unterschiedliche Arten von Services
Insbesondere wenn DDD mit den im vorigen Artikel beschriebenen Domänen isolierenden Ansätzen kombiniert wird, kann es hilfreich sein, die im Domain Core verwendeten Servicestrukturen etwas genauer zu unterteilen. Zur Erinnerung, mit Domänenisolierung meinen wir einen strukturellen Aufbau der Applikation in der Form, dass die Domänenlogik im Kern abgebildet wird und sich die technische Infrastruktur gewissermaßen in Ringen um diesen Domain Core herum anordnet. Der Aspekt der Isolation ist wichtig, um zu verdeutlichen, dass wir die Geschäftslogik im Domain Core möglichst unabhängig von den Einflüssen der technischen Infrastruktur außerhalb halten wollen. Erreicht wird dies durch den Einsatz dedizierter Übergänge in den Domain Core hinein oder aus diesem heraus.
Ein bekanntes konkretes Beispiel für einen Übergang aus dem Domain Core heraus ist hierbei das Repository, welches den Persistenz-Zugriff für Aggregates abstrahiert. Aus Sicht der Domänenlogik ist hinsichtlich eines Repositories im Wesentlichen wichtig, dass es als Schnittstelle in Richtung Persistenz-Infrastruktur dient. Im Domain Core gibt es lediglich ein Interface, welches diese Schnittstelle beschreibt, das Repository Interface. Auf Infrastrukturebene wird mit Hilfe der verwendeten konkreten Persistenz-Technologie dieses Interface dann implementiert. Über das Repository Interface wird programmiertechnisch die Unabhängigkeit der Core Domain von technischen Belangen der Infrastruktur erreicht (das entspricht dem bekannten Prinzip der “Dependency Inversion”). Es gibt weitere Stellen, an denen in gleicher Art und Weise “Dependency Inversion” angewendet werden kann, um Domänenkonzepte von technischen Implementierungsdetails zu entkoppeln. Auf diese wird im nachfolgenden Abschnitt eingegangen.
Domain-Driven Design: QueryHandlers und OutboundServices
Wir haben, wie bereits angedeutet, die Erfahrung gemacht, dass es sinnvoll ist, weitere Arten von Services zu unterscheiden, die potentiell direkt aus der Domänenlogik aufgerufen werden, aber deren Implementierung prinzipiell starke Abhängigkeiten zur Infrastruktur aufweist. Wir wollen gemäß dem Prinzip der “Domain Isolation” Fachlichkeit und Technik voneinander trennen. Für diese Fälle führen wir weitere Service-Klassifizierungen ein. Der eine Fall hierzu wäre, dass wir einen Service benötigen, der sogenannte ReadModels bereitstellt. ReadModels bündeln Daten für spezifische Lesebelange, in welchen Aggregates nicht geeignet sind. Wir werden das Konzept weiter unten etwas genauer erläutern. Hierzu wäre die Empfehlung, analog zum Repository für Aggregates einen sogenannten Query Handler für die Bereitstellung bzw. den Zugriff auf entsprechende ReadModels zu definieren. Die Implementierung kann nun entweder ein Datenbankzugriff auf eine View sein oder es wäre möglich, dass ein ReadModel aus einer anderen spezifischen Datenhaltung, bspw. einer Search Engine wie ElasticSearch, bereitgestellt wird.
Wenn man das Konzept der “Domain Isolation” weiterspinnt, gibt es unter Umständen aus der Domäne heraus auch synchrone Aufrufe in Richtung anderer externer Systeme oder eben manchmal auch den Bedarf, synchron aus der Geschäftslogik heraus eher technische Dienste anzustoßen, die einen starken Infrastrukturbezug haben (z. B. E-Mail-Benachrichtigungen). Diese Art von Services, die bewusst aus der Domäne heraus in den Bereich der technischen Infrastruktur reichen, aber ggf. aus der Domäne heraus getriggert werden, bezeichnen wir als OutboundServices. Sie definieren Interfaces, die in der Domänenlogik verankert sind, aber deren Implementierungen eine hohe Abhängigkeit zur verwendeten technischen Infrastruktur aufweisen und keine eigentliche Geschäftslogik im Sinne eines fachlichen Ablaufs oder Zustandsübergangs abbilden.
Domain-Driven Design: ApplicationServices
Darüber hinaus gibt es weitere Arten von Services, die nicht direkt zum Domain Core gehören, die sogenannten ApplicationServices. Diese tauchen zwar auch schon in den ursprünglichen Beschreibungen von Evans auf, aber die Unterscheidung von DomainServices und ApplicationServices ist manchmal nicht ganz eingängig, vor allem, weil in den Pattern-Kurzbeschreibungen zu DDD hauptsächlich immer auf DomainServices eingegangen wird. ApplicationServices werden im Rahmen der ursprünglichen Ports-&-Adapters-Beschreibung von Alistair Cockburn auch als Driver bezeichnet [6]. Diese bilden den Ort, an welchem alle Application Use Cases aufschlagen sollten. Sie steuern typischerweise das Transaktionshandling und verzweigen bei Bedarf in die Domäne hinab oder orchestrieren die Logik in Richtung von rein technischen Services, die unter Umständen eben auch benötigt werden, um Application Use Cases abzubilden. Ein Beispiel eines “rein technischen” Application Use Cases wäre der Download von Daten in einem spezifischen Dateiformat. Hauptsächlich aber stellen ApplicationServices den Use Case getriebenen Einstiegspunkt in die Domäne dar. Entlang der verketteten Methodenaufrufe aus den ApplicationServices heraus lässt sich somit die Abbildung eines fachlichen Ablaufs im Domain Core gut nachvollziehen.
Domain-Driven Design: Domain Commands
Kommen wir zu einer vermeintlichen Kleinigkeit. Evans beschreibt in seinem ursprünglichen Blauen Buch [7] zwar auch schon, dass die in der Domäne abgebildeten Operationen grob in Commands und Queries unterteilt werden können (s. den Abschnitt unter “Side-Effect-Free Functions”). Aber daraus geht nicht direkt hervor, dass die Abbildung von Commands einen deutlich größeren Effekt auf die Gestaltung der Aggregates hat als die Abbildung der Queries. Zustandsübergänge werden durch Commands ausgelöst. Die Sicherstellung der Konsistenz im Rahmen dieser Zustandsübergänge ist ein zentrales Anliegen beim Schnitt eines Aggregates. Diesen Zusammenhang werden wir im abschließenden Artikel dieser Serie im Rahmen des Aggregate Designs noch etwas genauer beleuchten. Gerade in der Unterscheidung und in der expliziten Abbildung von Lese- und Schreibbelangen liegen Antworten zu vielen Fragen, die bei taktischem DDD öfter auftreten. Dies beleuchten wir bzgl. spezifischer Lesebelange auch etwas weiter unten mit den ReadModels.
Bzgl. der Schreibbelange kann es unserer Erfahrung nach schon helfen, Commands konkret nicht nur als Methoden in Services und Aggregates abzubilden, sondern die Parameter in einer expliziten zugehörigen “Domain Command”-Struktur abzubilden. Im Gegensatz zum GOF-Command Pattern[8] soll ein entsprechendes Domain-Command-Objekt nicht das Verhalten implementieren, sondern lediglich die für die Ausführung des Domain Commands benötigten Parameter in einer Art “Parameter-Value-Object” bündeln. Wenn man die zugehörige Klasse auch noch in der “Ubiquitous Language” und in Befehlsform benennt, wird somit die fachliche Intention klar und man vermeidet nebenbei bei komplexeren Commands mit mehreren Parametern lange unverständliche Parameterlisten. Zudem kann man in dem entsprechenden Objekt auch Bedingungen an das gewünschte (im Sinne der Domänenlogik gültige) Parameter-Zusammenspiel als Geschäftsregel formulieren.
Oft werden diese DomainCommands von einem ApplicationService (s. u.) in ein Aggregate hinein gereicht, um dort die eigentliche Businesslogik auszuführen. Dieser Zusammenhang wird dann auch auf Typebene im Design verdeutlicht und zeigt, an welchen Stellen der Command eine entsprechende Weiterreichung erfährt. Dies wird durch das Klassendiagramm in Abb. 4 verdeutlicht.
Im Szenario einer Hotel-Management-Software müssen Check-in-Vorgänge abgebildet werden. Anhand dieses Use Cases können wir den Fluss des DomainCommand “CheckeEin” durch die Domänenlogik verfolgen. Der DomainCommand schlägt bei den “BuchungUseCases” erstmals auf. Dort wird er in einer entsprechenden Methode “handle” verarbeitet. Hierüber wird der DomainCommand dann in den zugehörigen DomainService “CheckIn” weitergeleitet. Dieser DomainService orchestriert die Geschäftslogik über die Aggregates “Buchung” und “Zimmer” hinweg. Das Aggregate “Buchung” muss jedenfalls als “eingecheckt” markiert werden, hierzu werden die über den DomainComand “CheckeEin” übermittelten Daten verwendet, die entsprechende Zimmernummer wird der Buchung zugeordnet und die “geplante Anzahl Nächte” wird vermerkt.
Domain-Driven Design: ReadModels
Genauso wie die explizite Abbildung von DomainCommands sehen wir Vorteile bei der expliziten Abbildung von Lesebelangen in einem Bounded Context. Es gibt diverse Konstellationen im Rahmen von Lesezugriffen auf Domänenkonzepte, bei denen die gewählte Aggregate-Struktur nicht hilfreich und manchmal sogar hinderlich ist. Oftmals sind diese Lesebelange stark durch die Anforderungen der Informationsdarstellung in den UIs getrieben. Folgende Fälle kommen vor, bei welchen Lesezugriffe nicht adäquat allein über die Aggregate-Struktur abgebildet werden:
- Daten-Aggregation über mehrere Aggregates hinweg
- Anreicherung eines Aggregates mit Informationen aus einem anderen Bounded Context
- Einschränkung der Information eines Aggregates (um nicht alles in der UI/API offenzulegen)
Unsere Empfehlung hierzu ist, diese für Aggregate-Strukturen ungünstigen Lesebelange explizit durch ein anderes spezifisches Modell abzubilden, ein sogenanntes ReadModel. Der Einsatz von spezifischen ReadModels ist unter anderem auch über CQRS (Command-Query-Responsibility-Segregation [9]) bekannt. In dem zuvor beschriebenen Ansatz von taktischem Design würden die Aggregates als WriteModel agieren und man hätte für spezifische Lesebelange dedizierte ReadModels. CQRS in einer Vollausbaustufe würde bedeuten, dass generell alle Lesebelange über ein dediziertes ReadModel abgebildet werden. Man muss jedoch keinesfalls immer gleich das komplette System auf CQRS umstellen. CQRS kann Vorteile haben und harmoniert in Verbindung mit taktischem Design generell recht gut. Es zieht aber auch sehr hohe Implementierungsaufwände und unter Umständen Nachteile bei konsistenten Leseanforderungen durch ggf. in diesem Fall erzwungene "Eventual Consistency" nach sich. Deshalb kann man auch Zwischenschritte gehen und nur für bestimmte Leseanforderungen ein dediziertes ReadModel definieren und andere Lesebelange wie gehabt aus den Aggregates bedienen. Ebenso muss man zur technischen Bereitstellung eines ReadModels nicht immer von vornherein den Weg gehen, diesen in einer vom WriteModel getrennten Infrastruktur zu verwalten, wie das im Sinne von CQRS häufig dargestellt wird. Beim Einsatz einer althergebrachten relationalen Datenbank kann auch beispielsweise ein View oder eine Query, welche Daten aus mehreren Aggregaten zusammenführt, für einen ersten Schritt in Richtung ReadModel ausreichen.
Domain-Driven Design auf den diesjährigen IT-Tagen
Spannende Vorträge und Workshops zum Thema Domain-Driven Design erwarten Euch auch auf den IT-Tagen, der Jahreskonferenz von Informatik Aktuell. Die IT-Konferenz findet jedes Jahr im Dezember in Frankfurt statt – dieses Jahr vom 08.-11.12.
Wir nutzen nun wieder das Hotel-Szenario, um ein Beispiel hierfür zu geben. Insbesondere beim Erstellen einer neuen “Buchung” muss sichergestellt werden, dass noch genügend Kapazitäten im Hotel verfügbar sind. Hierzu müssen sowohl die bestehenden “Zimmer”-”Belegungen” für den angefragten Zeitraum berücksichtigt werden als auch sonstige noch nicht eingecheckte “Buchungen” in derselben Zimmerkategorie für den angefragten Zeitraum. D. h., in der zuvor dargestellten Struktur der Aggregates (s. o. beim Beispiel zum DomainCommand) muss diese Auswertung mehrere Aggregates (“Buchungen” und “Zimmer” mit ihren “Belegungen”) umfassen. Das entspricht genau einem möglichen Szenario, bei welchem der Einsatz eines dedizierten ReadModels ggf. hilfreich ist.
Man könnte diese Logik prinzipiell zwar auch über einzelne Abfragen in den jeweiligen Repositories abbilden und die Daten dann entsprechend zusammenführen bzw. auswerten, aber über ein explizites ReadModel ist diese komplexe Leseoperation schön gekapselt und ggf. wollen wir dies auch als Übersicht in der UI zur Verfügung stellen, was über das ReadModel auch vereinfacht wird.
In der UI benötigt der User nun beim Erstellen von neuen “Buchungen” eine Übersicht mit der aktuellen Zimmerauslastung in einem betrachteten Zeitraum. Dazu ist es hilfreich, wenn für die einzelnen Tage und je Zimmerkategorie dargestellt werden kann, wie viele freie Zimmer mit welcher Kapazität (z. B. Einzel- oder Doppelzimmer) vorhanden sind. Das ReadModel “Zimmerauslastung” bündelt all diese Informationen passend, obwohl die transaktionale Verarbeitung und Änderungen auf der Ebene unterschiedlicher Aggregates stattfinden. Über einen dedizierten Query Handler “Zimmerauslastungen” wird dieses ReadModel bereitgestellt. In welcher Form die Daten technisch für dieses ReadModel aufbereitet und zusammengeführt werden, ist für die fachlichen Zusammenhänge in der Domäne unerheblich und das kann je nach Lösungsansatz auf unterschiedliche Arten geschehen.
Domain-Driven Design: Fazit
Taktisches Design in voller Ausprägung kann eine hohe Einstiegshürde darstellen. Es erfordert ein hohes Verständnis für die zugehörigen Design- und Entwurfskonzepte. Aber in erster Linie muss man sich klar machen, dass es zunächst nur um die ganz grundlegende Anwendung objektorientierter Prinzipien geht. Implizites Wissen explizit im Code abzubilden, ist dabei eine ergänzende wichtige Haltung. Das beginnt bei der Namensgebung und geht weiter mit der Strukturierung des Codes, orientiert an den fachlichen Bedürfnissen. Der Gedanke, die Spielregeln der Domäne explizit und sprechend im Code festzuhalten und somit den Code verständlicher zu gestalten, hilft ganz grundsätzlich dabei, die langfristige Evolution des Codes mit mehreren Beteiligten effizient zu gestalten. Explizite DomainCommands verdeutlichen dabei die fachliche Intention von schreibenden Zugriffen, die in die Domäne hineinreichen. Spezifische Lesebedürfnisse können dabei immer, wie im Artikel dargestellt, unabhängig davon über explizite ReadModels aufgelöst werden, gerade wenn der Aggregate-Schnitt hierfür ungeeignet erscheint. Gerade in Verbindung mit “Domain Isolation” stellt sich oft die Frage, was ist Teil der Domänenlogik, was gehört zur Technik und wie bringe ich beides in verbindenden Use Cases zusammen, ohne die grundlegende Separation of Concerns von Fachlichkeit und Technik zu verletzen.
Eine gute Orientierung hierfür gibt die Betrachtung, dass ApplicationServices den Use Case orientierten Einstieg in die Fachlichkeit der Domäne bilden. Auf der anderen Seite gilt es, Services, deren Implementierung aus der Domäne heraus reicht, über Dependency Inversion von der Domänenlogik zu entkoppeln. Wir haben im Rahmen des Artikels gesehen, dass dieses Prinzip nicht nur für Repositories angewendet werden kann.
Im kommenden abschließenden Artikel dieser Serie werden wir gesondert auf das Thema Schnitt der Aggregates im Rahmen der Modellierung von schreibenden Abläufen in einem Bounded Context eingehen und die Frage nach der passenden Aggregate-Größe in Verbindung mit Konsistenzgarantien klären.
Im dritten Teil des Artikels werden spezifische Problemstellungen beim taktischen Design konkret anhand einer Beispieldomäne dargestellt.
- Martin Fowler: Anemic Domain Model
- Martin Fowler: Tell Dont Ask
- Wikipedia: Gesetz von Demeter
- Wikipedia: Single-responsibility principle
- Wikipedia: Large Language Model
- Alistair Cockburn: Hexagonal architecture
- Eric Evans: Domain-Driven Design: Tackling Complexity in the Heart of Software
- Wikipedia: Command pattern
- Wikipedia: Command-Query-Responsibility-Segregation