Share via


Bewährte Methoden

Einführung in den domänengesteuerten Entwurf

David Laribee

Themen in diesem Artikel:

  • Modellierung auf Grundlage der Ubiquitous Language
  • Bounded Contexts und Aggregatstämme
  • Verwenden des Prinzips der einzigen Verantwortung
  • Repositorys und Datenbanken
In diesem Artikel werden folgende Technologien verwendet:
Visual Studio

Inhalt

Das platonische Modell
Sprechen Sie die richtige Sprache
Kontext
Beachten des geschäftlichen Nutzens
Ein System einziger Verantwortungen
Entitäten haben eine Identität und einen Lebenszyklus
Wertobjekte beschreiben Dinge
Aggregatstämme kombinieren Entitäten
Domänendienste modellieren primäre Vorgänge
Repositorys dienen zum Speichern und Verteilen von Aggregatstämmen
Datenbanken
Erste Schritte mit DDD

Beim domänengesteuerten Entwurf (Domain-Driven Design, DDD) handelt es sich um eine Sammlung von Prinzipien und Mustern, die Entwicklern beim Entwurf eleganter Objektsysteme helfen. Ordnungsgemäß angewandt, kann DDD zu Softwareabstraktionen führen, die als Domänenmodelle bezeichnet werden. Diese Modelle kapseln komplexe Geschäftslogik und schließen die Lücke zwischen Geschäftsrealität und Code.

In diesem Artikel werden die grundlegenden Konzepte und Entwurfsmuster beschrieben, die für DDD relevant sind. Gleichzeitig bietet er eine behutsame Einführung in den Entwurf und die Entwicklung reichhaltiger Domänenmodelle. Um die folgenden Erläuterungen in einen Zusammenhang zu stellen, ziehe ich als Beispiel ein komplexes Geschäftsfeld heran, mit dem ich vertraut bin: die Verwaltung von Versicherungspolicen.

Wenn Ihnen die in diesem Artikel vorgestellten Ideen gefallen, empfehle ich Ihnen nachdrücklich, Ihre Kenntnisse zu vertiefen, indem Sie das Buch Domain-Driven Design: Tackling Complexity in the Heart of Software von Eric Evans lesen. Es bietet nicht nur einfach die Einführung in DDD, sondern ist auch eine Schatzkiste mit wertvollen Informationen von einem der erfahrensten Softwaredesigner in der Branche. Die Muster und die zentralen Grundsätze von DDD, die in diesem Artikel beschrieben werden, sind von den in diesem Buch erläuterten Konzepten abgeleitet.

Gliedern von Kontexten nach architektonischen Anforderungen

Bounded Contexts müssen nicht ausschließlich nach dem Funktionsbereich einer Anwendung organisiert werden. Sie eignen sich hervorragend zur Unterteilung eines Systems, um gewünschte Architekturbeispiele zu erreichen. Ein klassisches Beispiel für diesen Ansatz ist eine Anwendung, die sowohl über ein stabiles Transaktionsvolumen als auch über ein Portfolio von Berichten verfügt.

Unter solchen Umständen (die recht häufig auftreten können) ist es oft wünschenswert, die Berichtsdatenbank von der Transaktionsdatenbank zu trennen. Sie möchten die Freiheit haben, den richtigen Normalisierungsgrad für die Entwicklung zuverlässiger Berichte zu verfolgen, und Sie möchten eine objektrelationale Zuordnung verwenden, damit Sie beim Programmieren der transaktionalen Geschäftslogik im objektorientierten Paradigma bleiben können. Mithilfe einer Technologie wie Microsoft Message Queue (MSMQ) können Sie aus dem Modell stammende Datenaktualisierungen veröffentlichen und sie in für Berichts- und Analysezwecke optimierte Data Warehouses integrieren.

Für den einen oder anderen mag dies vielleicht überraschend sein, aber es ist möglich, dass Datenbankadministratoren und Entwickler miteinander auskommen. Bounded Contexts ermöglichen Ihnen, einen Blick auf diesen paradiesischen Zustand zu erhaschen. Wenn Sie sich für architekturbezogene Bounded Contexts interessieren, empfehle ich Ihnen nachdrücklich, regelmäßig den Blog von Greg Young zu lesen. Er verfügt über umfangreiche Erfahrung mit diesem Ansatz, den er engagiert vertritt, und verfasst zahlreiche Beiträge zu diesem Thema.

Das platonische Modell

Da es sich hierbei um eine Einführung handelt, ist es sinnvoll zu definieren, was ich mit „Modell“ meine. Die Beantwortung dieser Frage begeben wir uns auf eine kurze, metaphysische Reise. Wer könnte uns dabei besser führen als Platon?

Platon, der bekannteste Schüler des Sokrates, stellte die These auf, dass die Konzepte, Personen, Orte und Dinge, die wir mit unseren Sinnen intuitiv erkennen und wahrnehmen, lediglich Schatten der Wahrheit sind. Er bezeichnete diese Idee von etwas Wahrem als „Form“.

Um Formen zu erklären, verwendete Platon das so genannte Höhlengleichnis. In diesem Gleichnis beschrieb er ein Volk von Menschen, die in einer tiefen, dunklen Höhle festgebunden sind. Diese Höhlenmenschen sind so angekettet, dass sie immer nur eine leere Wand der Höhle sehen können, auf die Licht vom Höhleneingang fällt. Wenn ein Tier am Eingang vorbeiläuft, wird ein Schatten auf die innere Wand geworfen, auf die die Höhlenbewohner blicken. Für die Höhlenbewohner sind diese Schatten die wahre Welt. Wenn ein Löwe vorbeiläuft, zeigen sie auf den Schatten des Löwen und schreien „in Deckung!“. In Wirklichkeit handelt es sich jedoch nur um einen Schatten der wahren Form, des Löwen selbst.

Sie können Platons Theorie der Formen auf DDD beziehen. Viele seiner Leitlinien helfen uns, im Laufe der Zeit dem idealen Modell näherzukommen. Der Weg zu der Form, die Sie mit Ihrem Code beschreiben möchten, versteckt sich in den Köpfen von Fachgebietsexperten, den Wünschen der Beteiligten und den Anforderungen der Branchen, in denen Sie arbeiten. Diese sind in einem sehr realen Sinne die Schatten für die imaginären Höhlenbewohner Platons.

