Datenpunkte

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

Julie Lerman

Julie LermanAuch in diesem Monat wird weiterhin das 10-jährige Jubiläum des Buchs „Domain-Driven Design: Tackling Complexity in the Heart of Software“ (Addison-Wesley Professional, 2003) von Eric Evans gefeiert. Ich stelle zusätzliche Tipps für datenorientierte Entwickler vor, die gerne einige der Codierungsmuster von DDD (Domain-Driven Design) nutzen möchten. Im Artikel im letzten Monat standen folgende Aspekte im Mittelpunkt:

  • Ignorieren der Persistenz beim Modellieren der Domäne
  • Verfügbarmachen von Methoden zum Steuern von Entitäten und Aggregaten (und nicht von Eigenschaftensettern)
  • Erkennen, dass bestimmte Subsysteme perfekt für einfache CRUD-Vorgänge (Create, Read, Update, Delete – Erstellen, Lesen, Aktualisieren und Löschen) geeignet sind und keine Domänenmodellierung erfordern
  • Warum Daten oder Typen nicht in gebundenen Kontexten freigegeben werden sollten

In diesem Artikel werden „anämische“ und „umfassende“ Domänenmodelle beschrieben und das Konzept der Wertobjekte erläutert. Wenn es um Wertobjekte geht, gehören die meisten Entwickler einem von zwei Lagern an. Entweder finden sie dieses Thema so einleuchtend, dass sie nicht verstehen, warum es überhaupt erläutert wird, oder es ist ihnen ein solches Rätsel, dass sie es in der Vergangenheit nie wirklich verstanden haben. Und ich lege Ihnen ans Herz, in bestimmten Szenarien Wertobjekte anstelle von verbundenen Objekten zu verwenden.

Ein herablassender Begriff: anämische Domänenmodelle

Diese Bezeichnungen hören Sie oft, wenn es um die Definition von Klassen in DDD geht – anämische und umfassende (oder reichhaltige) Domänenmodelle. In DDD bezieht sich ein Domänenmodell auf eine Klasse. Umfassende Domänenmodelle stehen im Einklang mit DDD, es handelt sich um Klassen (oder Typen), die anhand von Verhaltensweisen und nicht nur über Getter und Setter definiert werden. Ein anämisches Domänenmodell besteht hingegen nur aus Gettern und Settern (und möglicherweise ein paar einfachen Methoden). Das reicht in vielen Szenarien aus, aber DDD bringt in diesem Fall keinen Nutzen.

Als ich anfing, Entity Framework (EF) mit Code First zu verwenden, waren etwa 99 Prozent der Klassen, mit denen Code First funktionieren sollte, anämisch.Abbildung 1 enthält ein perfektes Beispiel mit einem Customer-Typ und dem Person-Typ, von dem „Customer“ erbt. Dabei habe ich oft einige Methoden hinzugefügt, aber der Basistyp war nur ein Schema mit Gettern und Settern.

Abbildung 1: Typische anämische Domänenmodellklassen sehen wie Datenbanktabellen aus

public class Customer : Person
{
  public Customer()
  {
    Orders = new List<Order>();
  }
  public ICollection<Order> Orders { get; set; }
  public string SalesPersonId { get; set; }
  public ShippingAddress ShippingAddress { get; set; }
}
public abstract class Person
{
  public int Id { get; set; }
  public string Title { get; set; }
  public string FirstName { get; set; }
  public string LastName { get; set; }
  public string CompanyName { get; set; }
  public string EmailAddress { get; set; }
  public string Phone { get; set; }
}

Als ich mir diese Klassen genauer angesehen habe, musste ich feststellen, dass ich im Prinzip lediglich Datenbanktabellen in meinen Klassen definiert hatte. Das ist nicht unbedingt negativ, je nachdem, wofür ein Typ verwendet werden soll.

Sehen Sie sich zum Vergleich den umfassenderen Customer-Typ in Abbildung 2 an. Er ähnelt dem Customer-Typ, den ich in meinem vorherigen Artikel besprochen habe (msdn.microsoft.com/magazine/dn342868). Er macht Methoden verfügbar, die den Zugriff auf Eigenschaften und andere Typen steuern, die Teil des Aggregats sind. Seit dem letzten Artikel habe ich diesen Customer-Typ optimiert, um einige der in diesem Monat erläuterten Themen besser illustrieren zu können.

