Datenpunkte

Codierung für Domain-Driven Design: Tipps für Entwickler mit Datenschwerpunkt

Julie Lerman

Julie Lerman
In diesem Jahr feiern wir das 10-jährige Jubiläum von „Domain-Driven Design: Tackling Complexity in the Heart of Software“ (Addison-Wesley Professional, 2003, amzn.to/ffL1k), dem bahnbrechenden Softwaredesignbuch von Eric Evans. In dieses Buch hat Evans die langjährige Erfahrung einfließen lassen, die er sammeln konnte, als er Großunternehmen in Sachen Softwareentwicklung Orientierung gab. Er beschäftigte sich dann weitere Jahre damit herauszufinden, wie sich die Muster, die bei erfolgreichen Projekten festzustellen sind, zusammenfassen lassen. Dabei arbeitete er eng mit dem Kunden zusammen, analysierte die geschäftlichen Probleme, die gelöst werden mussten, stellte Teams zusammen und kümmerte sich um die Softwarearchitektur. Der Mittelpunkt dieser Muster ist die Domäne des Unternehmens, und aus diesen beiden Komponenten besteht der domänengesteuerte Entwurf (Domain-Driven Design, DDD). Mit DDD wird die jeweilige Domäne modelliert. Die Muster sind das Ergebnis dieser Abstraktion Ihrer Kenntnisse über die Domäne. Wenn Sie das Vorwort von Martin Fowler und die Einleitung von Eric Evans noch einmal durchlesen, so bieten sie auch heute noch einen informativen Einblick in die Essenz von DDD.

In diesem und in den nächsten beiden Artikeln werde ich einige Tipps geben, die für mich Klarheit schufen, obwohl ich mich auf Daten und Entity Framework konzentriere. Dabei soll mein Code von einigen technischen DDD-Mustern profitieren.

Warum ich mich überhaupt mit DDD beschäftige?

Ich wurde durch ein kurzes Videointerview mit Jimmy Nilsson, das ich auf InfoQ.com fand, auf DDD aufmerksam. Als angesehener Architekt unter anderem in der .NET-Community sprach er über LINQ to SQL und über Entity Framework (bit.ly/11DdZue). Gegen Ende wurde Nilsson nach seinem Lieblingsbuch aus diesem Bereich gefragt. Seine Antwort: „Das für mich beste Computerbuch ist ‚Domain-Driven Design‘ von Eric Evans. Es liest sich wie Poesie. Nicht nur der Inhalt ist hervorragend, sondern es liest sich auch so gut, dass ich es immer wieder in die Hand nehme.“ Poesie! Ich schrieb damals gerade an meinem ersten technischen Buch, nämlich an „Programming Entity Framework“ (O’Reilly Media, 2009), und war von dieser Beschreibung fasziniert. Daher warf ich einen Blick in Evans Buch, um mir selbst ein Bild davon zu machen. Evans hat einen schönen, flüssigen Schreibstil. Und zusammen mit seiner scharfsinnigen, lebensnahen Einstellung zur Softwareentwicklung entstand ein Buch, das ein reiner Lesegenuss ist. Aber auch der Inhalt überraschte mich. Abgesehen vom Schreibstil verblüffte mich auch seine Sichtweise. Er sprach darüber, eine Kundenbeziehung aufzubauen sowie den jeweiligen Geschäftsbereich und die damit verbundenen Softwareprobleme genau zu verstehen. Es geht also nicht nur darum, Code abzuliefern. Dies war für mich in den 25 Jahren, die ich bereits in der Softwareentwicklung tätig bin, stets wichtig. Ich beschloss, das Thema weiterzuverfolgen.

Ein paar Jahre lang schlich ich mich auf Zehenspitzen um das Thema DDD herum und sammelte dann weitere Informationen. Ich traf Evans auf einer Konferenz und nahm später an seinem 4-tägigen Intensiv-Workshop teil. Zwar bin ich bei Weitem kein DDD-Experte, doch fand ich, dass ich das Muster des gebundenen Kontexts gleich einsetzen konnte. Schließlich war ich gerade dabei, meine eigene Softwareerstellung so zu verändern, dass sie eine besser organisierte, überschaubarere Struktur erhält. In meinem Artikel von Januar 2013, „Verkleinern von EF-Modellen mithilfe von gebundenen DDD-Kontexten“ (msdn.microsoft.com/magazine/jj883952), finden Sie hierzu weitere Informationen.

