Suche
Fachlichkeit im Mittelpunkt der Softwareentwicklung - Teil 2

DDD-Patterns – Bausteine für erfolgreiches Domain-driven Design

Stefan Priebsch

(c) Shutterstock / igor kisselev

Anstelle im ersten Schritt auszuwählen, welches MVC-Framework man einsetzen möchte, sollte man sich viel stärker auf die Fachlichkeit konzentrieren. Dieser Artikel zeigt, welche Entwurfsmuster Sie kennen müssen, um bei der Entwicklung die Fachlichkeit anstelle technischer Aspekte in den Mittelpunkt zu stellen.

Im ersten Artikel „Mit Ketchup oder Mayo?“ dieser Serie haben wir eine Pommesbude eröffnet – natürlich nur als Gedankenspiel. Im Zuge der Eröffnung gab es zahlreiche Entscheidungen zu treffen: Wir haben unter anderem gesehen, dass es sich nicht empfiehlt, zu früh auf technische Details zu fokussieren und dass es essenziell ist, mit dem Kunden in dessen Fachsprache zu sprechen, anstattt ihn mit technischen Begriffen zu verwirren.

Die Idee, die Fachlichkeit in den Mittelpunkt der Softwareentwicklung zu rücken (Domain-driven Design), zahlt sich besonders bei der Entwicklung von komplexen Systemen aus. Wobei wir in der letzten Ausgabe ja bereits gesehen hatten, dass selbst ein scheinbar einfaches Business wie eine Pommesbude eine erstaunliche Komplexität innehat, wenn man nur einmal genauer hinschaut. In diesem Artikel sehen wir uns die wichtigsten Entwurfsmuster an, die beim Domain-driven Design zum Einsatz kommen.

Obwohl es wichtig und lehrreich ist, die vorgestellten Muster zu kennen, zu verstehen und deren Einsatz einzuüben, fokussieren die Entwurfsmuster auf die Implementierung und nicht auf den besonders wichtigen strategischen Teil des Domain-driven Design (DDD). DDD ist mehr als nur eine Sammlung von Entwurfsmustern. DDD ist eine Entwicklungsphilosophie, und es braucht durchaus seine Zeit, bis man sich von den bisherigen Sicht- und Denkweisen löst und sich für diese neue Sichtweise öffnen kann.

Entity

Wir beginnen mit dem vermeintlich wichtigsten Muster, der Entität (Entity). Hierbei handelt es sich um ein (persistentes) Objekt mit einer Identität. Typische Beispiele hierfür sind Personen oder Kunden, die eine eindeutige Identität haben, die nicht von ihren Attributen abhängt. Herr Müller ist nicht unbedingt Herr Müller, auch wenn der Vorname gleich ist. Hinzu kommt, dass sich Attribute über die Zeit ändern können: Menschen ändern etwa ihren Namen, wenn sie heiraten. Sie können sogar ihr Geschlecht ändern. Es soll auch schon vorgekommen sein, dass sich der (zunächst angenommene) Geburtstag etwa von Flüchtlingen als falsch erweist, wenn plötzlich ihre Geburtsurkunde auftaucht.

Es empfiehlt sich, für jede Entität ein eigenes ID-Objekt zu erstellen. Im nachfolgenden Beispiel basiert diese PersonId auf einer UUID und wir gehen einfach davon aus, dass ein entsprechendes Objekt existiert. Interessant ist, dass weder die Klasse Person noch irgendwelcher anderer Code von UUID abhängen – UUID ist ein Implementierungsdetail von PersonId. Wir könnten in einer alternativen Implementierung von PersonId als Identifikator beispielsweise einen Auto-Increment-Wert der Datenbank verwenden (Listing 1).

<?php

class Person
{
  private $id;

  public function __construct(PersonId $id)
  {
    $this->id = $id;
  }

  // ... 
}

class PersonId
{
  private $uuid;

  public function __construct(UUID $uuid = null)
  {
    $this->uuid = $uuid;
  }

  public function __toString()
  {
    return (string) $this->uuid;
  }

}