Abbildung 2: Ein Customer-Typ als umfassendes Domänenmodell (nicht nur Eigenschaften)

public class Customer : Contact
{
  public Customer(string firstName, string lastName, string email)
  {
    FullName = new FullName(firstName, lastName);
    EmailAddress = email;
    Status = CustomerStatus.Silver;
  }
  internal Customer()
  {
  }
  public void UseBillingAddressForShippingAddress()
  {
    ShippingAddress = new Address(
      BillingAddress.Street1, BillingAddress.Street2,
      BillingAddress.City, BillingAddress.Region,
      BillingAddress.Country, BillingAddress.PostalCode);
  }
  public void CreateNewShippingAddress(string street1, string street2,
   string city, string region, string country, string postalCode)
  {
    BillingAddress = new Address(
      street1,street2,
      city,region,
      country,postalCode)
  }
  public void CreateBillingInformation(string street1,string street2,
   string city,string region,string country, string postalCode,
   string creditcardNumber, string bankName)
  {
    BillingAddress = new Address      (street1,street2, city,region,country,postalCode );
    CreditCard = new CustomerCreditCard (bankName, creditcardNumber );
  }
  public void SetCustomerContactDetails
   (string email, string phone, string companyName)
  {
    EmailAddress = email;
    Phone = phone;
    CompanyName = 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; }
}

Bei diesem umfassenderen Modell besteht die öffentliche Schnittstelle von „Customer“ aus expliziten Methoden, es werden also nicht nur Eigenschaften zum Lesen und Schreiben bereitgestellt. Weitere Details finden Sie im Abschnitt über private Setter und öffentliche Methoden im Artikel des letzten Monats. Hier möchte ich versuchen, Ihnen ein besseres Verständnis in Bezug auf den Unterschied zwischen einem anämischen und einem umfassenden Domänenmodell in DDD zu vermitteln.

Wertobjekte können verwirrend sein

Sie mögen einfach aussehen, aber DDD-Wertobjekte werfen für viele, mich eingeschlossen, merkliche Unklarheiten auf. Ich habe viele verschiedene Beschreibungen zu Wertobjekten aus unterschiedlichen Perspektiven gehört und gelesen. Glücklicherweise haben sich die diversen Erläuterungen nicht widersprochen, sondern mir geholfen, Wertobjekte besser zu verstehen.

Im Prinzip ist ein Wertobjekt eine Klasse ohne Identitätsschlüssel.

In Entity Framework gibt es das Konzept der „komplexen Typen“, das dem ziemlich nahe kommt. Komplexe Typen besitzen keinen Identitätsschlüssel – sie kapseln einen Satz von Eigenschaften ein. EF kann mit diesen Objekten in Bezug auf Datenpersistenz umgehen. Sie werden in der Datenbank als Felder der Tabelle gespeichert, der die Entität zugeordnet ist.

Anstelle einer FirstName-Eigenschaft und einer LastName-Eigenschaft in der Person-Klasse können Sie beispielsweise eine FullName-Eigenschaft verwenden:

public FullName FullName { get; set; }

„FullName“ kann eine Klasse sein, die neben den Eigenschaften „FirstName“ und „LastName“ auch einen Konstruktor enthält, der erzwingt, dass diese beiden Eigenschaften angegeben werden.

Ein Wertobjekt ist jedoch mehr als ein komplexer Typ. In DDD wird ein Wertobjekt durch drei Merkmale definiert:

  1. Es besitzt keinen Identitätsschlüssel (dies deckt sich mit dem komplexen Typ).
  2. Es ist unveränderlich.
  3. Beim Überprüfen seiner Gleichheit in Bezug auf andere Instanzen desselben Typs werden alle Werte des Wertobjekts verglichen.