Darüber hinaus sind Sie bei dem Versuch, diese Form zu erreichen, häufig Einschränkungen durch Programmiersprachen und Überlegungen zum Zeit- und Budgetrahmen unterworfen. Es ist nicht allzu weit hergeholt, diese Einschränkungen mit Höhlenbewohnern gleichzusetzen, die immer nur die innere Wand mit Schatten sehen können.

Gute Modelle zeichnen sich unabhängig von ihrer Implementierung durch eine Reihe von Merkmalen aus. Tatsache ist, dass ein angehender Domänenmodellierer als Erstes den Unterschied zwischen dem Modell in jedermanns Kopf und dem Modell, das in Codeform gebracht wird, verstehen sollte.

Die Software, die Sie erstellen, ist nicht das wahre Modell. Sie ist nur eine Manifestation – sozusagen ein Schatten – der Form der Anwendung, die Sie erreichen wollten. Obwohl es sich um eine Imitation der perfekten Lösung handelt, können Sie versuchen, diesen Code mit der Zeit näher an die wahre Form heranzubringen.

In DDD wird diese Idee als modellgesteuerter Entwurf bezeichnet. Ihr Verständnis des Modells wird in Ihrem Code entwickelt. Anwender des domänengesteuerten Entwurfs möchten sich nicht mit Unmengen an Dokumentation oder komplizierten Diagrammerstellungstools herumplagen. Stattdessen versuchen sie, ihr Verständnis des Fachgebiets (der „Domäne“) direkt in ihren Code einfließen zu lassen.

Die Idee, dass der Code das Modell erfasst, spielt in DDD eine zentrale Rolle. Wenn Sie Ihre Software auf das vorliegende Problem konzentrieren und auf die Lösung dieses Problems beschränken, erhalten Sie Software, die für neue Einblicke und Momente der Erleuchtung empfänglich ist. Mir gefällt, wie Eric Evans es nennt: Wissen zu Modellen verarbeiten. Wenn Sie etwas Wichtiges über das Fachgebiet lernen, wissen Sie genau, wohin Sie gehen müssen.

Sprechen Sie die richtige Sprache

Betrachten wir nun einige der Verfahren, die DDD für das Erreichen dieses Ziels bietet. Ein Großteil Ihrer Arbeit als Entwickler besteht in der Zusammenarbeit mit Nichtprogrammierern, um zu verstehen, was Sie liefern sollen. Wenn Sie in einer Organisation mit irgendeiner Art von Prozess arbeiten, lassen Sie die Anforderungen wahrscheinlich in Form einer Benutzergeschichte, einer Aufgabe oder eines Anwendungsfalls ausdrücken. Ungeachtet der Art der Anforderungen oder Spezifikation, die Sie erhalten – sind diese in der Regel vollständig?

Meist sind die Anforderungen etwas vage oder werden auf einer allgemeinen Verständnisebene ausgedrückt. Während des Entwerfens und Implementierens einer Lösung ist es vorteilhaft, wenn die Entwickler Zugang zu den Personen haben, die bestimmtes Fachwissen in die vorgesehene Domäne einbringen. Genau dies ist der Sinn von Benutzergeschichten, die in der Regel gemäß einer Vorlage wie der folgenden ausgedrückt werden: Ich als [Rolle] möchte [Feature], damit [Vorteil].

Betrachten wir ein Beispiel aus dem Bereich der Versicherungspolicenverwaltung: Als Underwriter möchte ich die Genehmigungskontrolle über eine Police, damit ich unbedenkliche Risiken zeichnen und bedenkliche Risiken ablehnen kann.

Versteht jeder, was das bedeutet? Ich verstand es nicht so ganz, als ich diese schriftlichen und nach Priorität geordneten Anforderungen las. Wie sollen Sie anhand dieser abstrahierten Beschreibung alles verstehen, was zur Bereitstellung der entsprechenden Software erforderlich ist?

Benutzergeschichten sind, wenn sie gut geschrieben sind, eine Einladung zu einem Gespräch mit ihrem Verfasser – dem Benutzer. Wenn Sie mit der Arbeit an dem Feature zur Genehmigung/Ablehnung von Policen beginnen, sollten Sie daher im Idealfall Zugang zu einem Underwriter haben. Für Uneingeweihte: Underwriter sind Fachgebietsexperten (zumindest die Guten), die bestimmen, ob die Abdeckung einer bestimmten Risikokategorie für einen Versicherungsträger unbedenklich ist.

Wenn Sie beginnen, das Feature mit dem zuständigen Underwriter (oder dem jeweiligen Fachgebietsexperten für das Projekt) zu erörtern, achten Sie besonders auf die Terminologie, die der Underwriter verwendet. Diese Fachgebietsexperten verwenden Standardterminologie des Unternehmens oder der Branche. In DDD wird dieses Vokabular als „Ubiquitous Language“ bezeichnet. Als Entwickler sollten Sie dieses Vokabular verstehen und es nicht nur beim Gespräch mit Fachgebietsexperten verwenden, sondern die gleiche Terminologie soll sich auch in Ihrem Code widerspiegeln. Wenn die Begriffe „Klassencode“ oder „Prämiensätze“ oder „Risiko“ beim Gespräch häufig verwendet werden, würde ich erwarten, entsprechende Klassennamen im Code zu finden.

Hierbei handelt es sich um ein grundlegendes Muster von DDD. Auf den ersten Blick erscheint die Ubiquitous Language wie eine offensichtliche Sache, und die Chancen stehen gut, dass einige von Ihnen diese Methode bereits intuitiv praktizieren. Es ist jedoch äußerst wichtig, dass Entwickler die Geschäftssprache bewusst und diszipliniert im Code verwenden. Dadurch wird die Trennung zwischen Geschäftsvokabular und technologischer Fachsprache abgeschwächt. Das Wie ordnet sich dem Was unter, und Sie bleiben näher an dem Grund für Ihre Arbeit: der Erzielung eines geschäftlichen Nutzens.

Kontext

