
In dieser Mini-Serie schaue ich mir die Building Blocks im Domain-driven Design (DDD) an. Diese Bausteine gehören zum Tactical Design des DDD. Abgrenzend gibt es das Strategic Design, welches sich mit den High-Level-Aspekten der Domäne als Ganzes befasst, wie der Ubiquituos Language, Bounded Contexts und Context Maps. Strategic Design ist der Hands-On-Part im DDD und die Building Blocks stellen konkrete Möglichkeiten dar, um DDD in Code zu gießen.
- Domain-driven Design: Value Object
- Domain-driven Design: Entity, Aggregate und Domain Service
- Domain-driven Design: Factory und Repository
- Domain-driven Design: Domain Event
Dieser zweite Teil der Serie befasst sich mit Entity, Aggregate und Domain Service.
Entity, Aggregate und Domain Service sind so eng miteinander verbunden, dass ich entschieden habe alle drei in einem gemeinsamen Artikel zu betrachten. Da es mir unmöglich erscheint, in dem Zuge nicht auch Factories, Repositories und Domain Events zumindest zu erwähnen, möchte ich folgende Abbildung zur Orientierung mitgeben.

Die Domain ist der Kern der Applikation. In der Domain befinden sich Domain Services, Aggregates, Entities, Value Objects, Factories und Repositories. Aggregates kapseln mehrere Entities und Value Objects. Sie werden über Factories erzeugt und von Repositories verwaltet. Domain Services führen Aggregate-übergreifende Geschäftslogik aus. Aus dem Application Layer ist der Zugriff auf Domain Services, Aggregates und Repositories des Domain Layers möglich.
Entity
Eine Entity repräsentiert in der Domäne ein fachliches Etwas mit einer Identität. Es kann sich dabei um alles handeln, was in der jeweiligen Fachdomäne von der Bedeutung her über eine reine Ansammlung von Werten hinausgeht. Eine Entity wird damit über eine ID identifiziert und mit anderen Entities verglichen, und nicht über ihre Werte.
Ein Beispiel, welches dies gut verdeutlicht, ist eine Person als Entity mit den Attributen Vorname und Nachname. Selbst wenn Vorname und Nachname zweier Person-Entities miteinander übereinstimmen, dann kann es sich dennoch um verschiedene Personen handelt. Auch die Ergänzung weiterer Attribute wie eines Geburtsdatums ändert nichts an dieser Tatsache. Um Person-Entities sinnvoll miteinander vergleichen zu können, bedarf es eines eindeutigen Identifizierungsmerkmals, einer ID. Für Angestellte eines Unternehmens, die in einer HR-Software verwaltet werden, kann dies beispielsweise die Personalnummer sein.
Da Entities über IDs identifiziert werden, muss es eine zentrale Stelle geben, die sie verwaltet und anhand der ID die richtige Entity ermitteln kann. Diese zentrale Stelle nennt man ein Repository. Zu einer ID gibt es entweder eine Entity, oder es gibt sie nicht. Damit es die Entity gibt, muss sie angelegt werden. Wenn sie wieder gelöscht wird, gibt es sie nicht mehr. Damit hat eine Entity eine Lebensspanne – im Gegensatz dazu steht das Value Object, das jederzeit spontan erzeugt werden kann.
Entities sollten nie nach Belieben neu instanziert werden können. Stattdessen sollte eine Factory für die Erzeugung einer Entity verantwortlich sein. Bei der Erzeugung müssen mögliche fachliche Rahmenbedingungen eingehalten werden. Es ist z.B. denkbar, dass das Repository für die Vergabe der ID verantwortlich ist, oder dass es einen festen Nummernkreis gibt und ein Domain Service die jeweils nächste ID aus dem Nummernkreis bezieht.
Entities müssen genau wie Value Objects dafür sorgen, dass sie in sich konsistent bleiben und ihre erlaubten Invarianten bewahren. Eine Entity sollte in keinem Fall alle Felder, die ihren State ausmachen, mit Gettern und Settern nach außen verfügbar machen.
Aggregate
Ein Aggregate bündelt (aggregiert) mehrere Entities, die zusammen eine geschlossene und in sich konsistente Einheit bieten. Ein Aggregate kapselt den Zugriff auf die in ihm gebündelten Entities über eine AggregateRoot, auch Root Entity genannt, welches die Schnittstelle nach außen, d.h. in die Domain Services und andere Einheiten außerhalb der Domäne darstellt.
Ein Aggregate kann z.B. eine Bestellung sein. Die Bestellung selbst hat eine variable Anzahl an Bestellpositionen und einen Kunden, der eine Rechnungs- und Lieferadresse hat. Eine Bestellposition kapselt ein Produkt, die Menge der Exemplare und für diese Position spezifische Bestellkonditionen wie einen möglichen Positionsrabatt. Diese Bestellposition hat ohne die Bestellung keine Bedeutung. Sie muss über die Bestellung gekapselt werden. Eine Änderung der Menge oder der positionsbezogenen Bestellkonditionen muss über das Aggregate vorgenommen werden. Das Aggregate stellt damit sicher, dass die Bestellung als Ganzes zu jedem Zeitpunkt in sich konsistent ist. Über das Aggregate werden auch Bestellpositionen hinzugefügt oder wieder entfernt. Es darf also auch nicht außerhalb einer Bestellung möglich sein, eine Bestellposition zu erzeugen.
Entities können Bestandteile mehrerer Aggregates sein. Der als Teil der Bestellung erwähnte Kunde mit seinen Adressen gehört auch zu einem anderen Aggregate. Nehmen wir an, seine Stammdaten werden über ein Aggregate Person gepflegt. Wenn aus der Bestellung heraus nun Stammdaten des Kunden modifiziert werden können, so müssen die anderen Aggregates darüber in Kenntnis gesetzt werden. Dafür bietet es sich an, ein Domain Event zu verwenden.
Domain Service
Aggregates sind in sich geschlossene Bausteine mit einem bestimmten Scope. Wenn eine fachliche Transaktion stattfindet, die über diesen Scope hinausgeht und mehrere Aggregates involviert, bietet es sich an dafür einen Domain Service zu verwenden. Ein Domain Service selbst ist stateless und gewährleistet die Einhaltung fachlicher Constraints über mehrere Aggregates hinweg. Er kann z.B. sicherstellen, dass bei Eingang einer Bestellung die zu versendende Ware an den Paketdienstleister gemeldet wird. Dies würde über den Scope der Bestellung selbst hinausgehen. Jegliche Geschäftslogik, die nicht innerhalb eines Aggregates abgebildet werden kann, sollte immer über einen Domain Service gekapselt werden. Das bedeutet aber nicht, das sämtliche fachliche Aufrufe von außerhalb der Domäne über einen Domain Service eingehen müssen. Repositories und Aggregates sollten auch außerhalb der Domäne zugänglich sein und direkt verwendet werden können.
Ich vertrete die Ansicht, dass ein Aggregate nach Möglichkeit keinen Domain Service injiziert bekommen sollte. Ein Anwendungsfall dafür wäre, dass eine Bestellung bei Anlage nochmal überprüft, ob die Produkte aller Bestellpositionen auch wirklich bestellbar sind. Wenn eine Bestellung über längere Zeit in einem Warenkorb liegt, ist das ein durchaus plausibles Szenario. Eine denkbare Lösung wäre es, dass die Bestellung eine Methode validateProducts()
bereitstellt, über die ein Warenlager-Domain-Service injiziert wird, den das Bestellung-Aggregate verwendet, um die Validität seiner Positionen zu ermitteln und dem Konsumenten zu signalisieren, wenn die Bestellung nicht bestellbare Positionen enthält. Dieser kann die Positionen dann entfernen oder sich z.B. dafür entscheiden, dass die Ware erst dann geliefert wird, wenn sie wieder verfügbar ist. Ich würde die Validierung in einem solchen Szenario nach Möglichkeit über ein Domain Event abwickeln, da das Injizieren des Domain Services die Komplexität des Aggregates erhöht und zusätzliche Abhängigkeiten schafft. Es besteht aus meiner Sicht die Gefahr, dass das Aggregate zu viel Verantwortung kapselt und Erweiterungen in der Domäne kompliziert und aufwändig werden, wenn es viele Querbeziehungen von Aggregates zu Domain Services gibt.