thePHP.cc Logo English Kontakt Menü
Domain-Driven Design mit PHP

Domain-Driven Design mit PHP

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.

Dieser Artikel ist eine aktualisierte und überarbeitete Fassung des gleichnamigen Artikels, der ursprünglich im PHP Magazin erschienen ist.

Im Artikel Ketchup oder Mayo 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, anstatt 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.

Entität (Entity)

Wir beginnen mit dem vermeintlich wichtigsten Muster, der Entität (Entity). Hierbei handelt es sich um ein Objekt mit einer Identität. Da solche Objekte zumeist deutlich länger leben als ein einzelner PHP-Prozess, kann man sagen, dass Entitäten eigentlich immer persistente Objekte sind. Allerdings ist es nicht empfehlenswert, Entitäten, beispielsweise durch den Einsatz eines ORM, generell stark an die Persistenz zu koppeln.

Typische Beispiele für Entitäten 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.

class   Person
{
   public   function   __construct ( public   readonly   PersonId   $id )
   {
   }
 
   // ... 
}
 
class   PersonId
{
   public   function   __construct ( private   readonly   UUID   $uuid )
   {
   }
 
   public   function   asString ( ) :   string
   {
     return   $this -> uuid -> asString ( ) ;
   }
 
   // ... 
}

PersonId ist ganz bewusst keine Subklasse von UUID, denn wir verwenden bevorzugt Komposition anstelle von Vererbung.

Natürlich hat eine 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(bool $flag): void

könnte man etwa zwei Methoden activate() und deactivate() erstellen.

Ein kardinaler Designfehler – gerade im Zusammenhang mit Entitäten – ist der Versuch, ein großes, 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, indem kleinere und einfachere Modelle für jeweils einen bestimmten Verwendungszweck geschaffen werden. Dies verbessert das Design der Software ganz erheblich.

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

Während es in vielen Fällen durchaus richtig ist, eine Entität als persistentes Objekt zu begreifen, sollte man die zu starke Kopplung von Entitäten an die Persistenz der Anwendung zu vermeiden. Moderne ORM-Lösungen bieten durch das Data Mapper-Pattern zwar eine technische Entkopplung von Objekt und Datenbank, in der Realität sind Objekte und Datenstruktur aber zumeist gleichgeschaltet und ändern sich gemeinsam. Viele Entwickler nehmen damit eine strukturelle Kopplung zwischen Objekten in der Anwendung und der Tabellenstruktur in Kauf, die eigentlich unnötig und unerwünscht 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 Infrastruktur gesehen, sondern als Teil der Domäne, da die verschiedenen Zugriffspfade zu einem Objekt wesentlich durch die Anwendungsfälle in der Domäne bestimmt werden.

Es empfiehlt sich, ein solches Repository als Interface zu definieren, da dies beim Testen das Mocken erleichtert. Um die Beispiele zu vereinfachen, verzichten wir hier jedoch auf das Interface.

Je weniger Zugriffsmethoden ein Repository hat, desto besser. Eine Methode findById() 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. Als alternativer Persistenz-Mechanismus bietet sich natürlich auch direkt Event Sourcing an.

In der Tat ist es so, dass man weniger Zugriffsmethoden in einem Repository benötigt, wenn man die Modelle (Entitäten) kleiner und spezifischer entwirft.

class   PersonRepository
{
   public   function   findById ( PersonId   $id ) :   Person
   {
     // ... fetch from persistence ...
   }
 
   // ...
}

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

class   PersonRepository
{
   public   function   create ( ... ) :   Person
   {
     $person   =   new   Person ( new   PersonId ,   ... ) ;  
     $this -> add ( $person ) ;
     return   $person ;
   }
 
   private   function   add ( Person   $person ) :   void
   {
     // ... 
   }
 
   // ...
}

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 fachlich erzeugende Methode für eine Entität wird im Lebenszyklus eines persistenten Objekts genau einmal aufgerufen. Das Laden des gespeicherten Objekts aus der Persistenz erfolgt dann später aus einer rein technischen Motivation heraus.

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.

class   PersonRepository
{
   private   array   $persons   =   [ ] ;
 
   public   function   findById ( PersonId   $id ) :   Person
   {
     if   ( $this -> has ( $id ) )   {
       return   $this -> get ( $id ) ;
     }
 
     // ... fetch from persistence ... 
   }
 
   private   function   add ( Person   $person ) :   void
   {
     $this -> persons [ $person -> id -> asString ( ) ]   =   $person ;
   }
 
   private   function   has ( PersonId   $id ) :   void
   {
     return   isset ( $this -> persons [ $id -> asString ( ) ] ) ;  
   }
 