Entwickler sind in einem gewissen Sinne Organisatoren. Ihr Code ist eine Abstraktion zur Lösung von Problemen. Tools wie Entwurfsmuster, geschichtete Architekturen und objektorientierte Prinzipien ergeben ein Framework für Anwendung von Ordnung auf immer kompliziertere Systeme.

DDD erweitert Ihr organisatorisches Repertoire und bedient sich bei bekannten Branchenmustern. An den Organisationsmustern, die DDD bietet, gefällt mir am besten, dass es Lösungen für jede Detailebene in einem System gibt. So genannte „Bounded Contexts“ führen Sie dahin, dass Sie sich Software als ein Portfolio von Modellen vorstellen. Module helfen Ihnen dabei, ein größeres Einzelmodell in kleinere Abschnitte zu unterteilen. An späterer Stelle werde ich Aggregatstämme als Verfahren für das Organisieren kleinerer Kollaborationen zwischen einigen eng verwandten Klassen erläutern.

In den meisten Unternehmenssystemen gibt es grob umrissene Verantwortungsbereiche. In DDD wird diese oberste Organisationsebene als „Bounded Context“ (abgegrenzter Kontext) bezeichnet.

Bei Arbeiterunfall-Versicherungspolicen müssen u. a. die folgenden Elemente berücksichtigt werden:

  • Angebotserstellung und Verkauf
  • Allgemeiner Policenworkflow (Verlängerungen, Kündigungen)
  • Überprüfung der Gehaltskostenschätzung
  • Vierteljährliche Selbsteinschätzungen
  • Festlegung und Verwaltung von Prämien
  • Provisionszahlungen an Agenturen und Makler
  • Rechnungsstellung für Kunden
  • Hauptbuchhaltung
  • Bestimmung akzeptabler Risiken (Underwriting)

Das ist aber eine ganze Menge! Sie könnten all dies in ein einzelnes, monolithisches System integrieren, begeben sich dadurch aber auf undurchsichtiges, amorphes Gelände. Es sind zwei völlig verschiedene Dinge gemeint, wenn im Kontext eines allgemeinen Workflows und im Kontext der Lohnlistenüberprüfung von einer Police gesprochen wird. Wenn Sie die gleiche Policenklasse verwenden, blähen Sie das Profil dieser Klasse auf und entfernen sich weit von bewährten Methoden wie dem Prinzip der einzigen Verantwortung (Single Responsibility Principle, SRP).

Systeme, in denen Bounded Contexts nicht isoliert und voneinander getrennt werden, gleiten häufig in einen Architekturstil ab, der (amüsanterweise) als „Big Ball of Mud“ (großer Matschklumpen, auch als „Spaghetticode“ bekannt) bezeichnet wird. 1999 haben Brian Foot und Joseph Yoder diesen Architekturstil (bzw. Antiarchitekturstil) in ihrer klassischen gleichnamigen Abhandlung (Big Ball of Mud) definiert.

DDD veranlasst Sie, Kontexte zu identifizieren und Ihre Modellierungsarbeit innerhalb bestimmter Kontexte vorzunehmen. Sie können die Grenzen des Systems mithilfe eines einfachen Diagramms untersuchen, das als „Context Map“ (Kontextzuordnung) bezeichnet wird. Ich habe oben die Kontexte aufgezählt, die Bestandteile eines umfassenden Systems zur Verwaltung von Versicherungspolicen sind, und in Abbildung 1 wird dies von einer Textbeschreibung in eine (partielle) grafische Context Map überführt.

fig01.gif

Abbildung 1 Vom Bounded Context zur Context Map

Haben Sie einige wichtige Beziehungen zwischen den verschiedenen Bounded Contexts bemerkt? Dies sind wertvolle Informationen, da Sie jetzt mit dem Treffen fundierter geschäftlicher und architekturbezogener Entscheidungen beginnen können, beispielsweise hinsichtlich Paketerstellung und Bereitstellungsentwurf, der Auswahl der für das Marshalling von Nachrichten zwischen Modellen verwendeten Technologie und – was vielleicht am wichtigsten ist – der Punkte, an denen Sie Meilensteine festlegen, und der Bereiche, in denen Sie Arbeit, Zeit und Talent aufwenden.

Ein letzter, aber sehr wichtiger Gedanke zu Bounded Contexts: Jeder Kontext besitzt seine eigene Ubiquitous Language. Es ist wichtig, zwischen der Idee einer Police im Überprüfungssubsystem und der Police im zentralen Workflow zu unterscheiden, da es sich dabei um verschiedene Dinge handelt. Obwohl sie die gleiche Identität haben können, unterscheiden sich die Wertobjekte und untergeordneten Entitäten (hierzu in Kürze mehr) häufig radikal. Da Sie innerhalb eines Kontexts modellieren, möchten Sie auch, dass die Sprache innerhalb dieses Kontexts für Präzision sorgt, damit Sie produktive Gespräche mit Fachgebietsexperten sowie innerhalb Ihrer Teams führen können.

Einige Bereiche innerhalb von Modellen werden näher aneinander gruppiert als andere. Module sind ein Mittel, mit dem diese Gruppen innerhalb eines bestimmten Kontexts organisiert werden können. Sie dienen als Minigrenzen, an denen Sie innehalten und über Zuordnungen zu anderen Modulen nachdenken sollten. Sie stellen zudem ein weiteres Organisationsverfahren dar, das Sie von „Small Balls of Mud“ (kleine Matschklumpen) wegführt. Technologisch gesehen, lassen sich Module auf einfache Weise erstellen. In Microsoft .NET Framework sind sie einfach Namespaces. Die Kunst der Identifizierung von Modulen erfordert aber, dass Sie etwas Zeit mit Ihrem Code verbringen. Schließlich sehen Sie vielleicht, wie sich einige Dinge als Minimodell innerhalb eines Modells herausbilden. Zu diesem Zeitpunkt können Sie die Unterteilung in Namespaces in Betracht ziehen.

Die Unterteilung von Modellen in zusammenhängende Module hat in Ihrer IDE einen nützlichen Effekt: Da Sie mehrere using-Anweisungen verwenden müssen, um Module explizit einzubeziehen, profitieren Sie von einer viel übersichtlicheren IntelliSense-Erfahrung. Gleichzeitig erhalten Sie eine Methode für die Untersuchung von Zuordnungen zwischen größeren konzeptionellen Abschnitten Ihres Systems mit einem statischen Analysetool wie NDepend.

