zur Übersicht

Domain-Driven Design

Lesedauer ca. 10 Minuten
10.01.2023

Die Veränderungen, die unsere Welt in den letzten Jahren durchlaufen hat, führen zu einer zunehmenden Komplexität in allen Bereichen. Damit steigen auch die Anforderungen an die Software, die wir nutzen. Wie kann der steigende Grad an komplexen Abläufen unserer Wirklichkeit in funktionsfähigen Softwaresystemen produktionsfördernd abgebildet werden? Der Ansatz des Domain-Driven Design hat sich für uns – der IT Sonix – bei großen und komplexen Softwareprojekten bewährt. Doch was ist Domain-Driven Design nun eigentlich? 2003 wurde der Begriff von Eric Evans mit seinem Buch Domain-Driven Design. Tackling Complexity in the Heart of Software etabliert. Im Grunde handelt es sich dabei um eine Sammlung von Werkzeugen und Techniken, die dabei helfen sollen, komplexe Software zu modellieren. Außerdem sind auch Denkweisen und Prinzipien zur Produktivitätssteigerung zentraler Bestandteil des Domain-Driven Designs. Erreicht wird diese Steigerung der Produktivität durch eine besonders detailgetreue Orientierung an der Fachlichkeit und Wirklichkeit der abzubildenden Geschäftslogik.

Entwickelnde und Fachpersonen arbeiten bei der Modellierung eng zusammen und erschaffen so eine Begriffswelt, die bis in den Code hineinreicht. Interessant bei diesem Ansatz ist, dass das Modell erst einmal nur auf Sprache angewiesen ist und vorerst gar nichts programmiert werden muss. In Theorie wie Praxis lässt sich das Domain-Driven Design in zwei große Bereiche aufteilen: Strategic Design und Tactical Design.

Während sich das Strategic Design mit der Architektur, also der Kontextabgrenzung und der Beziehung zwischen diesen beschäftigt, kümmert sich das Tactical Design um die Implementierung dieses erarbeiteten Models. Bevor wir jedoch im Folgenden tiefer in die Modellierung und den Code eindringen, wollen wir kurz die wichtigsten Begriffe des Strategic Designs erläutern.

Strategic Design

02_Strategic Für die fachgerechte Abbildung realer Geschäftsprozesse in der Software liefert das Strategic Design wertvolle Konzepte: Domain Model, Ubiquitous Language sowie Bounded Context.

Das Domain Model befasst sich, wie der Name bereits verrät, mit Domains. Domains bezeichnen hier die Wirklichkeit, also bspw. das gesamte Unternehmen bzw. die Fachlichkeit und nicht das Endprodukt, die Software. Diese Domains lassen sich wiederum in Subdomains untergliedern, wie bspw. Abteilungen, Teams oder Geschäftsprozesse. Natürlich ist das Domain Model nicht die Wirklichkeit, sondern eine Abstraktion dieser. Das geeignetste Mittel, Wirklichkeit in ein Model zu überführen, ist immer noch die Sprache, weshalb dieser im Domain-Driven Design auch besonders viel Aufmerksamkeit zukommt.

Der Schlüssel zum Erfolg liegt hier in der Ubiquitous Language. Ubiquitous bedeutet so viel wie allgegenwertig und genau das ist es auch, was Domain-Driven Design auszeichnet: Gemeinsam wird zwischen Fachpersonen und Entwickelnden eine Sprache erarbeitet, die von allen Beteiligten gesprochen wird und die von der Domain bis in den Code hinein konsistent ist. Begrenzt wird die Ubiquitous Language durch den Bounded Context.

Im Domain-Driven Design erhält ein Modell Bedeutung durch den Kontext, in dem es angewendet wird, und der Bounded Context bildet die Grenze eines Modells für einen bestimmten Zweck. Diese Abgrenzung ist besonders dann wichtig, wenn ein bestimmtes Konzept einer Domain durch verschiedene Modelle repräsentiert wird. So hat der Begriff „Kunde bspw. für den Support eine andere Bedeutung als für den Sales-Bereich. Obwohl es sich um denselben Kunden im Sinne der Domäne handelt, können hier somit verschiedene Bounded Contexts bestimmt werden, die jeweils ein zu den Teilaufgaben des Kontexts passenden Customer modellieren.

Bounded Contexts für eine bestimmte Domäne zu definieren stellt eine der größten Herausforderungen im Domain-Driven Design dar. Hierfür bieten sich kollaborative Methoden wie Event Storming oder Domain Story Telling an, bei denen Domänenexperten und Entwicklungsteams gemeinsam die jeweilige Domäne erforschen. Die daraus entstehenden Visualisierungen bieten eine erste Grundlage, möglichst unabhängige Teile der Domäne zu identifizieren und als Bounded Context zu definieren.

Die gefundenen Bounded Contexts sind bereits essentiell für die Teamorganisation, da ein Kontext immer von einem Team betreut wird. Ein Team kann dabei auch mehrere Kontexte betreuen, aber niemals umgekehrt mehrere Teams einen Kontext. Mittels Context Mapping können weiterhin Abhängigkeiten zwischen den Kontexten und damit auch Kommunikationsarten zwischen den Teams beschrieben werden.