   private   function   get ( PersonId   $id ) :   void
   {
     return   $this -> persons [ $id -> asString ( ) ] ;
   }
 
   // ...
}

In diesem Beispiel wurden aus Gründen der Übersichtlichkeit jegliche Fehlerprüfungen weggelassen.

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 Persistenz-Subsystem 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:

class   PersonRepository
{
   public   function   __construct (
     private   readonly   DoctrineRepository   $personRepository )
   {
   }
 
   // ...
}

Eine Subklassenbeziehung ist auch hier aufgrund der starken Kopplung zur Basisklasse nicht empfehlenswert. Die Regel Favour Composition over Inheritance kommt schließlich nicht von ungefähr.

Fabrik (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 nachfolgenden Beispiel hat nur die statische Fabrikmethode und nicht die Instanz der Klasse selbst eine Abhängigkeit auf Employee:

class   Person
{
   public   static   function   fromEmployee ( Employee   $employee ) :   self
   {
     return   new   self ( $employee -> name ( ) ,   ... ) ;
   }
 
   private   function   __construct ( $name ,   ... )
   {
     // ... 
   }
}

Man kann darüber diskutieren, ob das Mapping von Employee auf Person nicht in einem separaten Data Mapper stehen sollte. In einfachen Fällen wie hier kann man sich die separate Klasse, die normalerweise ohnehin parallel zu Änderungen an Employee oder Person gewartet werden muss, aber durchaus sparen.

Wertobjekt (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 skalaren Wert 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 nachfolgende Beispiel zeigt sehr schön den Einsatz von Fabrikmethoden, die in diesem Zusammenhang auch Named Constructors genannt werden:

class   Money
{
   public   static   function   fromParameters ( int   $amount ,   Currency   $currency ) :   self
   {
     // ... 
   }
 
   // ... 
}
 
class   Currency
{
   public   static   function   fromIsoCode ( string   $isoCode ) :   self
   {
     // ... 
   }
 
   // ... 
}

Wenn Sie sich wundern, warum $amount ein Integer ist: Es empfiehlt sich, Geldbeträge immer in der kleinsten Einheit, beispielsweise in Cents und als ganze Zahlen zu repräsentieren, um Rundungsfehler zu vermeiden.

Nun können wir Geldbeträge vergleichen:

class   Money
{
   public   readonly   int   $amount ;
   public   readonly   Currency   $currency ;
 
   // ... 
 
   public   function   equals ( Money   $money ) :   bool
   {
     if   ( $this -> currency   !=   $money -> currency )   {
       return   false ;
     }
 
     return   $this -> amount   =   $money -> amount ;
   }
}
 
class   Currency
{
   private   function   __construct ( private   readonly   string   $isoCode )
   {
   }
 
   public   static   function   fromIsoCode ( $isoCode ) :   self
   {
     return   new   self ( $isoCode ) ;
   }
 
   // ...
}

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 Duplikation und Fehler.

Natürlich gilt auch für Wertobjekte, was bereits für Entitäten gesagt wurde: versuchen Sie nicht, ein einziges komplexes Modell (Wertobjekt) für alle Use Cases zu erstellen, sondern definieren Sie mehrere voneinander unabhängige Wertobjekte, insbesondere wenn sie es mit konkurrierenden Anforderungen zu tun haben. Ein zugegebenermaßen konstruiertes Beispiel dafür wären Zeiten in einer Zeiterfassung, die mit dem Kunden abgerechnet werden sollen und ein Vielfaches von 0,25 Stunden sein müssen, während Arbeitszeiten minutengenau erfasst werden. Es dürfte schwierig werden, diese Anforderungen in ein einziges Wertobjekt abzubilden.

Für ganz besonders grundlegende Dinge wie Maße und Gewichte oder die elementaren fachlichen Konzepte in Ihrer Anwendung wird es vermutlich einige Wertobjekte geben, die in einer gemeinsamen Library über verschiedene Teile Komponenten geteilt werden.

Es gibt nun allerdings ein großes technisches 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.

Stellen wir uns dazu ein konkretes Beispiel vor: Ich habe 10 Euro als Wertobjekt in der Hand und gebe Ihnen davon eine "Kopie" in Form einer Referenz. Wir halten nun beide 10 Euro in der Hand. Wenn Sie nun den Zustand des Wertobjekts verändern und den Wert auf 20 Euro setzen, dann halten Sie 20 Euro in der Hand – aber ich auch. Genau dies ist unintuitiv; um nicht zu sagen falsch. Wo kämen wir dann da hin, wenn sich Geld auf diese Art und Weise vermehren würde?

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 das Problem der wundersamen Geldvermehrung zuverlässig zu vermeiden, müssen Wertobjekte unveränderlich – immutable – sein. Das bedeutet, dass sich der Zustand eines Wertobjekts nach der Erzeugung niemals mehr verändern darf. Methoden, die einen neuen Wert erzeugen möchten, geben dann einfach neue Objektinstanzen zurück:

class   Money
{
   // ... 
 
   public   function   addTo ( Money   $money ) :   self
   {
     $this -> ensureCurrenciesMatch (
       $this -> currency ,
       $money -> currency
     ) ;
 
     return   new   Money (
       $this -> amount   +   $money -> amount ,
       $this -> 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 Money-Instanz muss keines der Wertobjekte seinen Zustand ändern und es wird keine wundersame Geldvermehrung geben.

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

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, um die Entscheidung zwischen Entität und Wertobjekt anhand eines Beispiels zu illustrieren: Wenn ich Ihnen 10 Euro gebe, dann interessiert uns beide vermutlich nicht, welche Seriennummer der Schein hat. Die Identität des Scheins ist uns in diesem 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 Geldbeutel 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.

Aggregat (Aggregate)

Ein Aggregat ist eine Objektstruktur, die aus einer Entität und weiteren Objekten besteht. Die Wurzel des Aggregats nennt man Aggregate Root. Ein Aggregate Root ist eine Fassade, die für den Benutzer eine einfache API für das gesamte Aggregat bietet. Außer auf das Aggregate Root dürfen außerhalb des Aggregats keine Verweise auf Unterobjekte existieren, außer diese sind unveränderlich. Auch hier empfiehlt sich also der Einsatz von Wertobjekten. Oft wird gefragt, wie man denn den Zustand eines Aggregats ändern könne, wenn doch die einzelnen darin referenzierten Wertobjekte unveränderlich sind? Man ersetzt ein Wertobjekt durch eine neue Instanz, die einen anderen Zustand hat und damit einen anderen Wert repräsentiert.

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. Wichtig ist: ein Aggregat dient nicht als Datencontainer, sondern kapselt Verhalten. Im Sinne von CQRS ist es nicht sinnvoll, Aggregate für lesende Zugriffe zu verwenden.

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 - und am besten nur genau diesen einen. Aus Datensicht bedeutet dies, dass im Aggregat nur diejenigen Daten enthalten sind, die benötigt werden, um für einen Nutzungsfall die Einhaltung aller relevanten Geschäftsregeln zu sichern. Das nachfolgende Beispiel skizziert, wie ein Aggregat für eine Person aussehen könnte. Wir gehen in diesem Beispiel davon aus, dass wir sicherstellen müssen, dass eine Person nur eine begrenzte Anzahl von Bankkonten eröffnet, und dass eine Person weiß, welche Bankkonten sie überhaupt hat:

class   Person
{
   // ...
 
   public   function   __construct ( private   readonly   PersonId   $id )
   {
   }
 
   public   function   openAccount ( ... ) :   void
   {
     $this -> ensureMaximumNumberOfAccountsIsNotExceeded ( ) ;
 
     $this -> accounts [ ]   =   new   Account ( ... ) ;
   }
 
   public   function   balance ( ) :   Money
   {
     $balance   =   Money :: fromParameters (
       0 ,
       Currency :: fromIsoCode ( 'EUR' )
     ) ;
 
     foreach   ( $this -> accounts   as   $account )   {
       $balance   =   $balance -> addTo (
         $account -> balance ( )
       ) ;
     }
 
     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 Wertobjekt Account 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. Eine andere Bank könnte ja die gleiche Kontonummer vergeben haben; ein Problem, dass in der Praxis über Nummernkreise gelöst wird beziehungsweise dadurch, dass jede Bank ihre eindeutige ID, nämlich die Bankleitzahl, zum Bestandteil einer Kontonummer macht.

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) ein Entwurfsmuster, das im Domain-Driven Design beschrieben ist. Ein Service kapselt Funktionalität, die nicht unbedingt einer Entity oder einem 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 etwa 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. Bevor man sich bei dieser Entscheidung verzettelt, sollte man sich darauf fokussieren, einfach das gewünschte Verhalten zu programmieren. Dabei ergeben sich dann früher oder später Hinweise darauf, ob die Lösung ein Dienst oder eine Entität sein soll.

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 Buch (Eric Evans) und das rote (Vaughn Vernon) Buch, auch wenn gerade Evans keine einfache Lektüre ist.

Viel Spaß beim domänengetriebenen Entwickeln!