Die Einführung einer organisatorischen Änderung in Ihrem Modell sollte Sie veranlassen, pragmatisch über Kosten und Nutzen nachzudenken. Wenn Sie Ihr Modell mithilfe von Modulen (oder Namespaces) unterteilen, sollten Sie sich wirklich fragen, ob Sie es mit einem separaten Kontext zu tun haben. Die Kosten für die Abgrenzung eines weiteren Kontexts sind in der Regel viel höher: Nun haben Sie zwei Modelle, wahrscheinlich in zwei Assemblys, die Sie mit Anwendungsdiensten, Controllern usw. verbinden müssen.

Anti-Corruption Layers

Eine Anti-Corruption Layer (Antibeschädigungsschicht, ACL) ist ein weiteres DDD-Muster, das Sie bestärkt, Gatekeeper zu erstellen, die verhindern, dass sich nicht domänenbezogene Konzepte in Ihr Modell einschleichen. Sie halten das Modell sauber.

Im Grunde sind Repositorys eigentlich eine Art von ACL. Sie halten SQL- oder ORM-Konstrukte (Object-Relational Mapping, objektrelationale Zuordnung) aus Ihrem Modell heraus.

ACLs sind ein hervorragendes Verfahren für die Einführung eines Elements, das von Michael Feathers in seinem Buch Working Effectively With Legacy Code als „Seam“ (Naht) bezeichnet wird. Eine Naht ist ein Bereich, in dem Sie mit der Entfernung von Legacycode und mit der Einführung von Änderungen beginnen können. Das Auffinden von Nähten kann in Verbindung mit der Isolierung Ihrer zentralen Domäne äußerst wertvoll sein, wenn Sie die hochwertigsten Teile Ihres Codes mithilfe von DDD-Verfahren umgestalten und straffen.

Beachten des geschäftlichen Nutzens

In den meisten Entwicklerbüros gibt es einige erfahrene Geschäftsleute und erstklassige Entwickler, die ein Problem isolieren und beschreiben sowie eine elegante und wartungsfreundliche objektorientierte Lösung erstellen können. Um Ihrem Kunden ein optimales Preis-Leistungs-Verhältnis zu bieten, sollten Sie unbedingt die zentrale Domäne Ihrer Anwendung verstehen. Die zentrale Domäne ist der Bounded Context, der für die Anwendung von DDD von größtem Nutzen ist.

In jedem Unternehmenssystem gibt es einige Bereiche, die wichtiger sind als andere. Die wichtigeren Bereiche decken sich in der Regel mit den Kernkompetenzen des Kunden. In einem Unternehmen wird selten benutzerdefinierte Finanzbuchhaltungssoftware eingesetzt. Wenn es sich bei diesem Unternehmen aber um eine Versicherung handelt (um bei meinem früheren Beispiel zu bleiben) und sein gewinnbringendes Angebot in der Verwaltung von Risikopools besteht, bei denen die Haftung auf alle Mitglieder verteilt ist, sollte es besser verdammt gut darin sein, hohe Risiken abzulehnen und Trends zu erkennen. Vielleicht haben Sie auch einen Kunden, der Krankenversicherungsansprüche bearbeitet und dessen Strategie darin besteht, seine Wettbewerber durch die Automatisierung von Zahlungen preislich zu unterbieten, um die Bemühungen seiner rechnungszahlenden Mitarbeiter zu verstärken.

Unabhängig von der Branche verfügt Ihr Arbeitgeber oder Kunde über einen bestimmten Marktvorteil, und auf diesen Marktvorteil konzentriert sich in der Regel benutzerdefinierte Software. In dieser benutzerdefinierten Software finden und modellieren Sie wahrscheinlich die zentrale Domäne.

Wir können unsere Investition in die Wertschöpfung in einer weiteren Dimension messen – nämlich dort, wo wir unser intellektuelles Kapital in das Erreichen technischer Spitzenleistungen investieren. Allzu oft sind leitende Entwickler die Art von Mensch, die eine Obsession für neue Technologien entwickelt. Dies ist in bestimmtem Umfang zu erwarten – die Branche bringt in einem unerbittlichen Tempo Innovationen hervor, und Anbieter sind gezwungen, häufig neue Technologieangebote auf den Markt zu bringen, um die Nachfrage ihrer Kunden zu befriedigen und konkurrenzfähig zu bleiben. Die Herausforderung, wie ich sie sehe, besteht für leitende Entwickler darin, die grundlegenden Prinzipien und Muster zu beherrschen, die zum Nutzen des Kernstücks eines Systems beitragen. Es ist verlockend, sich auf ein neues Framework oder auf eine neue Plattform zu stürzen, aber wir müssen uns daran erinnern, dass Anbieter diese Produkte entwickeln, damit wir einfach darauf vertrauen können, dass sie funktionieren.

Ein System einziger Verantwortungen

Ich habe bereits erwähnt, dass DDD eine Mustersprache für die Strukturierung reichhaltiger Domänenmodelle bietet. Durch die Implementierung dieser Muster erreichen Sie in bestimmtem Umfang automatisch die Einhaltung des Prinzips der einzigen Verantwortung, und das ist mit Sicherheit wertvoll.

Das Prinzip der einzigen Verantwortung regt dazu an, den zentralen Zweck einer Schnittstelle oder Klasse herauszuarbeiten. Es führt Sie in Richtung einer hohen Kohäsion – eine sehr gute Sache, da sich der Code dadurch einfacher finden, wiederverwenden und warten lässt.

DDD identifiziert bestimmte Arten von Klassenverantwortlichkeiten in einer zentralen Mustersammlung. Im Folgenden werden einige der wichtigeren Muster behandelt, aber es ist erwähnenswert, dass im Originalbuch von Eric Evans zahlreiche Muster beschrieben werden, die von der Klassenebene bis zur Architektur reichen. Zur Einführung bleibe ich bei der Klassenebene und gehe dabei auf Entitäten, Wertobjekte, Aggregatstämme, Domänendienste und Repositorys ein. Da es sich hierbei um eine Einführung handelt, wird nur die Verantwortung jedes Musters in Verbindung mit jeweils ein bis zwei Codebeispielen oder Tipps erläutert.

