Wichtigste Erkenntnisse
1. Entwickeln Sie eine allgegenwärtige Sprache für gemeinsames Verständnis
Nutzen Sie das Modell als Rückgrat einer gemeinsamen Sprache.
Zersplitterte Kommunikation. In vielen Softwareprojekten ist die Kommunikation zersplittert. Fachexperten verwenden ihre Fachsprache, Entwickler sprechen technische Begriffe, und selbst innerhalb der Entwicklerteams entstehen eigene Abstraktionen. Diese sprachliche Kluft führt zu Missverständnissen, unzuverlässiger Software und verhindert das tiefe Zusammenspiel von Wissen, das für aussagekräftige Modelle nötig ist. Die Übersetzung zwischen diesen „Dialekten“ verwässert Konzepte und erschwert effektives Wissensmanagement.
Modellgetriebene Sprache. Die Lösung besteht darin, bewusst eine „Ubiquitäre Sprache“ zu pflegen, die direkt aus dem Domänenmodell abgeleitet wird. Diese Sprache – bestehend aus Klassennamen, Operationen und expliziten Regeln – muss konsequent in allen Kommunikationsformen verwendet werden: im Gespräch, in Diagrammen, schriftlichen Dokumenten und vor allem im Code selbst. Diese dynamische Sprache entwickelt sich mit dem Modell weiter, legt Schwachstellen offen und sorgt dafür, dass alle Beteiligten ein präzises gemeinsames Verständnis teilen.
Vorteile einer gemeinsamen Sprache. Eine Ubiquitäre Sprache stellt sicher, dass ein Begriff stets eindeutig und konsistent im Team und in der Software verwendet wird. Sie ermöglicht es Fachexperten, Anforderungen präziser zu formulieren, und Entwicklern, Software zu bauen, die die Geschäftsprinzipien wirklich widerspiegelt. Dieses gemeinsame Vokabular wird zum zentralen Träger von Designaspekten, fördert kontinuierliches Lernen und erlaubt dem Team, sein Domänenverständnis gemeinsam zu verfeinern.
2. Binden Sie Ihr Domänenmodell direkt an den Code
Gestalten Sie einen Teil des Softwaresystems so, dass das Domänenmodell buchstäblich abgebildet wird und die Zuordnung offensichtlich ist.
Die Analyse-Design-Dichotomie. Viele Projekte leiden unter einer Trennung zwischen dem anfänglichen „Analysemodell“ (konzeptionell) und dem „Designmodell“ (implementierungsorientiert). Diese Trennung führt oft dazu, dass das Analysemodell mit Beginn der Codierung irrelevant wird, wertvolle Erkenntnisse verloren gehen und komplexe, schwer wartbare Software entsteht. Der Code, ohne konzeptionelle Grundlage, wird zum bloßen Mechanismus, der seine Handlungen nicht erklärt.
Modellgetriebenes Design. Das zentrale Prinzip ist, diese Dichotomie aufzugeben und ein einziges Modell anzustreben, das sowohl Analyse als auch Design dient. Das bedeutet, dass der Code selbst eine direkte Ausdrucksform des Domänenmodells wird, wodurch die Zuordnung zwischen Konzepten und Implementierung klar wird. Jedes Objekt im Design übernimmt eine konzeptionelle Rolle, die im Modell beschrieben ist, sodass Erkenntnisse aus der Analyse erhalten bleiben und sich in der laufenden Software widerspiegeln.
Praktische Modellierer. Diese enge Bindung erfordert Entwickler, die „praktische Modellierer“ sind. Sie müssen nicht nur Code schreiben, sondern auch verstehen, wie ihr Code das Modell verändert. Dieser Ansatz verlangt ein Modell, das sowohl konzeptionell reichhaltig als auch praktisch umsetzbar ist, was oft iterative Verfeinerung und Refactoring erfordert. Objektorientierte Programmierung, mit ihren direkten Analogien zu konzeptionellen Objekten, eignet sich besonders gut, um diese buchstäbliche Entsprechung zu unterstützen.
3. Isolieren Sie die Domänenschicht von technischen Belangen
Konzentrieren Sie den gesamten Code, der das Domänenmodell betrifft, in einer Schicht und trennen Sie ihn von Benutzeroberfläche, Anwendungs- und Infrastrukturcode.
Komplexitätsdiffusion. In vielen Anwendungen ist domänenbezogener Code über UI, Datenbank und andere technische Komponenten verstreut. Diese Verflechtung erschwert es enorm, die Geschäftslogik nachzuvollziehen, da oberflächliche UI-Änderungen unbeabsichtigt Geschäftsregeln beeinflussen können und eine Änderung der Geschäftslogik oft eine mühsame Spurensuche durch verschiedene technische Schichten erfordert. Kohärente, modellgetriebene Objekte werden so praktisch unmöglich umzusetzen.
Geschichtete Architektur. Die Lösung ist eine „Geschichtete Architektur“, die das Programm in klar abgegrenzte Schichten unterteilt:
- Benutzeroberfläche (Präsentationsschicht): Verantwortlich für Interaktion und Anzeige.
- Anwendungsschicht: Definiert Softwareaufgaben, koordiniert Abläufe, delegiert an Domänenobjekte.
- Domänenschicht (Modellschicht): Repräsentiert Geschäftsbegriffe, Regeln und Zustände. Das Herzstück der Geschäftsanwendung.
- Infrastrukturschicht: Stellt generische technische Funktionen bereit (Persistenz, Messaging, UI-Komponenten).
Vorteile der Isolation. Durch die Isolierung der Domänenschicht werden Domänenobjekte von Aufgaben wie Darstellung oder Persistenz befreit. So kann das Modell wachsen, reichhaltig und klar werden, um das wesentliche Geschäftsverständnis abzubilden. Diese Trennung erleichtert zudem das Verstehen, Warten und Testen jeder Schicht, da sie sich unterschiedlich schnell entwickelt und auf verschiedene Anliegen reagiert.
4. Strukturieren Sie Ihr Domänenmodell mit Entitäten, Wertobjekten und Services
Wenn ein Objekt durch seine Identität definiert ist und nicht durch seine Attribute, machen Sie dies zum zentralen Merkmal im Modell.
Entitäten (Referenzobjekte). Manche Objekte sind grundlegend durch ihre Identität definiert, nicht durch ihre Eigenschaften. Eine „Entität“ besitzt einen Lebensfaden, der sich durch ihren gesamten Lebenszyklus zieht, auch wenn sich Attribute ändern. Zum Beispiel ist ein Kunde eine Entität, eindeutig identifiziert durch eine KundenID, unabhängig von Änderungen an Adresse oder Namen. Entitäten sollten einfach gehalten werden, mit Fokus auf Identität; ihre Klassendefinitionen, Verantwortlichkeiten und Beziehungen drehen sich um „wer sie sind“.
Wertobjekte. Im Gegensatz dazu repräsentieren „Wertobjekte“ beschreibende Aspekte der Domäne ohne konzeptionelle Identität. Hier zählt nur „was sie sind“, nicht „welches“. Beispiele sind Adresse, Geldbetrag oder Farbe. Wertobjekte sollten unveränderlich (immutable) sein; ändert sich ein Wert, wird ein neues Wertobjekt erzeugt. Diese Unveränderlichkeit vereinfacht das Design, erlaubt sicheres Teilen und Kopieren und ermöglicht Performance-Optimierungen.
Services. Manchmal gehören domänenspezifische Operationen nicht natürlich zu einer Entität oder einem Wertobjekt. Diese „Services“ sind eigenständige Schnittstellen, definiert durch das, was sie für einen Klienten tun können. Services sind meist zustandslos, werden nach Tätigkeiten (Verben) benannt und ihre Schnittstellen beziehen sich auf andere Domänenobjekte. Sie werden gezielt eingesetzt, um Verhalten nicht aus Entitäten oder Wertobjekten zu entfernen, sondern um wichtige domänenspezifische Aktionen oder Transformationen angemessen unterzubringen.
5. Verwalten Sie Objektlebenszyklen mit Aggregaten, Fabriken und Repositories
Fassen Sie ENTITÄTEN und WERTOBJEKTE zu AGGREGATEN zusammen und definieren Sie klare Grenzen.
Integrität bewahren. Komplexe Objektbeziehungen können zu verworrenen Netzen führen, die es erschweren, Datenkonsistenz zu wahren und Invarianten durchzusetzen. „Aggregate“ lösen dieses Problem, indem sie assoziierte Objekte zu einer Einheit für Datenänderungen bündeln. Jedes Aggregate besitzt eine „Wurzel“-Entität, die einzige Instanz außerhalb des Aggregats, die Referenzen halten darf. Alle Invarianten innerhalb des Aggregats müssen beim Commit der Wurzel erfüllt sein, was Transaktionsmanagement vereinfacht und Konsistenz sicherstellt.
Erstellung kapseln. Die Objekterzeugung kann komplex sein, mit aufwändiger Zusammensetzung und Invariantenprüfung. „Fabriken“ kapseln diese Komplexität, indem sie die Verantwortung für die Instanziierung komplexer Objekte oder Aggregate übernehmen. Fabriken bieten eine absichtserkennende Schnittstelle, die interne Strukturen und konkrete Klassen verbirgt und sicherstellt, dass nur gültige, konsistente Objekte oder Aggregate erzeugt werden. Sie sind auch essenziell für das Wiederherstellen gespeicherter Objekte.
Zugriff auf persistente Objekte. Klienten benötigen eine praktische Möglichkeit, Referenzen auf bestehende Domänenobjekte zu erhalten, ohne sich mit Persistenztechnologien zu belasten. „Repositories“ erfüllen diese Aufgabe, indem sie alle Objekte eines Typs als konzeptionelle In-Memory-Sammlung repräsentieren. Sie kapseln Datenbankabfragen und Metadaten-Mapping und erlauben es Klienten, Objekte anhand von Kriterien über eine einfache, modellorientierte Schnittstelle anzufordern. Repositories werden typischerweise nur für Aggregate-Wurzeln bereitgestellt, die globalen Zugriff benötigen.
6. Refaktorieren Sie unermüdlich für tiefere Domänen-Einsichten
Jede Verfeinerung von Code und Modell verschafft Entwicklern eine klarere Sicht. Diese Klarheit schafft Potenzial für Durchbrüche.
Der iterative Zyklus. Softwareentwicklung ist ein iterativer Prozess ständigen Lernens und Verfeinerns. Entwickler refaktorieren Code nicht nur aus technischer Sauberkeit, sondern um ihr Domänenverständnis zu vertiefen und das Design enger an diese Einsichten anzupassen. Dieser Prozess umfasst:
- Absichtserkennende Schnittstellen: Klassen und Operationen werden so benannt, dass ihr Zweck klar wird, ohne dass Klienten interne Details kennen müssen.
- Nebenwirkungsfreie Funktionen: Komplexe Logik wird in Operationen ausgelagert, die Ergebnisse ohne beobachtbare Nebenwirkungen liefern, was sichere Kombination und Tests ermöglicht.
- Assertions: Postbedingungen und Invarianten werden explizit formuliert, um garantierte Zustände und Verhalten vorhersehbar zu machen.
Konzeptionelle Konturen. Effektive Zerlegung richtet Design-Elemente an den „konzeptionellen Konturen“ der Domäne aus – den natürlichen, sinnvollen Gliederungen. So entsteht ein flexibles Design, das dort anpassbar ist, wo Veränderung häufig ist, und stabil bleibt, wo Konzepte grundlegend sind. Beim Refactoring gilt es, zugrundeliegende Konsistenzen zu erkennen, die Muster von Wandel und Beständigkeit erklären.
Durchbrüche. Viele Verbesserungen sind inkrementell, doch kontinuierliches Refactoring bereitet oft den Boden für plötzliche „Durchbrüche“ – tiefgreifende Erkenntnisse, die das Modell dramatisch vereinfachen, vielseitiger machen und bei Fachexperten stark Resonanz finden. Diese Momente sind herausfordernd, aber entscheidend für wirklich kraftvolle und elegante Designs.
7. Bewahren Sie die Modellintegrität über begrenzte Kontexte hinweg
Definieren Sie explizit den Kontext, in dem ein Modell gilt.
Die Herausforderung großer Systeme. In großen Projekten ist ein einziges, einheitliches Domänenmodell für das gesamte Unternehmen oft unrealistisch oder unwirtschaftlich. Es entstehen zwangsläufig mehrere Modelle, was zu Problemen wie doppelten Konzepten, „falschen Freunden“ (gleicher Begriff, unterschiedliche Bedeutung) und unzuverlässiger Software führt, wenn Code aus verschiedenen Modellen kombiniert wird.
Begrenzte Kontexte (Bounded Contexts). Die Lösung ist, „Bounded Contexts“ klar zu definieren – abgegrenzte Bereiche eines Softwaresystems, in denen ein einheitliches Modell gilt. Diese Grenzen orientieren sich an Teamorganisation, spezifischen Anwendungsbereichen und physischen Manifestationen wie Codebasen. Innerhalb eines Bounded Context bleibt das Modell strikt konsistent, außerhalb gelten andere Modelle mit eigener Terminologie und Regeln.
Kontextkarte. Eine „Kontextkarte“ bietet einen globalen Überblick, identifiziert alle Bounded Contexts im Projekt und beschreibt deren Berührungspunkte und Beziehungen. Sie macht sichtbar, wo Modelle auseinandergehen und Übersetzungen nötig sind. Innerhalb jedes Bounded Context ist „Continuous Integration“ (häufiges Zusammenführen, automatisierte Tests, konsequente Nutzung der Ubiquitären Sprache) essenziell, um Modellfragmentierung zu verhindern.
Beziehungsmuster. Verschiedene Muster definieren, wie Bounded Contexts interagieren:
- Shared Kernel: Zwei Teams teilen einen Teil des Modells und Codes.
- Customer/Supplier: Ein nachgelagertes Team agiert als Kunde des vorgelagerten Teams, verhandelt Anforderungen und nutzt automatisierte Abnahmetests.
- Conformist: Das nachgelagerte Team folgt dem vorgelagerten Modell strikt, was Integration erleichtert, aber Designfreiheit einschränkt.
- Anticorruption Layer: Eine isolierende Schicht, die zwischen dem Domänenmodell eines Klienten und einem anderen System (oft Legacy oder extern) übersetzt und das Klientenmodell vor Verunreinigung schützt.
- Separate Ways: Zwei Kontexte haben keine Verbindung, was einfache, spezialisierte Lösungen erlaubt.
- Open Host Service: Ein Subsystem definiert ein öffentliches Protokoll als Satz von Diensten für viele Integratoren.
- Published Language: Eine gut dokumentierte gemeinsame Sprache (z. B. XML DTD) dient als Kommunikationsmedium zwischen Systemen.
8. Destillieren Sie strategisch Ihre Kerndomäne
Reduzieren Sie das Modell auf das Wesentliche. Finden Sie die KERNDOMÄNE und schaffen Sie Mittel, sie leicht von unterstützendem Modell und Code zu unterscheiden.
Verdeckter Wert. In großen Systemen kann der wahre Geschäftswert – die „Kerndomäne“ – von einer Masse unterstützender Komponenten verdeckt werden. Das führt zu:
- Schwierigkeiten beim Verstehen und Ändern des Systems.
- Fehlallokation von Talenten (Top-Entwickler beschäftigen sich mit generischen technischen Problemen).
- Schwachem Design in den wichtigsten, differenzierenden Teilen der Software.
Kerndomäne. Die Kerndomäne umfasst die wertvollsten, spezialisierten und charakteristischen Konzepte, die den Zweck der Anwendung ausmachen. Hier sollten sich die besten Talente konzentrieren, um ein tiefes Modell zu finden und ein flexibles Design zu entwickeln. Investitionen in andere Teile des Systems sollten sich daran messen, wie sie diesen Kern unterstützen.
Destillationstechniken:
- Generische Subdomänen: Identifizieren Sie zusammenhängende Subdomänen, die nicht zentral für das Projekt sind (z. B. Zeitzonen, generische Buchhaltung). Lagern Sie sie in separate Module aus, erwägen Sie Standardlösungen oder Outsourcing. So wird die Kerndomäne entlastet.
- Domain Vision Statement: Ein kurzes (eine Seite) Dokument, das den Wert der Kerndomäne beschreibt und Ressourcenallokation sowie Modellierungsentscheidungen leitet.
- Hervorgehobener Kern: Markieren Sie Kernelemente im Hauptmodell-Repository (z. B. Diagramme, Code-Kommentare), um sie mühelos sichtbar zu machen.
- Kohäsive Mechanismen: Gliedern Sie konzeptionell zusammenhängende Rechenprobleme (z. B. Graphdurchlaufalgorithmen) in separate, leichte Frameworks aus. So kann die Kerndomäne sich auf die Problemformulierung konzentrieren und das „Wie“ an den Mechanismus delegieren.
- Getrennter Kern: Refaktorieren Sie das Modell, um Kernkonzepte physisch in eigene Module zu trennen, was Kohäsion stärkt und Kopplung an unterstützenden Code reduziert.
- Abstrakter Kern: Lagern Sie die grundlegendsten Konzepte in eigene abstrakte Klassen oder Schnittstellen in einem eigenen Modul aus, die die meisten Interaktionen zwischen wichtigen Komponenten ausdrücken.
9. Verordnen Sie eine groß angelegte Struktur für Systemverständnis
Entwickeln Sie ein Muster aus Regeln, Rollen und Beziehungen, das das gesamte System überspannt und ein Verständnis für die Stellung jedes Teils im Ganzen ermöglicht – auch ohne detailliertes Wissen über dessen Verantwortung.
Überwältigende Komplexität. Selbst mit klar definierten Modulen und Destillation kann ein großes Modell zu komplex sein, um es als Ganzes zu erfassen. Ohne ein übergeordnetes Ordnungsprinzip fällt es Entwicklern schwer, Zusammenhänge zu erkennen, was zu inkonsistenten Designs und fragmentiertem Wissen führt.
Groß angelegte Struktur. Eine „Groß angelegte Struktur“ ist eine Sprache aus hochrangigen Konzepten oder Regeln, die das gesamte System durchdringt, Design lenkt und das Verständnis erleichtert. Sie hilft, unabhängige Arbeiten zu koordinieren, indem sie ein gemeinsames Bild des Ganzen schafft. Diese Struktur entwickelt sich mit der Anwendung weiter und vermeidet starre, vorab festgelegte Architekturen, die Entwicklung hemmen.
Arten von Strukturen:
- Systemmetapher: Eine konkrete, leicht verständliche Analogie (z. B. „Firewall“), die Vorstellungskraft anregt und Designentscheidungen systemweit lenkt.
- Verantwortungsschichten: Natürliche Domänenschichten werden als breite abstrakte Verantwortlichkeiten gegliedert (z. B. „Potential“, „Operationen“, „Entscheidungsunterstützung“, „Richtlinien“, „Verpflichtung“). Schichten nutzen Dienste darunter, sind aber unabhängig von darüberliegenden.
- Wissensebene: Eine eigene Objektsammlung, die Struktur und Verhalten des Basismodells beschreibt und einschränkt, um Anpassungen zu ermöglichen (z. B.
Mitarbeitertyp, der Regeln fürMitarbeiterdefiniert). - Plug-in-Komponenten-Framework: Ein sehr ausgereiftes Modell mit einem „Abstrakten Kern“ aus Schnittstellen und Interaktionen, das vielfältige Implementierungen frei austauschbar macht. Typisch für Systeme mit vielen interoperierenden Anwendungen.
Vorteile und Vorsicht. Eine passende groß angelegte Struktur macht das System übersichtlicher, erleichtert Transformationen und unterstützt Entwickler bei konsistenten Entscheidungen. Eine unpassende oder zu restriktive Struktur kann jedoch Entwicklung behindern, weshalb Minimalismus und kontinuierliche Überprüfung entscheidend sind.
Rezensionsübersicht
Domain-Driven Design wird unterschiedlich bewertet. Viele loben die bahnbrechenden Ideen zur Softwareentwicklung, insbesondere die Betonung, sich auf die Fachdomäne zu konzentrieren und eine gemeinsame Sprache zwischen Entwicklern und Fachexperten zu schaffen. Leser schätzen die praxisnahen Beispiele und wertvollen Einsichten. Dennoch kritisieren einige die Länge des Buches, den teils wiederholenden Inhalt sowie den dichten Schreibstil. Während es als unverzichtbare Lektüre für erfahrene Entwickler gilt, kann es für Einsteiger eine Herausforderung darstellen. Trotz seiner Schwächen wird das Werk weithin als einflussreich für die Gestaltung moderner Softwarearchitektur angesehen.
Andere lasen auch