Unveränderlichkeit ist ein sehr interessantes Konzept. Ohne einen Identitätsschlüssel wird die Identität eines Typs durch seine Unveränderlichkeit definiert. Sie können eine Instanz stets anhand ihrer kombinierten Eigenschaften identifizieren. Da der Typ unveränderlich ist, können seine Eigenschaften nicht geändert werden, daher können die Werte des Typs zum Identifizieren einer bestimmten Instanz verwendet werden. (Sie haben bereits gesehen, wie sich DDD-Entitäten durch die Verwendung vertraulicher Setter vor zufälligen Änderungen schützen, aber möglicherweise arbeiten Sie mit einer Methode, über die Sie diese Eigenschaften beeinflussen können.) Ein Wertobjekt versteckt die Setter nicht nur, sondern verhindert auch, dass Sie die Eigenschaften ändern können. Durch das Modifizieren einer Eigenschaft würde sich der Wert des Objekts ändern. Da der Wert durch das gesamte Objekt dargestellt wird, findet keine Interaktion mit einzelnen Eigenschaften statt. Wenn für das Objekt andere Werte festgelegt werden sollen, erstellen Sie eine neue Instanz des Objekts mit einem neuen Wertesatz. Es ist also letztendlich nicht möglich, eine Eigenschaft eines Wertobjekts zu ändern, also ist eine Formulierung wie „wenn für das Objekt andere Werte festgelegt werden sollen“ ein Oxymoron. Eine hilfreiche Alternative kann eine Zeichenfolge sein, wobei es sich ebenfalls um einen unveränderlichen Typ handelt (jedenfalls in allen mir bekannten Sprachen). Beim Verwenden einer Zeichenfolgeninstanz ersetzen Sie keine einzelnen Zeichen, sondern erstellen eine neue Zeichenfolge.

Ein Beispiel für das FullName-Wertobjekt ist in Abbildung 3 dargestellt. Es besitzt keine Identitätsschlüsseleigenschaft. Sie sehen, dass eine Instanziierung nur durch das Bereitstellen beider Eigenschaftswerte möglich ist und dass keine dieser Eigenschaften geändert werden kann. Damit ist die Anforderung der Unveränderlichkeit erfüllt. Die letzte Anforderung, dass es eine Möglichkeit geben muss, die Gleichheit mit einer anderen Instanz dieses Typs zu vergleichen, versteckt sich in der benutzerdefinierten ValueObject-Klasse (die ich von Jimmy Bogard unter bit.ly/13SWd9h entliehen habe). Von dieser Klasse erbt das Wertobjekt. Hierbei handelt es sich um ziemlich verschachtelten Code. Zwar werden bei diesem „ValueObject“ keine Sammlungseigenschaften berücksichtigt, aber es erfüllt meine Anforderungen, da mein Wertobjekt keine Sammlungen enthält.

Abbildung 3: Das FullName-Wertobjekt

public class FullName:ValueObject<FullName>
{
  public FullName(string firstName, string lastName)
  {
    FirstName = firstName;
    LastName = lastName;
  }
  public FullName(FullName fullName)
    : this(fullName.FirstName, fullName.LastName)
  {    }
  internal FullName() { }
  public string FirstName { get; private set; }
  public string LastName { get; private set; }
 // More methods, properties for display formatting
}

Da Sie unter Umständen eine geänderte Kopie benötigen (zum Beispiel ähnlich wie „FullName“, aber mit einer anderen FirstName-Eigenschaft), schlägt der Autor von „Implementing Domain-Driven Design” (Addison-Wesley Professional, 2013) Vaughn Vernon vor, dass „FullName“ Methoden enthalten könnte, mit denen neue Instanzen aus der vorhandenen Instanz erstellt werden:

public FullName WithChangedFirstName(string firstName)
{
  return new FullName(firstName, this.LastName);
}
public FullName WithChangedLastName(string lastName)
{
  return new FullName(this.FirstName, lastName);
}