Entitäten haben eine Identität und einen Lebenszyklus

Eine Entität ist ein „Ding“ in Ihrem System. Es ist oft hilfreich, sich Entitäten in Form von Substantiven vorzustellen: Personen, Orte und, nun ja, eben Dinge.

Entitäten haben sowohl eine Identität als auch einen Lebenszyklus. Wenn ich beispielsweise auf eine spezielle Kundin in meinem System zugreifen möchte, kann ich anhand einer Nummer nach ihr fragen. Wenn ich einen Handelsauftrag abschließe, ist dieser für mein System erledigt und kann im Langzeitspeicher (dem Verlaufsberichtssystem) abgelegt werden.

Sie sollten sich Entitäten nicht als Dateneinheiten, sondern als Verhaltenseinheiten vorstellen. Versuchen Sie, Ihre Logik in die Entitäten einzufügen, zu denen sie gehört. Meistens gibt es eine Entität, die einen bestimmten Vorgang erhalten sollte, den Sie Ihrem Modell hinzuzufügen versuchen, oder eine neue Entität wartet geradezu darauf, erstellt oder extrahiert zu werden. In einfacherem Code finden Sie viele Dienst- oder Managerklassen, die Entitäten von außen überprüfen. Ich bevorzuge es im Allgemeinen, diese Überprüfung innerhalb der Entität vorzunehmen – Sie erhalten alle Vorteile, die mit dem grundlegenden Prinzip der Kapselung verbunden sind, und Ihre Entitäten werden dadurch verhaltensorientiert.

Einige Entwickler stören sich an Abhängigkeiten in ihren Entitäten. Offensichtlich müssen Sie Zuordnungen zwischen den verschiedenen Entitäten in Ihrem System erstellen. Beispielsweise müssen Sie unter Umständen eine Product-Entität aus der Policy-Entität abrufen, damit Sie sinnvolle Standardwerte für die Police bestimmen können. Problematisch scheint es zu werden, wenn Sie einen bestimmten externen Dienst benötigen, um ein einer Entität innewohnendes Verhalten durchzuführen.

Ich für meinen Teil störe mich nicht an dem Erfordernis, andere Nichtentitätsklassen einzubeziehen, und ich würde versuchen zu vermeiden, das zentrale Verhalten außerhalb meiner Entität zu bewerkstelligen. Sie sollten immer daran denken, dass Entitäten an sich Verhaltenseinheiten sind. Oft wird dieses Verhalten als eine Art Zustandsautomat implementiert – wenn Sie einen Befehl für eine Entität aufrufen, ist sie für die Änderung ihres internen Zustands verantwortlich – aber manchmal ist es notwendig, dass Sie zusätzliche Daten abrufen oder der Außenwelt Nebenwirkungen auferlegen. Mein bevorzugtes Verfahren hierfür besteht darin, der Befehlsmethode die Abhängigkeit zur Verfügung zu stellen:

 

public class Policy {
  public void Renew(IAuditNotifier notifier) {
    // do a bunch of internal state-related things,
    // some validation, etc.
    ...
    // now notify the audit system that there's
    // a new policy period that needs auditing
    notifier.ScheduleAuditFor(this);
  }
}

Der Vorteil dieses Ansatzes ist, dass Sie keinen IOC-Container (Inversion of Control, Steuerungsumkehrung) benötigen, der eine Entität für Sie erstellt. Ein anderer Ansatz, der meiner Meinung nach durchaus akzeptabel ist, würde darin bestehen, einen Service Locator zu verwenden, um IAuditNotifier innerhalb der Methode aufzulösen. Dieses Verfahren hat den Vorteil, dass die Schnittstelle sauber gehalten wird. Ich finde jedoch, dass ich durch die erstere Strategie viel mehr über meine Abhängigkeiten auf einer höheren Ebene erfahre.

Wertobjekte beschreiben Dinge

Wertobjekte sind Deskriptoren oder Eigenschaften, die in der Domäne, die Sie modellieren, wichtig sind. Im Unterschied zu Entitäten haben sie keine Identität, sondern beschreiben einfach die Dinge, die Identitäten haben. Ändern Sie eine Entität namens „Fünfunddreißig Dollar“, oder erhöhten Sie das Guthaben auf einem Konto?

Das Schöne an Wertobjekten besteht darin, dass sie die Eigenschaften von Entitäten auf viel elegantere und aufschlussreichere Weise beschreiben. „Money“, ein häufig verwendetes Wertobjekt, sieht als Funktionsparameter einer Geldüberweisungs-API viel besser als eine Dezimalzahl aus. Wenn Sie es in einer Schnittstelle oder Entitätsmethode entdecken, wissen Sie sofort, womit Sie es zu tun haben.

Wertobjekte sind unveränderlich. Nach ihrer Erstellung können sie nicht mehr geändert werden. Warum ist es wichtig, dass sie unveränderlich sind? Mit Wertobjekten streben Sie nebenwirkungsfreie Funktionen an, ein weiteres Konzept, das von DDD übernommen wurde. Wenn Sie 20 $ zu 20 $ hinzuaddieren, ändern Sie dann 20 $? Nein, Sie erstellen den neuen Money-Deskriptor „40 $“. In C# können Sie das Schlüsselwort „readonly“ für öffentliche Felder verwenden, um Unveränderlichkeit und nebenwirkungsfreie Funktionen zu erzwingen (siehe Abbildung 2).

Abbildung 2 Erzwingen von Unveränderlichkeit mithilfe von „readonly“

public class Money {
  public readonly Currency Currency;
  public readonly decimal Amount;

  public Money(decimal amount, Currency currency) {
    Amount = amount;
    Currency = currency;
  }

  public Money AddFunds(Money fundsToAdd) {
    // because the money we're adding might
    // be in a different currency, we'll service 
    // locate a money exchange Domain Service.
    var exchange = ServiceLocator.Find<IMoneyExchange>();
    var normalizedMoney = exchange.CurrentValueFor(fundsToAdd,         this.Currency);
    var newAmount = this.Amount + normalizedMoney.Amount;
    return new Money(newAmount, this.Currency);
  }
}