Inzwischen habe ich mein Wissen aber vertieft. DDD fasziniert und inspiriert mich. Weil ich mich aber an erster Stelle auf Daten konzentriere, fällt es mir schwer, einige der technischen Muster zu verstehen, durch die DDD erfolgreich ist. Dieses Problem werde ich wohl mit vielen Entwicklern teilen, weshalb ich hier einige Dinge weitergeben möchte, die ich mithilfe von Evans und einer Reihe anderer Praktiker und Trainer aus dem DDD-Bereich – wie Paul Rayner, Vaughn Vernon, Greg Young, Cesar de la Torre und Yves Reynhout – lernen konnte. Sie standen mir nicht nur mit Rat und Tat zur Seite, sondern zeigten sich auch interessiert und großzügig.

Persistenz spielt beim Modellieren der Domäne keine Rolle

Beim Modellieren der Domäne geht es einzig und allein um die Aufgaben, die im Unternehmen durchzuführen ist. Wenn ich Typen und deren Eigenschaften und Verhaltensweisen entwerfe, neige ich stark dazu, mir Gedanken darüber zu machen, wie eine Beziehung in der Datenbank funktionieren wird und wie das von mir gewählte, objektbezogene Mapping-Framework (ORM), Entity Framework, mit den Eigenschaften, Beziehungen und Vererbungshierarchien umgehen wird, die ich erstelle. Wenn Sie nicht gerade Software für ein Unternehmen erstellen, das im Bereich der Datenspeicherung und -abfrage tätig ist (z. B. Dropbox), spielt Datenpersistenz für die Anwendung nur eine untergeordnete Rolle. Es ist, als ob Sie eine Quell-API für das Wetter abrufen, damit einem Benutzer die aktuelle Temperatur angezeigt wird. Oder als ob man Daten von Ihrer App an einen externen Dienst sendet, zum Beispiel für die Registrierung auf Meetup.com. Selbstverständlich kann es sein, dass Ihre Daten komplizierter sind, doch mit einem DDD-Ansatz der gebundenen Kontexte, durch den bei der Typerstellung der Schwerpunkt auf Verhaltensweisen und Einhaltung von DDD-Richtlinien liegt, kann die Persistenz im Vergleich zu den Systemen, die Sie vielleicht zurzeit erstellen, wesentlich schlichter ausfallen.

Und wenn Sie sich mit ORM beschäftigt und dabei zum Beispiel gelernt haben, wie Datenbankzuordnungen mit der Entity Framework-Fluent-API konfiguriert werden, sollten Sie in der Lage sein, die gewünschte Persistenz herzustellen. Im schlimmsten Fall müssen Sie vielleicht an den Klassen ein paar Änderungen vornehmen. In Extremfällen, zum Beispiel bei einer Legacydatenbank, können Sie sogar ein für die Datenbankzuordnung entworfenes Persistenzmodell einfügen und dann ein Tool wie AutoMapper zur Harmonisierung zwischen dem Domänenmodell und dem Persistenzmodell verwenden.

Diese Fragen haben mit dem geschäftlichen Problem, das Ihre Software lösen soll, jedoch nichts zu tun. Persistenz sollte daher nicht das Domänendesign beeinträchtigen. Dies ist für mich eine echte Herausforderung, denn beim Entwerfen meiner Entitäten muss ich daran denken, wie EF die Datenbankzuordnungen beeinträchtigt. Ich versuche daher, diese Gedanken aus meinem Kopf zu verbannen.

Private Setter und öffentliche Methoden

Eine weitere Faustregel ist, private Eigenschaftensetter zu verwenden. Anstatt es zuzulassen, dass aufrufender Code unterschiedliche Eigenschaften willkürlich festlegt, sollten Sie die Interaktion mit DDD-Objekten und deren Daten anhand von Methoden steuern, bei denen die Eigenschaften geändert werden. Und, nein, damit meine ich nicht Methoden wie „SetFirstName“ oder „SetLastName“. Anstatt beispielsweise einen neuen Customer-Typ zu instanziieren und dann die Eigenschaften einzeln festzulegen, haben Sie beim Erstellen eines neuen Kunden vielleicht ein paar Regeln zu beachten. Sie können diese Regeln in den Customer-Konstruktor integrieren, eine Factory Pattern-Methode nutzen oder sogar eine Create-Methode im Customer-Typ verwenden. Abbildung 1 zeigt einen Customer-Typ, der gemäß dem DDD-Muster eines Aggregatstamms definiert wird (also einem Objektdiagramm übergeordnet, was bei DDD auch als „Stammentität“ bezeichnet wird). Customer-Eigenschaften haben private Setter, damit nur andere Member der Customer-Klasse diese Eigenschaften direkt beeinflussen können. Die Klasse macht einen Konstruktor verfügbar, um ihre Instanziierung zu steuern, und blendet den parameterlosen Konstruktor (von Entity Framework benötigt) als intern aus.