Natürlich hat die Person noch weitere Attribute und insbesondere Verhalten. Im Domain-driven Design liegt der Fokus nicht auf Daten, sondern auf Verhalten. Entitäten haben daher typischerweise keine Getter und Setter, sondern Methoden mit Namen, die das Verhalten des Objekts aus geschäftlicher Sicht beschreiben. Anstelle eines klassischen Setters wie setActive($flag), der einen booleschen Wert als Parameter erwartet, könnte man etwa zwei Methoden activate() und deactivate() erstellen. So spart man sich auch gleich noch die Validierung des Flags und muss sich nicht mit der Fehlersituation auseinandersetzen, dass kein boolescher Wert übergeben wird.

Ein kardinaler Designfehler – gerade im Zusammenhang mit Entitäten – ist der Versuch, ein allumfassendes Modell zu erstellen. Je nach Kontext kann eine Person etwa ein Angestellter, ein Kunde oder etwa ein Geschäftspartner sein. Doch damit nicht genug: eine identische Person kann in einem komplexen System in mehreren Rollen existieren. Eine der wichtigsten Erkenntnisse des Domain-driven Design ist es, dass man diese unterschiedlichen Aspekte nicht in einer einzigen Klasse abbilden will, sondern dafür mehrere Modelle schaffen muss. So wird Komplexität vermieden.

Die Persistenzinfrastruktur der Anwendung muss für Entitäten sicherstellen, dass für jede ID jeweils nur eine Instanz im Speicher ist. Ansonsten gäbe es zwei (oder gar mehr) möglicherweise unterschiedliche Zustände einer identischen Person, und wir könnten nicht entscheiden, welcher Zustand nun der Richtige ist.

Repository

Zum Laden einer Entität kommt ein Repository zum Einsatz. Repositories sind Benutzern von ORM-Lösungen wohlbekannt, allerdings wird ein Repository im DDD nicht als Bestandteil der Persistenzinfrastruktur gesehen, sondern als Teil der Domäne. Es empfiehlt sich, ein solches Repository als Interface zu definieren, da dies beim Testen das Mocken erleichtert. Um die Listings zu vereinfachen, verzichten wir hier jedoch auf das Interface.

Je weniger Zugriffsmethoden ein Repository hat, desto besser. Eine Methode findById() (Listing 2) wird jedoch mindestens benötigt. Kommt man ohne weitere Zugriffspfade aus, eröffnet dies interessante Möglichkeiten, die Persistenz zu implementieren. Man könnte eine Entität beispielsweise in einer Dokumentendatenbank speichern und sich das objektrelationale Mapping sparen, das zumeist deshalb erfolgt, damit man über verschiedene Zugriffspfade auf ein Objekt zugreifen kann.

<?php

class PersonRepository
{
  public function findById(PersonId $id)
  {
    // ... 
  }
}

Interessant wird es, wenn man die Fabrikmethode(n) zum Erzeugen einer Entität direkt in das Repository packt (Listing 3). Man braucht dann keine gesonderte add()-Methode zum Hinzufügen einer Entität zum Repository. Warum würde man auch eine Person erzeugen, um sie niemals zu speichern?

<?php

class PersonRepository
{
  public function create()
  {
    return new Person(new PersonId());
  }

  // ... 
}

Ganz im Sinne von Methodennamen, die eine direkte fachliche Bedeutung haben, sollte ein Name wie create() vermieden werden. Wenn eine Person etwa als Mitarbeiter eingestellt werden soll, dann könnte beispielsweise hire() als Methodenname verwendet werden.

Die Hauptaufgabe von Repositories ist das Identitätsmanagement von Entitäten. Im Prinzip ist ein Repository ein In-Memory-Cache für Objekte: Wird ein Objekt abgerufen, das noch nicht im Speicher ist, dann wird es geladen. Befindet sich das angefragte Objekt bereits im Speicher, wird eine (weitere) Referenz darauf zurückgegeben. Auf diese Weise wird sichergestellt, dass nicht mehrere Kopien der gleichen (im Sinne von identischen) Person im Speicher sind.

Eine weitere wichtige Aufgabe des Repositories ist die Speicherung von Änderungen. Dazu dient normalerweise eine Methode commit(), bei deren Aufruf alle im Speicher befindlichen Entitäten, deren Zustand sich geändert hat, persistiert. Genauer gesagt muss nur die jeweilige Zustandsänderung persistiert werden. Ein Repository ist somit eine Fassade, die das Persistenzsubsystem verbirgt und den Zugriff darauf vereinfacht.