Tactical Design

03_Tactical Sind das Domain Model, eine Ubiquitous Language sowie der Bounded Context einmal festgelegt, kann mit der Gestaltung und Implementierung spezifischer Softwarestrukturen und -Muster begonnen werden. Dies ist die Aufgabe des Tactical Design – mit seinen dafür vorgesehenen Bausteinen: Value Object, Entity, Aggregate, Repository, Service, Factory. Diese reichen aus, um jede beliebige Domain zu modellieren.

Das Value Object ist der kleinste Baustein einer Domain und wird allein durch seine Eigenschaften geprägt – es hat keine Identität und seine Eigenschaften sind unveränderbar, es kapselt primitiven Wert. Demnach können Value Objects, die dieselben Eigenschaften aufweisen, auch ausgetauscht werden. So wird im Domain-Driven Design bspw. für jede Postleitzahl ein eigenes Objekt generiert, etwas, das auch für andere komplexe Datentypen wie bspw. Geldbeträge genutzt werden kann. Jede Rechenoperation mit einem Value Object, das als Geldbetrag definiert ist, würde aus dem jeweiligen Ergebnis ein neues Value Object erstellen. Egal wie klein das Gegenstück in der Wirklichkeit sein mag, im Domain-Driven Design gibt es für alles ein Modell.

Einer der Vorteile dieser Methode ist, dass sich für alles ein Objekt erzeugen lässt und es sich dabei nicht nur um einen variablen Namen handelt, womit es nicht mit anderen (Strings) verwechselt werden kann. Der Wert des Objekts ist unveränderbar. Ein Value Object kann auch aus mehreren Value Objects bestehen, wie bspw. eine Adresse, die sich dann aus den unterschiedlichen Value Objects Straße, Postleitzahl und Land zusammensetzt.

Den nächsten wichtigen Baustein zum Modellieren bilden Entities. Im Gegensatz zu Value Objects verfügen diese über eine eindeutige Identität. Sie sind auch nicht von den ihnen zugewiesenen Eigenschaften abhängig – diese sind variabel und austauschbar und haben keinen Einfluss auf ihre Identität. Es sind die Entities, die das Herzstück des gesamten Domain-Driven Design und am Ende auch der Software bilden.

Wenn wir bei unserem Musterfall mit den Straßen und Postleitzahlen bleiben, so könnte es sich bspw. bei einer Einzelperson um ein Entity handeln. Die Einzelperson bleibt dieselbe Person, auch wenn sich ihre Adresse ändert bzw. ist sie durch ihre Identität auch von einer anderen Einzelperson mit demselben Namen unterscheidbar. Entities können aber auch Business-Funktionen abbilden. Wird von den Fachpersonen festgelegt, dass es für den Geschäftsprozess notwendig ist, dass etwas eine bestimmte Wissensfunktion erfüllen muss, so wird dafür meist ein eigenes Entity angelegt.

Im Domain-Driven Design werden Value Objects und Entities hierarchisch organisiert. Dafür braucht es Aggregates. Anstatt zusammenhängende Logiken im Code in verschiedene Services zu unterteilen, was bei falschen Aufrufen zu Inkonsistenzen führen kann, kapseln Aggregates sämtliche Geschäftslogiken zu einem bestimmten Konzept (Bounded Context) – sie definieren Invarianten. Dadurch werden nach außen nur Schnittstellen freigegeben, die immer einen konsistenten Zustand erzeugen.

Für Business-Funktionen, die etwas an mehreren Entities verändern sollen, wird immer das Aggregate verwendet. Hierfür wird an dem Aggregate Root, welcher allen innerhalb des Aggregate angeordneten Entities übersteht, etwas geändert und nicht an den einzelnen Entities selbst. Nur so kann sichergestellt werden, dass diese Funktionen bei allen Entities, wo es notwendig ist, auch durchgeführt werden. Die Operationen an den Aggregate Roots entsprechen Transaktionen ähnlich Ansätzen, die mit Datenbanken arbeiten.

Um eine Menge an Aggregate Roots repräsentieren zu können, benötigt es Repositories. Sie sind innerhalb der Domain völlig unabhängig von Persistenz-Technologie und meist nur eine Abstraktion dieser. Erst durch das Infrastructure Layer werden sie implementiert.

Für Fachlichkeiten, die nicht durch ein Entity oder Value Object abgebildet werden können, gibt es die Möglichkeit, diese als Service zu definieren. Auch wenn es sich dabei um eine Seltenheit handelt, da mit den oben genannten Bausteinen nahezu alles festgelegt werden kann, so können Services doch hilfreich sein, um gewisse Funktionalitäten, die bspw. nicht hierarchisch funktionieren oder gar nicht erst als Entität existieren, zu modellieren.

Die Factory ermöglicht es, die Verantwortung für das Erstellen komplexer Aggregates in ein eigenes Objekt zu verlagern.

Layered Architecture

04_Architecture Neben diesen rein domänenspezifischen Bausteinen ist es notwendig auch technische Aspekte zu implementieren. Diese sollen so organisiert werden, dass eine klare Trennung des Zwecks und lose Kopplung untereinander entsteht. Hierfür bietet das Domain-Driven Design ein Layered-Architecture-Modell. Dieses besteht aus: Infrastructure, Interfaces, Application und Domain.

Die Infrastructure unterstützt dabei alle anderen Layer und besteht oft aus Implementierungen von Abstraktionen der Domain. Beispiele dafür sind Datenbank-Repositories, Message Backend, REST Clients und externe Bibliotheken.

Als nächster Layer folgen Interfaces, die die Schnittstellen nach außen darstellen, wie bspw. REST-Endpunkte oder Message Handler. Sie bilden die oberste Schicht und dürfen daher auf alle darunterliegenden zugreifen. Kernpunkt des Domain-Driven Designs ist, dass die Domain keine Abhängigkeit zur Infrastructure entwickelt. Die Regel hierbei lautet, dass Zugriffe nur von oben nach unten erfolgen dürfen und nicht umgekehrt. Es sind jedoch auch Abkürzungen möglich. So kann es durchaus sein, dass das Interfaces Layer direkt auf die Domain zugreift, ohne den Umweg über den Application Layer. Häufig geschieht dies, wenn nur etwas ausgelesen werden soll, z. B. über ein REST-Interface. Der Interface Layer kümmert sich um die Serialisierung und Validierung. Primitive Daten wie bspw. eine einfache ID werden hier in ein Value Object umgewandelt, was gleichzeitig auch die Validierung der Eingabeparameter ist. Use Cases oder die Domain-Logik fallen nicht in seinen Zuständigkeitsbereich.

Für die Use Cases ist der Application Layer zuständig. Hier werden komplette Use Cases aufgeführt und koordiniert jedoch ohne Domain-Logik. Datenbanktransaktionen sowie Sicherheitsmechanismen wie bspw. User-Rechte finden meist auf dieser Ebene statt. Gerade bei User-Rechten kann es manchmal zu Schwierigkeiten kommen, wenn es um die Zuordnung des richtigen Layers geht. Wie sehr sind diese Teil der Domain oder nur technisches Hilfsmittel? Handelt es sich dabei um etwas rein Technisches, dann ist im Domain-Driven Design der Application Layer dafür vorgesehen. Wichtig dabei ist, dass dieser Layer unabhängig vom aufrufenden Interface – also der darüberliegenden – Schicht ist. Der Applications Layer sollte zur Ein- bzw. Ausgabe nur Objekte aus der Domain verwenden, da sie diese nur benutzt und nicht umwandelt. Es wird empfohlen, dass jede Methode dieser Schicht nur mit maximal einem Aggregate Root interagiert, um die Komplexität gering zu halten. Erfordert ein Use Case eine Interaktion mit mehreren Entities, so kann dies ein Zeichen für die Notwenigkeit eines weiteren Aggregate Roots sein, welches diese kapselt und die Konsistenz und Invarianten dieser sicherstellt.

Den letzten Layer – das Herz der Software – bildet die Domain selbst. Er enthält sämtliche Domain-Logik – gegebenenfalls auch nur als Abstraktion – und orientiert sich konsequent an die im Strategic Design definierte Ubiquitous Language.

Da das Domain Model keine Abhängigkeit zu allen anderen Schichten hat, kann Domain-Driven Design auch mit einem Hexagon- oder Onion Layer Model realisiert werden. Es wird immer von außen nach innen zugegriffen und jede Schicht hat ihre eigene Berechtigung.

Wie arbeiten wir?

Für die Umsetzung komplexer und großer Projekte ist für uns vor allem das Layered Architecture Model von großem Nutzen. Die klar getrennten Schichten helfen uns dabei, die Geschäftslogik vollkommen unabhängig von den eingesetzten Technologien zu denken. Ändert sich bspw. etwas in den Geschäftsregeln, so muss es nur im Domain Layer angepasst werden, ohne maßgebliche Auswirkungen auf die anderen Schichten. Zudem ermöglicht Domain-Driven Design auch anhand des Codes im Domain Layer die Geschäftslogik ohne großen Aufwand auszulesen, da diese eben ohne Technologien wie bspw. Datenbanken, REST-Schnittstellen etc. auskommt. Gleichzeitig können auch neue Technologien implementiert werden, ohne die Fachlichkeit zu beschädigen.

Da die Anwendung von Domain-Driven Design mit sehr großem Aufwand verbunden ist, empfiehlt sich dieser Ansatz vorrangig für sehr große und besonders komplexe Projekte. Hier kann diese Methode jedoch auf nahezu ganzer Linie punkten, da durch die isolierte Domain-Schicht und eben durch die konsistent in allen Schichten verwendete Sprache (Ubiquitous Language) – bis in den Code hinein – alles aus der Fachlichkeit wiedergefunden werden kann.