public enum Currency {
  USD,
  GBP,
  EUR,
  JPY
}

Aggregatstämme kombinieren Entitäten

Ein Aggregatstamm (aggregate root) ist eine spezielle Entitätsart, auf die Nutzer direkt verweisen. Durch die Angabe von Aggregatstämmen können Sie eine zu starke Kopplung der Objekte vermeiden, aus denen Ihr Modell besteht, indem Sie einige einfache Regeln festlegen. Sie sollten beachten, dass Aggregatstämme ihre Unterentitäten sorgfältig schützen.

Die wichtigste Regel, die Sie sich merken müssen, besteht darin, dass Aggregatstämme die einzige Entitätsart sind, auf die Ihre Software verweisen darf. Dies hilft dabei, den „Big Ball of Mud“ zu vermeiden, da jetzt eine Einschränkung vorhanden ist, die Sie daran hindert, ein eng gekoppeltes System zu erstellen, bei dem sämtliche Elemente eine Zuordnung zu allen anderen Elementen aufweisen.

Angenommen, Sie verfügen über eine Entität namens „Policy“ (Police). Da Policen auf jährlicher Basis verlängert werden, ist wahrscheinlich eine Entität namens „Period“ (Zeitraum) vorhanden. Da ein Zeitraum nicht ohne eine Police existieren kann und Sie über die Policy-Entität Aktionen für Zeiträume durchführen können, wird Policy als Aggregatstamm bezeichnet, und Period ist ein ihm untergeordnetes Element.

Ich mag es, wenn meine Aggregatstämme die benötigten Informationen für mich herausfinden. Betrachten Sie den folgenden Nutzercode, durch den auf einen Policy-Aggregatstamm zugegriffen wird:

Policy.CurrentPeriod().Renew()

Ich versuche, eine Versicherungspolice zu verlängern – erinnern Sie sich an das Klassendiagramm der zentralen Domäne der Versicherungspolicenverwaltung. Sehen Sie, wie ich mir mit dem Punktoperator den Weg zu dem Verhalten bahne, das ich aufrufen möchte?

Mit diesem Ansatz sind mehrere Probleme verbunden. Zunächst einmal verstoße ich eindeutig gegen das Gesetz von Demeter. Eine Methode M eines Objekts O sollte nur die Methoden der folgenden Arten von Objekten aufrufen: sie selbst, ihre Parameter, Objekte, die von ihr erstellt oder instanziiert werden, oder ihre direkten Komponentenobjekte.

Ist „Deep Dotting“ nicht irgendwie praktisch? IntelliSense ist eines der besten und nützlichsten Features von Visual Studio und anderen modernen IDEs, aber wenn Sie anfangen, sich per Dotting den Weg zu der Funktion zu bahnen, die Sie tatsächlich aufrufen möchten, führen Sie eine unnötige Kopplung in das System ein. Im obigen Beispiel ist jetzt eine Abhängigkeit von der Policy-Klasse und der Period-Klasse vorhanden.

Wenn Sie an weiterführender Literatur interessiert sind: Auf der Website von Brad Appleton ist ein hervorragender Artikel verfügbar, der ausführlichere Informationen zu den Implikationen, Theorien, Tools und Vorsichtsmaßnahmen in Bezug auf das Gesetz von Demeter enthält.

Das Klischee „Tod durch tausend Schnitte“ ist eine gute Beschreibung der potenziellen Probleme bei der Wartung eines zu stark gekoppelten Systems. Wenn Sie überall unnötige Verweise erstellen, erstellen Sie auch ein starres Modell, bei dem Änderungen an einer Stelle zu einer Flut von Änderungen im gesamten Nutzercode führen. Sie könnten das gleiche Ziel mit einem meiner Meinung nach viel ausdrucksstärkeren Codeabschnitt erreichen:

Policy.Renew()

Sehen Sie, wie das Aggregat es einfach löst? Intern kann es den aktuellen Zeitraum finden, feststellen, ob es bereits einen Verlängerungszeitraum gibt, und alle anderen erforderlichen Aktionen durchführen.

Wenn ich Komponententests für meine Aggregatstämme mit einem Verfahren wie der verhaltensgesteuerten Entwicklung (Behavior-Driven Development, BDD) durchführe, stelle ich in der Regel fest, dass meine Tests mehr zum Blackbox- und Zustandstestmodell tendieren. Aggregatstämme und Entitäten enden häufig als Zustandsautomaten, und das Verhalten stimmt entsprechend überein. Das Ergebnis sind Zustandsüberprüfung, Addition und Subtraktion. Im Beispiel für die Verlängerung in Abbildung 3 findet einiges an Verhalten statt, und es ist recht klar, wie Sie dies bei einem BDD-Test ausdrücken können.

Abbildung 3 Testen von Aggregatstämmen

public class 
  When_renewing_an_active_policy_that_needs_renewal {

  Policy ThePolicy;
  DateTime OriginalEndingOn;

  [SetUp]
  public void Context() {
    ThePolicy = new Policy(new DateTime(1/1/2009));
    var somePayroll = new CompanyPayroll();
    ThePolicy.Covers(somePayroll);
    ThePolicy.Write();
    OriginalEndingOn = ThePolicy.EndingOn;
  }

  [Test]
  public void Should_create_a_new_period() { 
    ThePolicy.EndingOn.ShouldEqual(OriginalEndingOn.AddYears(1));
  }
}

Domänendienste modellieren primäre Vorgänge

Manchmal verfügen Sie über Vorgänge oder Prozesse, die in Ihrer Domäne keine Identität oder keinen Lebenszyklus haben. Domänendienste bieten Ihnen ein Tool für die Modellierung dieser Konzepte. Sie sind in der Regel zustandslos und hoch kohäsiv und bieten häufig eine einzelne öffentliche Methode und manchmal eine Überladung für die Durchführung von Aktionen an Sätzen.