Abbildung 1: Eigenschaften und Methoden eines Typs, der als Aggregatstamm fungiert

public class Customer : Contact
{
  public Customer(string firstName,string lastName, string email)
  { ... }
  internal Customer(){ ... }
  public void CopyBillingAddressToShippingAddress(){ ... }    
  public void CreateNewShippingAddress(
   string street, string city, string zip) { ... }
  public void CreateBillingInformation(
   string street, string city, string zip,
   string creditcardNumber, string bankName){ ... }    
  public void SetCustomerContactDetails(
   string email, string phone, string companyName){ ... }
  public string SalesPersonId { get; private set; }
  public CustomerStatus Status{get;private set;}
  public Address ShippingAddress { get; private set; }
  public Address BillingAddress { get;private set; }
  public CustomerCreditCard CreditCard { get; private set; }
}

Der Customer-Typ steuert und schützt die anderen Aggregatsentitäten (einige Adressen und ein Kreditkartentyp), indem bestimmte Methoden, wie „CopyBillingAddressToShippingAddress“, preisgegeben werden, mit denen diese Objekte erstellt und manipuliert werden. Der Aggregatstamm muss gewährleisten, dass die Regeln, durch die die einzelnen Entitäten innerhalb des Aggregats definiert werden, so angewendet werden, dass Domänenlogik und Verhalten in diesen Methoden implementiert werden. Und ganz wichtig: Der Aggregatstamm ist im ganzen Aggregat für invariante Logik und Konsistenz zuständig. In meinem nächsten Artikel werden ich näher auf Invarianten eingehen, an dieser Stelle möchte ich aber folgenden Blogbeitrag von Jimmy Bogard empfehlen: „Strengthening Your Domain: Aggregate Construction“ unter bit.ly/ewNZ52. Dort finden Sie eine hervorragende Erklärung von Invarianten in Aggregaten.

Letztendlich gibt Customer Verhalten preis und nicht Eigenschaften wie CopyBillingAddressToShippingAddress, CreateNewShipping­Address, CreateBillingInformation und SetCustomerContactDetails.

Denken Sie daran, dass sich der Contact-Typ, auf den Customer zurückzuführen ist, in einer anderen Assembly (Common) befindet, weil er möglicherweise von anderen Klassen benötigt wird. Ich muss die Eigenschaften von Contact verbergen, sie können aber nicht privat sein, weil Customer sonst nicht darauf zugreifen könnte. Stattdessen werden sie auf „Geschützt“ gesetzt.

public class Contact: Identity
{
  public string CompanyName { get; protected set; }
  public string EmailAddress { get; protected set; }
  public string Phone { get; protected set; }
}

Ein Kommentar über Identitäten am Rande: Customer und Contact können wie DDD-Wertobjekte aussehen, weil sie keinen Schlüsselwert haben. Bei meiner Lösung wird der Schlüsselwert jedoch durch die Identity-Klasse bereitgestellt, die auf Contact zurückzuführen ist. Und keiner dieser Typen ist unveränderlich, weshalb sie auch nicht als Wertobjekte betrachtet werden können.

Da Customer von Contact erbt, kann es auf diese geschützten Eigenschaften zugreifen und diese festlegen. Wie in dieser SetCustomerContactDetails-Methode:

public void SetCustomerContactDetails (string email, string phone, string companyName)

{

  EmailAddress = email;

  Phone = phone;

  CompanyName = companyName;

}

Manchmal genügt CRUD