In Kombination mit meiner Persistenzebene (Entity Framework) wird diese Lösung in EF als ComplexType-Eigenschaft jeder Klasse angesehen, in der sie verwendet wird. Wenn ich „FullName“ als Eigenschaft für meinen Person-Typ verwende, speichert EF die Eigenschaften von „FullName“ in der Datenbanktabelle, in der der Person-Typ gespeichert ist. Standardmäßig werden die Eigenschaften in der Datenbank „FullName_FirstName“ und „FullName_LastName“ genannt.

„FullName“ ist ziemlich übersichtlich. Selbst bei datenbezogenen Designs ist es möglicherweise nicht ideal, „FirstName“ und „LastName“ in separaten Tabellen zu speichern.

Wertobjekte oder verbundene Objekte?

Stellen Sie sich nun ein anderes Szenario vor – einen Kunden mit der Lieferadresse „ShippingAddress“ und der Rechnungsadresse „BillingAddress“:

public Address ShippingAddress { get; private set; }
public Address BillingAddress { get; private set; }

Da mein Gehirn datenbezogen arbeitet, erstelle ich standardmäßig „Address“ als Entität, die eine AddressId-Eigenschaft enthält. Und aus demselben Grund gehe ich davon aus, dass „Address“ in der Datenbank als separate Tabelle gespeichert wird. OK, ich arbeite an mir, um beim Modellieren meiner Domäne nicht immer an die Datenbank zu denken, also sollten diese Überlegungen irrelevant sein. Allerdings wird zu irgendeinem Zeitpunkt meine Datenschicht in EF hinzugefügt, und EF geht von derselben Annahme aus. Dabei ist EF jedoch nicht in der Lage, die Zuordnungen richtig zu erkennen. EF geht von einer 0..1:*-Beziehung zwischen „Address“ und „Customer“ aus; anders ausgedrückt wird erwartet, dass eine Adresse mit einer beliebigen Anzahl von Kunden verbunden sein kann und dass pro Kunde entweder eine oder keine Adresse vorhanden ist.

Abgesehen davon, dass dieses Ergebnis meinem DBA wohl nicht gefallen wird, werde ich meinen Anwendungscode auf keinen Fall auf der Annahme von Entity Framework basieren, nämlich dass mehrere Kunden einer einzigen oder keinen Adresse zugewiesen sind. Aus diesem Grund kann EF unerwartete Auswirkungen auf die Datenpersistenz oder den Datenabruf haben. Aus der Perspektive einer Person, die viel mit EF arbeitet, besteht mein erster Schritt darin, die EF-Zuordnungen mithilfe der Fluent-API zu korrigieren. Aber wenn ich mich auf EF verlasse, um dieses Problem zu beheben, muss ich Navigationseigenschaften hinzufügen, die von „Address“ zurück auf „Customer“ verweisen, und das möchte ich in meinem Modell vermeiden. Wie Sie sehen, käme ich vom Hundertsten ins Tausendste, wenn ich versuchen würde, dieses Problem über EF-Zuordnungen zu lösen.

Wenn ich einen Schritt zurücktrete und mich auf die Domäne konzentriere, und nicht auf EF oder die Datenbank, erscheint es sinnvoller, „Address“ zu einem Wertobjekt anstelle einer Entität zu machen. Dazu sind drei Schritte erforderlich:

  1. Ich muss die Schlüsseleigenschaft aus dem Address-Typ entfernen. (Sie heißt wahrscheinlich „Id“ oder „AddressId“.)
  2. Ich muss sicherstellen, dass „Address“ unveränderlich ist. Dank dem Konstruktor kann ich alle Felder ausfüllen. Ich muss alle Methoden entfernen, mit denen Eigenschaften geändert werden können.
  3. Ich muss gewährleisten, dass „Address“ anhand der Eigenschaften und Felder auf Gleichheit überprüft werden kann. Dazu verwende ich wieder die schöne ValueObject-Klasse von Jimmy Bogard (siehe Abbildung 4).

Abbildung 4: „Address“ als Wertobjekt

public class Address:ValueObject<Address>
{
  public Address(string street1, string street2, 
    string city, string region,
    string country, string postalCode) {
    Street1 = street1;
    Street2 = street2;
    City = city;
    Region = region;
    Country = country;
    PostalCode = postalCode;  }
  internal Address()  {  }
  public string Street1 { get; private set; }
  public string Street2 { get; private set; }
  public string City { get; private set; }
  public string Region { get; private set; }
  public string Country { get; private set; }
  public string PostalCode { get; private set; }
  // ... 
}