Es gibt mehrere Gründe dafür, dass ich gern Dienste verwende. Wenn an einem Verhalten mehrere Abhängigkeiten beteiligt sind und ich bei einer Entität keinen natürlichen Ort für die Platzierung dieses Verhaltens finden kann, verwende ich einen Dienst. Wenn in meiner Ubiquitous Language ein Prozess oder Vorgang als Konzept erster Ordnung bezeichnet wird, stelle ich in Frage, ob ein Dienst als zentrale Orchestrierungsstelle für das Modell sinnvoll ist.

Sie könnten im Fall der Policenverlängerung einen Domänendienst verwenden. Dies ist ein alternativer Stil. Statt einen IAuditNotifier direkt in die Methode der Renew-Methode der Policy-Entität zu injizieren, können Sie sich dafür entscheiden, einen Domänendienst zu extrahieren, der für Sie die Abhängigkeitsauflösung übernehmen soll. Es ist natürlicher, einen Domänendienst von einem IOC-Container als von einer Entität aus aufzulösen. Diese Strategie ist für mich viel sinnvoller, wenn es mehrere Abhängigkeiten gibt, aber ich stelle dennoch die Alternative vor.

Es folgt ein kurzes Beispiel für einen Domänendienst:

public class PolicyRenewalProcesor {
  private readonly IAuditNotifier _notifier;

  public PolicyRenewalProcessor(IAuditNotifier notifier) {
    _notifier = notifier;
  }
  public void Renew(Policy policy) {
    policy.Renew();
    _notifier.ScheduleAuditFor(policy);
  }
}

Das Wort „Dienst“ ist in der Entwicklerwelt ein hochgradig überladener Begriff. Manchmal denke ich an das Konzept des „Service“, wie es bei der serviceorientierten Architektur (SOA) verwendet wird. Zu anderen Zeiten stelle ich mir Dienste wie kleine Klassen vor, die keine bestimmten Personen, Orte oder Dinge in meiner Anwendung darstellen, aber in der Regel Prozesse verkörpern. Domänendienste gehören meist zur letzteren Kategorie. Sie werden in der Regel nach Verben oder Geschäftsaktivitäten benannt, die von Fachgebietsexperten in die Ubiquitous Language eingeführt werden.

Anwendungsdienste stellen hingegen eine hervorragende Möglichkeit für die Einführung einer geschichteten Architektur dar. Sie können dafür verwendet werden, Daten innerhalb des Domänenmodells einer für eine Clientanwendung erforderlichen Form zuzuordnen. Beispiel: Vielleicht müssen Sie tabellarische Daten in einem DataGrid anzeigen, möchten aber ein detailliertes und verzweigtes Objektdiagramm in Ihrem Modell beibehalten.

Anwendungsdienste sind auch recht nützlich für die Integration mehrerer Modelle – beispielsweise für die Übersetzung zwischen der Policenüberprüfung und dem zentralen Policenworkflow. Ich verwende sie auch, um Infrastrukturabhängigkeiten einzubeziehen. Stellen Sie sich ein gängiges Szenario vor, bei dem Sie Ihr Domänenmodell mit Windows Communication Foundation (WCF) verfügbar machen möchten. Ich würde hierzu einen mit den WCF-Attributen versehenen Anwendungsdienst verwenden, statt zuzulassen, dass sich WCF in mein sauberes Domänenmodell einschleicht.

Anwendungsdienste sind in der Regel sehr breit und flach angelegt und verkörpern zusammenhängende Funktionalität. Sie können die Schnittstelle und die partielle Implementierung, die Sie im Code in Abbildung 4 sehen, als ein gutes Beispiel für einen Anwendungsdienst betrachten.

Abbildung 4 Ein einfacher Anwendungsdienst

public IPolicyService {
  void Renew(PolicyRenewalDTO renewal);
  void Terminate(PolicyTerminationDTO termination);
  void Write(QuoteDTO quote);
}

public PolicyService : Service {
  private readonly ILogger _logger;
  public PolicyService(ILogger logger, IPolicyRepository policies) {
    _logger = logger;
    _policies = policies;
  }

  public void Renew(PolicyRenewalDTO renewal) {
    var policy = _policies.Find(renewal.PolicyID);
    policy.Renew();
    var logMessage = string.Format(
      "Policy {0} was successfully renewed by {1}.", 
      Policy.Number, renewal.RequestedBy);
    _logger.Log(logMessage);
  }
}

Repositorys dienen zum Speichern und Verteilen von Aggregatstämmen

Woher rufen Sie Entitäten ab? Wie speichern Sie sie? Diese Fragen werden durch das Repository-Muster beantwortet. Repositorys stellen eine speicherinterne Auflistung dar, und meist wird davon ausgegangen, dass Sie am Ende über ein Repository pro Aggregatstamm verfügen.

Repositorys eignen sich gut als übergeordnete Klasse oder für das, was von Martin Fowler als „Layer Supertype“-Muster bezeichnet wird. Die Verwendung von Generika zur Ableitung einer Repositorybasisschnittstelle aus dem vorhergehenden Beispiel ist eine einfache Angelegenheit:

public interface IRepository<T>
  where T : IEntity
{
  Find<T>(int id);
  Find<T>(Query<T> query);
  Save(T entity);
  Delete(T entity);
}

 

Repositorys verhindern, dass Datenbank- oder Persistenzkonzepte wie SQL-Anweisungen oder gespeicherte Prozeduren mit Ihrem Modell vermischt werden und von der eigentlichen Aufgabe ablenken: dem Erfassen der Domäne. Diese Trennung des Modellcodes von der Infrastruktur ist eine gute Eigenschaft. Eine ausführlichere Erläuterung finden Sie in der Randleiste „Anti-Corruption Layers“.

Jetzt haben Sie wahrscheinlich bemerkt, dass ich nicht darauf eingehe, wie Aggregatstämme, ihre untergeordneten Entitäten und die zugehörigen Werteobjekte tatsächlich persistent auf dem Datenträger gespeichert werden. Dies ist so beabsichtigt. Das Speichern der Daten, die für ein Verhalten in Ihrem Modell benötigt werden, ist ein Aspekt, der sich zum Modell selbst orthogonal verhält. Die Persistenz gehört zur Infrastruktur.