Nicht alles in Ihrer App muss mit DDD erstellt werden. DDD ist dazu da, mit komplexen Verhaltensweisen umzugehen. Wenn Sie nur etwas grob und aufs Geratewohl bearbeiten oder abfragen müssen, genügt eine einfache Klasse (oder ein Klassensatz), der genauso definiert wird, wie Sie gewöhnlich mit „EF Code First“ vorgehen (mit Eigenschaften und Beziehungen), und mit Methoden zum Einfügen, Aktualisieren und Löschen kombiniert wird (über ein Repository oder einfach DbContext). Wenn Sie zum Beispiel eine Bestellung und die entsprechenden Auftragspositionen erstellen möchten, könnte es Ihnen mit DDD leichter fallen, bestimmte geschäftliche Regeln und Verhaltensweisen durchzugehen. Handelt es sich etwa um einen Gold Star-Kunden, der die Bestellung aufgibt? In diesem Fall benötigen Sie einige Kundendaten, um entscheiden zu können, ob diese Frage mit „Ja“ zu beantworten ist. Ist dem so, berechnen Sie für jeden Posten, der dieser Bestellung hinzugefügt wird, einen Rabatt von 10 Prozent. Hat der Benutzer seine Kreditkartendaten angegeben? Dann müssen Sie sich vielleicht an eine Bestätigungsstelle wenden, um zu gewährleisten, dass es sich um eine gültige Kreditkarte handelt.

Am wichtigsten ist es bei DDD, dass die Domänenlogik als Methoden innerhalb der Entitätsklassen der Domäne enthalten ist. So wird von OOP profitiert, anstatt „Transaktionsskripte“ in statusfreie Geschäftsobjekte zu implementierten, wie dies bei einer typischen Code First-Klasse der Demoware der Fall ist. 

Aber manchmal muss nur etwas ganz Normales erledigt werden, zum Beispiel ein Kontakteintrag erstellt, der „Name“, „Adresse“, „Empfohlen von“ usw. aufweist und dann gespeichert wird. Hier geht es nur um Erstellen, Lesen, Aktualisieren und Löschen, also um CRUD, das für die englischen Begriffe „create“, „read“, „update“ und „delete“ steht. Sie müssen hierzu weder Aggregate noch Stämme oder Verhaltensweisen erstellen.

Wahrscheinlich wird Ihre Anwendung eine Kombination aus komplexen Verhaltensweisen und schlichtem CRUD sein. Nehmen Sie sich die Zeit, die Verhaltensweisen zu klären, aber verschwenden Sie keine Zeit, keine Energie und kein Geld damit, die Teile Ihrer App, die wirklich schlicht sind, übermäßig komplex zu gestalten. In diesen Fällen müssen zwischen unterschiedlichen Subsystemen oder gebundenen Kontexten Grenzen gezogen werden. Ein gebundener Kontext könnte durchaus datengesteuert sein (nur CRUD), wohingegen ein wichtiger, durch Kern und Domäne gebundener Kontext unter Verwendung von DDD-Ansätzen entworfen werden sollte.

Freigegebene Daten ... für komplexe Systeme manchmal ein Fluch

Ein anderes Thema, über das ich mir den Kopf zerbrochen und dann geschimpft und gejammert habe, als netterweise versucht wurde, mir dies genauer zu erklären, ist die Freigabe von Typen und Daten innerhalb von Subsystemen. Mir wurde klar, dass ich nicht alles gleichzeitig haben kann. Ich musste mir also erneut über die Annahme Gedanken machen, dass Typen innerhalb von Systemen unbedingt freigegeben werden müssen und all diese Typen mit derselben Tabelle einer Datenbank interagieren müssen.

Ich werde besser darin zu entscheiden, wann Daten freigegeben werden müssen, und konzentriere mich auf bestimmte Problembereiche. Manche Dinge sind es vielleicht gar nicht wert, probiert zu werden. Zum Beispiel die Zuordnung aus unterschiedlichen Kontexten zu einer einzigen Tabelle oder gar einer einzigen Datenbank. Das am häufigsten auftretende Beispiel ist die Freigabe eines Kontakts, durch den systemübergreifend alle Anforderungen erfüllt werden sollen. Wie führen Sie die Quellcodeverwaltung zusammen und nutzen diese für einen Contact-Typ, der vielleicht in zahlreichen Systemen benötigt wird? Was geschieht, wenn die Definition dieses Contact-Typs von einem System geändert werden muss? Und in Bezug auf ein ORM: Wie ordnen Sie einen Kontakt, der systemübergreifend genutzt wird, einer einzigen Tabelle in einer einzigen Datenbank zu?

DDD führt Sie von der Freigabe von Domänenmodellen und Daten weg, indem Ihnen vermittelt wird, dass nicht immer auf dieselbe Person-Tabelle in einer einzigen Datenbank verwiesen werden muss.