Wie ich diese Address-Klasse aus meiner Customer-Klasse verwende, ändert sich nicht, wobei ich jedoch eine neue Instanz erstellen muss, wenn ich die Adresse ändern möchte.

Aber ich muss mir keine Gedanken mehr über das Verwalten dieser Beziehung machen. Außerdem werden Wertobjekte einer weiteren Bewährungsprobe unterzogen, da die Customer-Klasse tatsächlich über ihre Liefer- und Rechnungsdaten definiert wird. Denn wenn ich diesem Kunden etwas verkaufe, muss ich es aller Wahrscheinlichkeit nach versenden, und die Buchhaltungsabteilung schreibt vor, dass eine Rechnungsadresse vorhanden ist.

Das bedeutet nicht, dass jede 1:1- oder 1:0..1-Beziehung durch Wertobjekte ersetzt werden kann, aber hier ist dies eine gute Lösung, und meine Arbeit war auf einmal viel einfacher, als ich nicht mehr ein Rätsel nach dem anderen lösen musste, das auftauchte, weil ich versucht habe, Entity Framework zum Verwalten dieser Beziehung zu zwingen.

Wie bei dem Beispiel mit „FullName“ bedeutet das Ändern von „Address“ in ein Wertobjekt, dass Entity Framework „Address“ als „ComplexType“ ansieht und all diese Daten in den Customer-Tabellen speichert. Da ich mich seit Jahren auf Datenbanknormalisierung konzentriere, dachte ich automatisch, dies sei ein Problem, aber meine Datenbank kann damit reibungslos umgehen, und die Lösung funktioniert für meine Domäne.

Ich kann mir zahlreiche Argumente gegen diesen Ansatz vorstellen, aber sie fangen alle mit „Angenommen dass“ an. Und Argumente, die sich nicht auf meine Domäne beziehen, gelten nicht. Beim Codieren wird viel Zeit mit Eventualitäten vergeudet, die niemals eintreffen. Ich versuche, mehr Rücksicht zu nehmen, wenn es um die Integration vorbeugender Maßnahmen in meine Lösungen geht.

Das war noch nicht alles

Ich arbeite mich nach und nach durch die Liste der DDD-Konzepte durch, die mich als Datenfreak beschäftigen. Je mehr dieser Konzepte ich begreife, desto mehr möchte ich lernen. Wenn ich meine Denkweise ein bisschen anpasse, ergeben diese Muster Sinn. Mein Interesse an Datenpersistenz verliere ich dadurch überhaupt nicht, aber nachdem die Modelle „Domäne“ und „Datenpersistenz und Infrastruktur“ so viele Jahre lang in meinem Kopf zusammenhingen, fühlt sich ihre Trennung einfach richtig an. Es darf jedoch nicht vergessen werden, dass DDD für viele softwarebezogene Aktivitäten nicht erforderlich ist. DDD kann beim Lösen komplexer Probleme hilfreich sein, aber bei einfachen Hindernissen ist es oft Overkill.

Im nächsten Artikel werde ich einige andere technische DDD-Strategien erläutern, die auf den ersten Blick ebenfalls nicht zu meinem datenbezogenen Weltbild passen, zum Beispiel das Aufgeben bidirektionaler Beziehungen, das Berücksichtigen von Invarianten und die scheinbare Notwendigkeit, Datenzugriff aus Aggregaten auszulösen.

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: Stephen Bohlen (Microsoft)
Stephen A. Bohlen arbeitet derzeit als leitender Technologieexperte für die Microsoft Corporation und blickt auf über 20 Jahre Erfahrung als praktizierender Architekt, CAD-Manager, IT-Technologe, Softwaretechniker, CTO und Consultant zurück. Er hilft ausgewählten Partnerorganisationen von Microsoft bei der Einführung von Vorabversionen und innovativen Produkten und Technologien für Microsoft-Entwickler.