Auch wenn man einen objektrelationalen Mapper wie Doctrine verwendet, sollte man die benötigten Repositories als Bestandteil der eigenen Domäne definieren und darin lediglich auf die Implementierung des ORM zurückgreifen. Eine Subklassenbeziehung ist aufgrund der starken Kopplung nicht empfehlenswert. Die Regel „favour composition over inheritance“ kommt schließlich nicht von ungefähr.

Factory

Auch Factories, also Fabriken, gehören im DDD zur Domäne. Factories erzeugen, wie wir alle wissen, Objekte. Um etwa den aufrufenden Code vom konkreten Klassennamen zu entkoppeln, kann eine abstrakte Fabrik verwendet werden. Hat das zu erzeugende Objekt Abhängigkeiten oder ist die Erzeugung komplex, sollte ein Fabrikobjekt (in diesem Zusammenhang gerne auch „Dependency Injection Container“ genannt) oder ein Builder-Entwurfsmuster verwendet werden.

Mittels Fabrikmethoden können Objekte übrigens entkoppelt werden. Im Beispiel in Listing 4 hat nur die statische Fabrikmethode, aber nicht die Personinstanz selbst eine Abhängigkeit auf Employee.

<?php

class Person
{
  public static function fromEmployee(Employee $employee)
  {
    return new self($employee->getName(), ...);
  }

  private function __construct($name, ...)
  {
    // ... 
  }
}

Value Object

Das vielleicht wichtigste Muster beziehungsweise Konzept im Domain-driven Design sind Wertobjekte (Value Objects). Das Lehrbuchbeispiel für Wertobjekte ist das „Money-Objekt“. Anstelle Geldbeträge als Integer- oder Fließkommazahl herumzureichen, sollte in Geschäftsanwendungen immer ein Money-Objekt verwendet werden. Der Grund dafür ist, dass Geldbeträge aus zwei zusammengehörigen Informationen bestehen, und zwar Betrag und Währung. 10 Euro sind schließlich nicht 10 US-Dollar. Ein Money-Objekt kapselt diese beiden Informationen in einem Objekt. Das Beispiel in Listing 5 zeigt sehr schön den Einsatz von Fabrikmethoden. Mit dem Beispiel aus Listing 6 können wir anschließend Geldbeträge vergleichen.

<?php

class Money
{
  public static function fromParameters($amount, Currency $currency)
  {
    // ... 
  }

  // ... 
}

class Currency
{
  public static function fromIsoCode($isoCode)
  {
    // ... 
  }

  // ... 
}

 

<?php

class Money
{
  private $amount;
  private $currency;

  public static function fromParameters(
    $amount,
    Currency $currency
  )
  {
    return new self($amount, $currency);
  }

  // ... 

  public function equals(Money $money)
  {
    if ($this->currency != $money->getCurrency()) {
      return false;
    }

    return $this->amount = $money->getAmount();
  }
}

class Currency
{
  private $isoCode;

  public static function fromIsoCode($isoCode)
  {
    return new self($isoCode);
  }

  private function __construct($isoCode)
  {
    $this->isoCode = $isoCode;
  }

  public function __toString()
  {
    return (string) $this->currency;
  }
}

Verwenden Sie Wertobjekte immer dann, wenn Sie mit mehreren zusammengehörigen Informationen zu tun haben. Neben Geld könnten dies beispielsweise Gewichte, Maße oder sonstige Einheiten sein. Selbst für einzelne Informationen kann es sich lohnen, Wertobjekte zu verwenden, nämlich immer dann, wenn es Plausibilitätsprüfungen gibt, deren Erfüllung Sie sicherstellen wollen. Für eine Zeiterfassung könnte man beispielsweise sicherstellen wollen, dass erfasste Arbeitszeiten immer ein Vielfaches von 0,25 Stunden sind. Solche Prüfungen in Wertobjekte zu kodieren vermeidet Codeduplikation und Fehler.

Es gibt nun ein großes Problem mit Wertobjekten. Wenn wir in PHP einen skalaren Wert herumreichen, dann wird dieser bei jedem Aufruf kopiert (das ist zwar implementierungstechnisch gesehen nicht ganz richtig, das ignorieren wir hier aber geflissentlich, da es uns nur auf das nach außen wahrnehmbare Verhalten von PHP ankommt). Ein Objekt wird dagegen per Referenz weitergegeben. Das ist wichtig, denn ansonsten könnte der aufgerufene Code keine Veränderungen an übergebenen Objekten vornehmen. Genau dies ist aber bei Wertobjekten unerwünscht, weil von verschiedenen Stellen aus Referenzen auf ein bestimmtes Wertobjekt vorhanden sein können.

