Deep Dive Domain-Driven Design und warum die Modularisierung von Softwaresystemen so wichtig ist
Ein Fachbeitrag von Markus Grothoff und Rubeswaran Mahalingam aus dem Segment Industry
Innovation, Kreativität und Flexibilität gehören zu den Erfolgsfaktoren für Unternehmen. Das Zusammenarbeiten über Abteilungsgrenzen hinweg wird als Kollaboration bezeichnet. Sowohl Kollaborationen als auch agile Arbeitsmodelle tragen dazu bei, die Unternehmungsstrukturen Stück für Stück zu verbessern.
Domain-Driven-Design (DDD) kann in Softwareprojekten eingesetzt werden, um komplexe Systemkomponenten mit unklaren Anforderungen effizienter umzusetzen. Dazu müssen sich Teams funktionsübergreifend als Generalist einen Gesamtüberblick verschaffen und sich zudem als Spezialist tiefgreifendes Wissen über einzelne Module aneignen.
Gründe für den Einsatz von DDD (Domain Driven Design)
Bei DDD wird im ersten Schritt in intensiver Zusammenarbeit mit Domänenexpertinnen und -experten der Problemraum (Domänenproblem) erforscht. Durch eine strukturierte Zerlegung vom Gesamtsystem in Module wird die Komplexität beherrschbarer. Ein weiterer Vorteil ist, wenn diese Module über dedizierte Schnittstellen kommunizieren und wenig Abhängigkeiten aufweisen, so dass sie besser gewartet und Änderungen schneller umgesetzt werden können.
Besonders wichtig ist, sich von veralteten Prozessen und Denkweisen zu lösen, um neue Ideen und Lösungen zu schaffen. Alte Gewohnheiten und Überzeugungen verhindern die Zusammenarbeit mit Domänenexpertinnen und -experten und die Entwicklung von agilen und flexiblen Softwaresystemen.
Umsetzung von DDD in der Praxis
Für die Umsetzung in der Praxis muss ein Verständnis der Domäne erarbeitet werden. Im Anschluss daran wird ein strategisches Design erstellt. Daraus entsteht am Ende für jedes Modul ein taktisches Design, das die Umsetzung der Software definiert.
Domäne verstehen
Für ein gemeinsames Verständnis eines Problemraums ist es wichtig eine einheitliche Sprache zu sprechen. Diese Sprache wird als „Ubiquitous Language“ bezeichnet. Zum Beispiel hat das Wort „Konto“ in unterschiedlichen Kontexten immer unterschiedliche Bedeutung. So muss festgelegt werden, ob es sich um ein Girokonto, Tagesgeldkonto, Festgeldkonto, Referenzkonto usw. handelt. Damit trägt eine einheitliche Sprache dazu bei, Missverständnisse und Verwirrung zu vermeiden. Der durchgängige Einsatz der „Ubiquitous Languages“ auf allen Ebenen fördert die effektive Zusammenarbeit.
Strategisches Design
Das strategische Design befasst sich mit dem großen Ganzen und dient dazu, komplexe Anwendungen zu verstehen und einen High-Level-Überblick, auch im Hinblick auf eine teamübergreifende Zusammenarbeit, zu erhalten. Es dient ferner dazu, Abhängigkeiten zwischen Teams hinsichtlich der Kommunikation zu verdeutlichen und zu bestimmen, welche Teams ggf. von Entscheidungen betroffen sein können. Im DDD wird kein kanonisches Domänenmodell angestrebt, sondern mehrere, voneinander isolierte Domänenmodelle. Diese sollen kontextabhängig entwickelt werden, um möglichst viel Flexibilität und Unabhängigkeit bei der Entwicklung zu erhalten. Die Domänenmodelle werden entsprechend voneinander abgegrenzt, stehen aber in einer Beziehung zueinander und müssen zur Erfüllung von Anwendungsfunktionalität zusammenarbeiten. Alle Beziehungen, ihre Art und der Datenfluss zwischen den Domänenmodellen sind Teil des strategischen Designs, auf dessen Bestandteile nachfolgend eingegangen wird.
Bestandteile des strategischen Designs
Bounded Contexts
Ein zentraler Bestandteil des DDD ist die Ubiquitous Language. Dabei sind die dort definierten Begriffe und ihre Bedeutung nicht zwangsläufig eineindeutig, sondern ergeben sich immer aus dem Kontext. Je nach Kontext kann ein Begriff eine andere Bedeutung haben. Ein Bounded Context definiert nun eine in sich abgeschlossene Einheit, in der Begriffe und Definitionen konsistent sind und ordnet ihnen eine Semantik zu. Jeder Bounded Context sollte von einem Team verwaltet werden, das aus Fachleuten für Domänen und Softwareentwicklung besteht. Da teamübergreifende Kommunikation und Abstimmungen komplex und aufwandsintensiv sein können, sollten nur geringe Abhängigkeiten zwischen Bounded Contexts geschaffen werden. Deshalb werden eine hohe Kohäsion und eine geringe Kopplung angestrebt. Diese Flexibilität wird dadurch erreicht, dass für jeden Bounded Context ein auf ihn genau abgestimmtes und von anderen Bounded Contexts isoliertes Domänenmodell entwickelt wird. Redundanzen zwischen verschiedenen Domänenmodellen werden dafür in Kauf genommen.
Context Maps
Damit eine Anwendung ihre Aufgaben erfüllen kann, müssen Bounded Contexts integriert werden und Daten und Nachrichten austauschen. Die Beziehungen zwischen Bounded Contexts und ihre Kommunikation untereinander wird in einer Context Map abgebildet. Sie bietet eine Übersicht über die gesamte Anwendung und definiert zugleich auch die Eigenschaften und Schnittstellen sowie Verantwortlichkeiten aller Bounded Contexts. Welche Verantwortlichkeiten ein solcher in einer bilateralen Beziehung hat, ergibt sich aus dem verwendeten Kollaborationsmuster, auf die weiter unten eingegangen wird. Die Auswahl des Kollaborationsmuster hat wesentlichen Einfluss auf die Zusammenarbeit der an den Bounded Contexts beteiligten Teams und ihre Entscheidungsmöglichkeiten hinsichtlich der Schnittstellenentwicklung.
Shared Kernel
Ein Shared Kernel zeichnet sich dadurch aus, dass sich zwei Teams Teile des Domänenmodells, des Codes und/oder der Datenbank teilen. Dies erhöht die Wiederverwendung und reduziert Redundanzen, was den Entwicklungsaufwand reduzieren kann. Allerdings erhöhen sich mit der Menge der geteilten Artefakte und Daten zugleich auch die Abhängigkeiten zwischen den Teams. Das kann in erhöhtem Kommunikations- und Koordinationssaufwand resultieren, sobald sich Änderungen am Shared Kernel ergeben. Besonders problematisch werden invasive, nicht abwärtskompatible Änderungen, die so weit wie möglich vermieden werden sollte. Eine teamunabhängige Weiterentwicklung wird damit erschwert und die Komplexität des geteilten Domänenmodells kann sich erhöhen, wenn es den Anforderungen beider Teams gerecht werden soll. Aus diesem Grund sollte ein Shared Kernel möglichst klein gehalten werden.
Consumer/Supplier
Das Consumer/Supplier-Kollaborationsmuster zeichnet sich dadurch aus, dass ein Team als Consumer Anforderungen hinsichtlich benötigter Funktionalität an das andere Team, dem Supplier, stellt und dieser dann die vom Consumer gewünschte Funktionalität zur Verfügung stellt. Neue Funktionalität hinsichtlich der Schnittstelle wird also nur auf Anfrage des Consumers entwickelt. Das hat zur Folge, dass es immer eine Verzögerung zwischen dem Stellen der Anforderungen und der Bereitstellung der Funktionalität gibt und der Consumer entsprechend Wartezeit einplanen muss. Auf der anderen Seite kann der Supplier die Funktionalität genauso zur Verfügung stellen, wie der Consumer sie benötigt.
Conformist
Im Gegensatz zum Consumer/Supplier hat beim Conformist-Kollaborationsmuster das Team, das die Funktionalität eines anderen Teams nutzen möchte, keinerlei Einfluss auf dieses und ist an die Schnittstellen und Vorgaben des Suppliers gebunden. Diese Art findet vor allem dann Anwendung, wenn es sich beim Supplier um einer Third-Party-Komponente, eine Fremdanwendung oder ein Legacy-System handelt, das nicht mehr oder nur in begrenztem Umfang erweitert wird.
Anticorruption Layer
Ein Anticorruption Layer ähnelt dem Conformist-Kollaborationsmuster in der Hinsicht, dass der Consumer keinen Einfluss auf die Funktionalität des Suppliers hat. Der Consumer entscheidet sich allerdings dafür, eine zusätzliche Schicht einzuführen, die ihn von den Schnittstellen, den Regeln und Konventionen des Suppliers entkoppelt. Die Schicht übernimmt dabei die Aufgabe eines uni- oder bidirektionalen Mappings zwischen Consumer und Supplier, die das Modell des Suppliers auf das Modell des Consumers abbildet. Die Folge ist zusätzlicher Aufwand für die Entwicklung und Wartung der neu eingeführten Schicht.
Open Host Service
Ein Open Host Service liegt vor, wenn die Schnittstelle öffentlich gemacht bzw. veröffentlich wird und von anderen (externen) Anwendungen und Services benutzt werden kann. Der Open Host Service bietet damit eine Dienstleistung, die von mehreren Consumern in Anspruch genommen werden kann. Eine stabile, flexible und gut dokumentierte API ist eine wichtige Voraussetzung, entsprechend viel Aufwand sollte in die API gesteckt werden. Nachträgliche Änderungen an ihr sind, insbesondere hinsichtlich invasiver Änderungen, problematisch und sollten vermieden werden, da sie Auswirkungen auf alle Consumer haben. Im besten Fall wird dies bereits während der Entwicklung berücksichtigt und eine versionierte API zur Verfügung gestellt.
Taktisches Design
Während sich das strategische Design mit der Aufteilung der Anwendung in Bounded Contexts und deren Beziehungen befasst, fokussiert sich das taktische Design auf deren Inhalt. Im Rahmen eines Bounded Contexts wird ein für ihn spezielles Domänenmodell entwickelt, das auf die Geschäftsregeln und Konventionen in dem jeweiligen Kontext ausgelegt ist und die Begriffe der Ubiquitous Language verwendet, die für den jeweiligen Kontext gültig sind. Ein Bounded Context sollte möglichst frei von technischen Aspekten sein und sich ausschließlich auf Domänenlogik fokussieren und diese explizit modellieren. Generische Ansätze sind zu vermeiden und widersprechen dem DDD. Zur Unterstützung des taktischen Designs in der Realisierung haben sich verschiedene Bestandteile herauskristallisiert.
Bestandteile des taktischen Designs
Entitys
Entitys repräsentieren Elemente aus der Domäne, die eine Identität und einen Lebenszyklus haben und deren Zustand sich im Laufe der Zeit ändern kann. Ihre Identität grenzt sie eindeutig von anderen Entitys ab. Ihnen kommt eine wichtige Rolle im Domänenmodell zuteil, da sie i. A. viel Domänenlogik enthalten. Durch fachliche Validierungsregeln stellen sie sicher, dass Invarianten der Domänenlogik nicht verletzt werden. Damit Letzteres sichergestellt wird, sollten Änderungen am Zustand von Entitys nur von ihnen selbst durch Ausführung der von ihnen zur Verfügung gestellten Domänenlogik erfolgen. Entitys werden in der Regel persistiert.
Value Objects
Value Objects unterscheiden sich von Entitys dahingehend, dass sie nur durch die Werte ihrer Attribute definiert sind, d. h. sie haben keine Identität. Values Objects mit gleichen Attributwerten sind beliebig austauschbar und gelten semantisch als gleich. Sie repräsentieren (komplexe) Werte und verfügen über keinen Lebenszyklus. Ihr Zustand ändert sich daher nicht, sie können aber, ebenso wie Entitys, Domänenlogik beinhalten.
Aggregates
Ein Aggregate gruppiert mehrere fachlich zusammengehörige Entitys und Value Objects zu einem Ganzen, befindet sich stets in einem konsistenten Zustand und wird immer komplett persistiert und geladen. Die Größe eines Aggregates sollte klein gehalten werden. In einem Aggregate ist immer eine Entity enthalten, die das Aggregate besitzt und gleichzeitig nach außen als Fassade fungiert. Sie wird als Aggregate Root bezeichnet und hat die Aufgabe, die Konsistenz und Invarianten des Aggregates sicherzustellen, insbesondere wenn Invarianten nur entity-übergreifend sichergestellt werden können. Die innere Struktur des Aggregates sollte von diesem gekapselt und nicht nach außen verfügbar und zugreifbar gemacht werden. Das vereinfacht zukünftige Änderungen, sollte die Domänenlogik angepasst werden und die aktuelle Struktur nicht mehr ausreichend sein.
Domain Services
Hin und wieder muss Domänenlogik realisiert werden, die nicht eindeutig einer Entity oder einem Value Object zugeordnet und daher nicht passend platziert werden kann. Diese Domänenlogik kann in (zustandslosen) Domain Services untergebracht werden. Jeder Domain Service sollte dabei nur eine spezifische Funktionalität realisieren.
Repositorys
Entitys und Value Objects eines Aggregates müssen zur späteren Verwendung oft persistiert und geladen werden. Diese Aufgabe übernehmen beim DDD Repositorys. Zu jedem Aggregate, das persistiert werden soll, existiert ein Repository, das eine fachliche Schnittstelle definiert. Über diese können Aggregates gesucht, persistiert und gelöscht werden. Query- und Aggregationsfunktionen wie z. B. Summenbildung, die auf Basis von persistierten Daten operieren, werden ebenfalls in der Schnittstelle des Repositorys aufgenommen.
Factorys
Die Erstellung von nicht persistierten Aggregates wird von Factorys übernommen, die insbesondere die Erstellung komplexer Aggregates erleichtern. Sie stellen zugleich sicher, dass sie sich anschließend in einem konsistenten Zustand befinden, insbesondere dann, wenn mehrere Schritte zum Aufbau eines Aggregates erforderlich wären. Bei einfachen Aggregates kann der Aggregate Root die Funktion einer Factory auch selbst übernehmen.
Vorteile von DDD
Ein großer Vorteil von DDD besteht in der Verwendung einer Ubiquitous Language zwischen Fachleuten für Domänen und für die Softwareentwicklung, die sich von der interpersonellen Kommunikation bis hin zum Domänenmodell und der Implementierung durchschlägt und ein gemeinsames Verständnis der Domäne fördert. Dies erleichtert die Kommunikation zwischen beiden Stakeholdern und vermeidet Missverständnisse aufgrund unklarer Begrifflichkeiten oder Übersetzungen zwischen technischem und fachlichem Vokabular.
Domänenregeln und -konzepte werden explizit in der Implementierung festgehalten und isoliert von technischen Aspekten zentral im Domänenmodell hinterlegt. Auf generische Lösungen wird verzichtet, um Fachlichkeit leicht aus der Implementierung ableiten zu können. Das ermöglicht, dass Domänenexpertinnen und -experten im Idealfall zusammen mit Softwareentwicklerinnen und -entwicklern fachlichen Code lesen und reviewen können.
DDD macht keine Vorgaben hinsichtlich zu verwendender Architekturstile. Allerdings wird eine strikte Trennung zwischen Domänenlogik und Infrastruktur angestrebt und das Domänenmodell sollte von anderen Teilen der Anwendung isoliert, in sich gekapselt und strikt von Zugriffen von außen geschützt werden. Der Vorteil ist ein Maximum an Flexibilität hinsichtlich zukünftiger Änderungen.
Nachteile von DDD
Von DDD kann am meisten profitiert werden, wenn Domänenexpertinnen und -experten sowie Softwareentwicklerinnen und -entwickler eng zusammenarbeiten. Dies setzt voraus, dass sie zur Verfügung stehen und dann im direkten Kontakt zueinander sind. Ansonsten besteht kaum eine Möglichkeit, ein gemeinsames und tiefgehendes Verständnis für die Domäne sowie eine Ubiquitous Language aufzubauen. DDD wäre in diesem Fall nicht konsequent anwendbar.
Auf Seiten der Softwareentwicklung muss hingegen Aufwand investiert werden, um die Domäne gründlich zu verstehen. Das ist nicht nur zeitintensiv, sondern setzt auch voraus, dass die Beteiligten willens sind, sich konzentriert und kontinuierlich mit der Domäne auseinanderzusetzen.
Anwendungen mit einfacher oder kaum vorhandener Domänenlogik eignen sich weniger für DDD, da die möglichen Vorteile der strikten Kapselung der Domänenlogik kaum zum Tragen kommen. Die Nachteile hinsichtlich des erhöhten Aufwands von der Kommunikation bis hin zur Isolierung des Domänenmodell in der Implementierung bleiben aber erhalten und überwiegen die Vorteile. Auch generische oder (hoch) konfigurative Lösungen eigenen sich nicht für DDD, da die Domänenlogik höchstens externalisiert vorhanden ist und nicht als Teil des Domänenmodells implementiert wird.
Technische Umsetzung von DDD: Über Microservices, Self-Contained Systems und alternative Architekturansätze
Microservices und Self-Contained Systems (SCS) sind Architekturstile, die es ermöglichen, einzelne Module aus dem Bounded Context zu implementieren. DDD setzt diese Architekturstile nicht zwingend voraus. Das Ziel von DDD in der Umsetzung ist die Modularisierung. Daher ist DDD auch ohne Einsatz von Microservices denkbar. Aber der Einsatz von Microservices setzt DDD oder alternative Ansätze voraus, um geeignete Module als Microservices zu identifizieren. Der Fokus von DDD liegt bei der Festlegung von Modulen mit klaren Schnittstellen und geringen Abhängigkeiten.
Erst bei der technischen Umsetzung wird der Architekturstil Microservices oder SCS festgelegt. Als Alternativen passen auch andere Architekturstile, die es ermöglichen, Module zu definieren und über dedizierte Schnittstellen kommunizieren lassen.
Fazit
Komplexe Domänenprobleme in Softwareprojekten erfolgreich zu bewältigen ist das Ziel von DDD. Dabei können auch einzelne Prinzipien und Techniken von DDD verwendet werden. Wichtig ist, alle Beteiligten an einen Tisch zu bekommen, damit ein Gesamtbild entsteht und einzelne Module definiert werden können. Anschließend können die Module von einzelnen Teams detailliert ausgearbeitet werden. In der heutigen Zeit ist es nicht mehr erforderlich, dass sich alle Beteiligten an einem Ort treffen müssen. Es gibt viele technische Möglichkeiten, virtuell zu kollaborieren. Auch dieser Fachbeitrag ist durch gemeinsames Bearbeiten eines Dokuments entstanden.
Der menschliche Faktor spielt eine große Rolle bei der erfolgreichen Umsetzung von Softwareprojekten. Das Stille-Post-Prinzip von der Fachabteilung über Personen aus der Projektleitung zu Softwareentwicklerinnen und -entwicklern führt nur in seltenen Fällen zum Erfolg. Daher muss die Zusammenarbeit mit Domänenexpertinnen und -experten gefördert werden: mit einer einheitlichen Vision, klaren Prinzipien und Werten der Zusammenarbeit sowie einer offenen Fehlerkultur. Durch regelmäßige Reflexion und kontinuierliche Verbesserung können Projekte dann effektiver und effizienter umgesetzt werden.
Links
Bei weiteren Fragen rund um Deep Dive Domain-Driven Design kontaktieren Sie uns gern.
Joachim Seidler
Segment Manager Industry – Manufacturing
j.seidler@smf.de
Weiterführende Links