Ein Überblick über diese Technologien ginge weit über eine Einführung in DDD hinaus. An dieser Stelle soll nur erwähnt werden, dass es eine Reihe geeigneter und ausgereifter Optionen für das Speichern der Daten Ihres Modells gibt, von ORM-Frameworks (Object-Relational Mapping, objektrelationale Zuordnung) über dokumentorientierte Datenbanken bis hin zu benutzerdefinierten Datenmappern für einfache Szenarios.

DDD-Ressourcen

Dan North zur Strukturierung von Benutzergeschichten

Der „Big Ball of Mud“-Architekturstil

Blog von Greg Young auf CodeBetter

Abhandlung von Robert C. Martin zum Prinzip der einzigen Verantwortung

Brad Appleton zum Gesetz von Demeter

Martin Fowler beschreibt das Layer Supertype-Muster

Robert C. Martin zu den S.O.L.I.D.-Prinzipien

Datenbanken

Ich bin sicher, dass Sie bereits an dem Punkt angelangt sind, an dem Sie gedacht haben: „Das ist ja alles schön und gut, Dave. Aber wo speichere ich meine Entitäten?“ Ja, Sie müssen sich mit diesem wichtigen Detail befassen, aber wie oder wo Sie Ihre Modelle persistent speichern, ist für einen Überblick über DDD nur von nebensächlicher Bedeutung.

Einige Entwickler oder Datenbankadministratoren werden annehmen, dass die Datenbank das Modell ist. In vielen Fällen trifft dies teilweise zu. Datenbanken können, wenn sie mit einem Diagrammerstellungstool stark normalisiert und visualisiert werden, viel über die Informationen und Beziehungen in Ihrer Domäne aussagen.

Die Datenmodellierung als primäres Verfahren lässt jedoch einiges zu wünschen übrig. Wenn Sie die derselben Domäne innewohnenden Verhaltensweisen verstehen möchten, versagen nur auf Daten gestützte Verfahren wie Entitätsbeziehungsdiagramme oder -modelle sowie Klassendiagramme. Sie müssen die Teile der Anwendung in Aktion sehen sowie betrachten können, wie Typen zur Erledigung von Aufgaben zusammenarbeiten.

Ich ziehe bei der Modellierung häufig Sequenzdiagramme auf einem Whiteboard als Kommunikationstool heran. Durch diesen Ansatz wird das Wesentliche für die Vermittlung eines Verhaltensentwurfs oder -problems ohne die Komplexitäten von UML (Unified Modeling Language) oder der modellgesteuerten Architektur erfasst. Ich vermeide gern unnötige Umstände, insbesondere wenn ich die Diagramme auf dem Whiteboard wieder löschen werde. In meinen Diagrammen kümmere ich mich nicht um die hundertprozentige Einhaltung der UML-Konventionen, sondern bevorzuge stattdessen einfache Felder, Pfeile und Verantwortlichkeitsbereiche, die ich schnell skizzieren kann.

Wenn Sie in Ihrem Team noch keine Sequenzdiagramme verwenden, empfehle ich Ihnen dringend, sich mit diesem Verfahren vertraut zu machen. Ich habe beobachten können, wie positiv sie Teammitglieder beeinflussen und sie veranlassen, Probleme in Bezug auf das Prinzip der einzigen Verantwortung, die geschichtete Architektur, den Verhaltensentwurf über Datenmodellierung und die Architektur im Allgemeinen zu durchdenken.

Erste Schritte mit DDD

Die Einarbeitung in die objektorientierte Programmierung ist kein leichtes Unterfangen. Ich glaube, dass das Erlangen von Kompetenz auf diesem Gebiet innerhalb der Reichweite der meisten professionellen Entwickler liegt, aber es erfordert Hingabe, das Studieren einschlägiger Literatur und Übung, Übung, Übung. Es ist auch hilfreich, eine Haltung anzunehmen, die auf handwerkliches Können und kontinuierliches Lernen abzielt.

Wie fangen Sie am besten an? Einfach gesagt: Machen Sie Ihre Hausaufgaben. Informieren Sie sich über Themen wie die S.O.L.I.D.-Prinzipien, und lesen Sie das Buch von Eric Evans. Die investierte Zeit wird sich mehr als bezahlt machen. InfoQ hat eine kürzere Version des DDD-Buchs herausgebracht, in dem einige Hauptkonzepte vorgestellt werden. Wenn Ihr finanzielles Budget oder Ihre Zeit knapp bemessen ist oder Sie einfach neugierig sind, empfiehlt es sich, damit anzufangen. Sobald Sie über solide Kenntnisse verfügen, sollten Sie die DDD-Gruppe von Yahoo! besuchen, um zu erfahren, mit welchen Problemen Ihre Designerkollegen zu kämpfen haben, und um sich an der Diskussion zu beteiligen.

DDD ist keine neue Doktrin oder Methodik, sondern eine Sammlung bewährter Strategien. Wenn Sie für die Praxis bereit sind, versuchen Sie, die Philosophien, Verfahren und Muster anzupassen, die für Ihre Situation am sinnvollsten sind. Einige Elemente von DDD haben einen universelleren Geltungsbereich als andere. Das Verständnis des Grunds für die Existenz Ihrer zentralen Domäne durch Aufdecken und Verwenden der Ubiquitous Language und Identifizieren der Kontexte, in denen Sie modellieren, ist weitaus wichtiger als eine nicht transparente und universelle Repositorylösung.

Achten Sie beim Entwerfen Ihrer Lösungen besonders auf den praktischen Nutzen. Wenn Designer Kunst hervorbringen und Entwickler eine Art Designer sind, muss unser Medium der geschäftliche Nutzen sein. Der Nutzen hat Vorrang vor anderen Überlegungen wie der Einhaltung von Dogmen und der Auswahl der Persistenztechnologie – so wichtig diese Entscheidungen manchmal auch erscheinen.

Dave Laribee betreut das Produktentwicklungsteam von VersionOne. Er hält regelmäßig Vorträge auf regionalen und nationalen Entwicklerveranstaltungen und wurde für 2007 und 2008 zum Microsoft MVP für Architektur ernannt. Dave Laribee schreibt zudem Beiträge für das CodeBetter-Blognetzwerk unter thebeelog.com.