Lesen Sie auch: Microservices: Agilität mit Architektur skalieren

Stellen wir uns ein konkretes Beispiel vor: Ich habe 10 Euro in der Hand und gebe Ihnen eine „Kopie“ in Form einer Referenz. Wir halten nun beide 10 Euro in der Hand. Wenn Sie nun den Zustand „Ihres“ Wertobjekts verändern und den Wert auf 20 Euro setzen, dann halten Sie 20 Euro in der Hand – und ich auch. Genau dies ist unintuitiv; um nicht zu sagen falsch. Da ein Wertobjekt einen skalaren Wert ersetzt, würde man erwarten, dass bei der Übergabe eine Kopie erzeugt wird, anstelle per Referenz zu arbeiten.

Um diese Probleme zu vermeiden, müssen Wertobjekte unveränderlich – immutable – sein. Das bedeutet, dass sich der Zustand eines Wertobjekts nach der Erzeugung niemals mehr verändern kann. Entsprechende Methoden geben einfach eine neue Objektinstanz zurück (Listing 7).

<?php

class Money
{
  // ... 

  public function addTo(Money $money)
  {
    $this->ensureCurrenciesMatch(
      $this->currency,
      $money->getCurrency()
    );

    return new Money(
      $this->amount + $money->getAmount(),
      $this-&gt:currency
    );
  }

Natürlich addieren wir die beiden Beträge nur dann, wenn die Währung übereinstimmt. Man kann eben nicht 10 Euro und 10 US-Dollar addieren. Dank der neu erzeugten Instanz muss keines der Wertobjekte seinen Zustand ändern.

Wertobjekte haben – im Gegensatz zu Entitäten – keine Identität. Man kann davon beliebig viele Instanzen erzeugen und vergleicht diese auf Gleichheit, indem man die Attribute vergleicht. Meist wird dazu eine Methode equals() oder isEqualTo() implementiert, die die Details des Vergleichens implementiert.

Woran kann man nun festmachen, ob man in einer bestimmten Situation ein Wertobjekt oder eine Entität verwendet? Die Antwort auf diese Frage ist einfach, aber möglicherweise unbefriedigend: Es kommt darauf an. Sie ahnen es … die Entscheidung ist nicht von technischen Faktoren abhängig, sondern von der Fachlichkeit.

Bleiben wir beim Geld: Wenn ich Ihnen 10 Euro gebe, dann interessiert uns beide vermutlich nicht, welche Seriennummer der Schein hat. Die Identität des Scheins ist uns (im aktuellen Kontext) egal. Selbst wenn wir einem Geldscheinobjekt die Seriennummer als Attribut geben, macht dies unser Wertobjekt noch nicht unbedingt zu einer Entität, denn wir kümmern uns nicht um die Identität. Wenn Sie und ich jeweils einen Geldschein mit identischer Seriennummer im Geldbeuel haben, dann werden wir das nicht herausfinden (können). Wenn wir aber eine Zentralbank sind, die Geldscheine druckt und anhand von Seriennummern für den Verkehr freigibt, dann werden wir die Geldscheine (in diesem Kontext) vermutlich zu Entitäten machen. Als Grundregel sollten Sie immer annehmen, dass ein Objekt „nur“ ein Wertobjekt ist, bis das Gegenteil bewiesen ist beziehungsweise Sie gezwungen sind, ein Objekt zu einer Entität zu machen.

Aggregate

Ein Aggregat ist eine Objektstruktur, die aus einer Entität und weiteren Objekten besteht. Die „Wurzel“ des Aggregats ist das so genannte „Aggregate Root“. Ein Aggregate Root ist eine Fassade, die für den Benutzer ein einfaches API für das gesamte Aggregat bietet. Objekte innerhalb des Aggregats müssen keine global eindeutige ID haben; eine lokal eindeutige ID genügt, da die Objekte innerhalb eines Aggregats immer nur durch das Aggregate Root modifiziert werden.

Ein Aggregat ist zugleich die kleinste Einheit, die von der Persistenz geladen wird. Welchen Umfang ein Aggregat hat und wie es strukturiert ist, hängt – Sie ahnen es – rein von der Fachlichkeit ab. Und es ist in der Tat nicht immer einfach, ein Aggregat zu definieren.

Ein Aggregat hat die Aufgabe, Konsistenz zu sichern, und muss mindestens einen geschäftlich bedeutsamen Vorgang eigenverantwortlich durchführen können. Aus Datensicht bedeutet dies, dass im Aggregat alle Daten enthalten sind, die benötigt werden, um für einen Nutzungsfall die Einhaltung aller relevanten Geschäftsregeln zu sichern. Das Beispiel in Listing 8 skizziert, wie das Aggregatobjekt für eine Person aussehen könnte. Wir gehen in diesem Beispiel davon aus, dass es dessen Aufgabe ist, sicherzustellen, dass eine Person nur eine begrenzte Anzahl von Bankkonten eröffnet.

<?php

class Person
{
  private $id;