Was mich am stärksten hiervon abhielt, war, dass ich mich seit 25 Jahren auf die Vorteile der erneuten Nutzung von Code und Daten konzentrieren. Hiermit hatte ich daher so meine Probleme, ich gewöhne mich aber langsam an den Gedanken, dass es kein Verbrechen ist, Daten zu duplizieren. Natürlich werden nicht alle Daten in dieses für mich neue Paradigma passen. Aber wie sieht es mit etwas Einfachem aus, zum Beispiel dem Namen einer Person? Wenn ich also den Vor- und Nachnamen einer Person in mehreren Tabellen oder gar in mehreren Datenbanken, die für unterschiedliche Subsysteme der Softwarelösung bestimmt sind, dupliziere? Langfristig gesehen fällt die Systemerstellung wesentlich leichter, wenn Sie sich die Komplexität der Datenfreigabe sparen. Auf alle Fälle muss die Daten- und Attributduplizierung in verschiedenen gebundenen Kontexten stets auf ein Minimum beschränkt werden. Manchmal benötigen Sie zum Berechnen von Rabatten in einem preisgebundenen Kontext nur die ID und den Status des Kunden. Der Vor- und Nachname dieses Kunden ist vielleicht nur in einem durch Contact Management gebundenen Kontext erforderlich.

Dennoch müssen noch viele Informationen für verschiedene Systeme zugänglich sein. Sie können von etwas profitieren, das bei DDD „Antibeschädigungsschicht“ genannt wird. Dabei kann es sich um etwas so Banales handelt wie um einen Dienst oder eine Nachrichtenwarteschlange. Dadurch wird beispielsweise gewährleistet, dass, wenn jemand in einem System einen neuen Kontakt erstellt, entweder erkannt wird, dass diese Person bereits an anderer Stelle vorhanden ist, oder die Person zusammen mit einem gemeinsamen Identitätsschlüssel in einem anderen Subsystem erstellt wird.

Viele Denkanstöße bis zum nächsten Monat

Während ich mich mit den technischen Aspekten von Domain-Driven Design beschäftigt und versucht habe, alte Gewohnheiten mit neuen Ideen in Einklang zu bringen, und dabei auch oft Aha-Erlebnisse hatte, konnte ich durch die Tipps, die ich in diesem Artikel besprochen habe, mehr Licht als Dunkelheit sehen. Manchmal kommt es nur auf die Perspektive an. Und die Perspektive, die ich hier dargestellt habe, half mir, genauer zu verstehen, um was es geht.

In meinem nächsten Artikel werde ich Sie an einigen meiner Aha-Erlebnisse teilhaben lassen. Dabei geht es um einen herablassenden Begriff, den Sie vielleicht schon einmal gehört haben: „anämisches Domänenmodell“. Aber auch sein DDD-Cousin, das „reichhaltige Domänenmodell“, unidirektionale Beziehungen und was zu erwarten ist, wenn bei Verwendung von Entity Framework Datenpersistenz hinzugefügt werden muss, wird besprochen. Darüber hinaus werde ich auf einige weitere, für mich wichtige DDD-Themen kurz eingehen, damit Ihre Lernkurve schneller ansteigt.

In der Zwischenzeit können Sie sich Ihre eigenen Klassen genauer ansehen und überlegen, wie Sie zu einem stärkeren Kontrollfreak werden, indem Sie die Eigenschaftensetter verbergen und deskriptivere und explizitere Methoden preisgeben. Und vergessen Sie eines nicht: SetLastName-Methoden sind nicht erlaubt! Schummeln gilt nicht.

Julie Lerman ist Microsoft MVP, .NET-Mentor und Unternehmensberaterin und lebt in den Bergen von Vermont. Sie hält weltweit in Benutzergruppen und bei Konferenzen Vorträge zum Thema „Datenzugriff“ und zu anderen Microsoft .NET-Themen. Julie Lerman führt unter thedatafarm.com/blog einen Blog. Sie ist die Autorin von „Programming Entity Framework“ (2010) sowie der Ausgaben „Code First“ (2011) und „DbContext“ (2012). Alle Ausgaben sind im Verlag O’Reilly Media erschienen. Folgen Sie ihr auf Twitter unter twitter.com/julielerman, und besuchen Sie ihre Pluralsight-Kurse unter juliel.me/PS-Videos.

Unser Dank gilt dem folgenden technischen Experten für die Durchsicht dieses Artikels: Cesar de la Torre (Microsoft)