  public function __construct(PersonId $id)
  {
    $this->id = $id;
  }

  public function openAccount(...)
  {
    $this->ensureMaximumNumberOfAccountsIsNotExceeded();
    $this->accounts[] = new Account(...);
  }

  public function getBalance()
  {
    $balance = Money::fromParameters(
               0,
               Currency::fromIsoCode('EUR')
    );

    foreach ($this->accounts as $account) {
      $balance = $balance->addTo(
                 $account->getBalance()
      );
    }

    return $balance;
  }
}

Die einzelnen Bankkonten haben in diesem Beispiel keine Identität, sondern sind nur Wertobjekte, denn im vorliegenden Kontext interessiert uns nur der Kontostand. Da ein Wertobjektaccount alleine niemals persistiert oder von der Persistenz geladen wird, müssen wir uns um dessen Identitätsmanagement kein Sorgen machen. Das Aggregate Root wird – beispielsweise anhand einer Kontonummer oder IBAN – lokal die Identität der Konten verwalten.

Wir können mit dieser Lösung beispielsweise nicht sicherstellen, dass Kontonummern global eindeutig sind. Das ist aber auch nicht die Aufgabe einer Person, sondern die Aufgabe der Bank. Und streng genommen kann auch eine Bank nur die lokale Eindeutigkeit einer Kontonummer sicherstellen. Im Domain-driven Design existiert ein Aggregat, um für einen oder mehrere eng miteinander verwandte Geschäftsvorfälle die Einhaltung der Geschäftsregeln (und damit die Konsistenz) zu sichern. (Daten-)Redundanz ist dabei kein Problem; es kann durchaus sein, dass sowohl die Bank den Kontostand protokolliert (für alle Konten) als auch der Benutzer (für seine Konten). Die Kommunikation zwischen beiden erfolgt typischerweise durch Messaging.

Service

Zu guter Letzt sind Dienste (Services) ebenfalls ein Entwurfsmuster, das im Domain-driven Design wichtig ist. Ein Service kapselt Funktionalität, die nicht unbedingt einer Entity oder Aggregat zuzuordnen ist. Damit sind in diesem Zusammenhang weniger technische Services wie das Senden von E-Mails oder die Erzeugung von PDF-Dokumenten gemeint, sondern Dienste wie die Ermittlung eines Produktpreises (möglicherweise unter Berücksichtigung von Kundengruppe und Rabatten) oder etwa eine Bonitätsbewertung durch einen externen Dienst.

Es ist nicht immer einfach zu entscheiden, was ein Service wird und welche Funktionalität in eine Entität beziehungsweise in ein Aggregat gehört.

Fazit

Denken Sie immer daran: Domain-driven Design ist viel mehr als nur eine Sammlung von Entwurfsmustern. Das Verständnis für die hier beschrieben Muster ist lediglich eine Voraussetzung dafür, erfolgreich domänengetrieben Software zu entwickeln. Nehmen Sie sich die Zeit und lesen die das blaue (Evans) und das rote (Vaughn) Buch, auch wenn gerade Evans keine einfache Lektüre ist. Viel Spaß beim domänengetriebenen Entwickeln!

Geschrieben von
Stefan Priebsch
Stefan Priebsch
Diplom-Informatiker Stefan Priebsch ist Mitgründer von the PHP Consulting Company (thePHP.cc) und berät Firmen zum Einsatz von PHP und verwandten Technologien, wenn er nicht gerade als Zwillingsvater im Privatleben Skalierungsprobleme löst.
Kommentare

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht.