Domain-driven Design: Value Object

Lesezeit: 10 Minuten

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.

Dieser erste Teil der Serie befasst sich mit dem Value Object.

Ein Value Object zeichnet sich dadurch aus, dass es sich von anderen Value Objects über die Ausprägungen seiner Felder unterscheidet. Damit hat es keine Identität, was der entscheidende Unterschied zu einer (Domain) Entity ist. Populäre Beispiele für Value Objects sind Geldbeträge, Zeitspannen und Zeitpunkte, Koordinaten, Gewichte, Geschwindigkeiten, Längenangaben oder Abmessungen.

Unveränderlichkeit

Ein Value Object sollte in der Regel unveränderlich (immutable) sein. Das bringt nicht nur viele Vorteile mit sich, sondern ist auch aus Sicht der Domäne logisch. Ein Geldbetrag (Value Object) kann als Preis eines Verkaufsartikels (Entity) referenziert werden. Ändert sich der Preis des Artikels, so ändert sich aber nicht der Geldbetrag. Stattdessen sollte der Preis des Verkaufsartikels einen anderen Geldbetrag referenzieren. Der Geldbetrag selbst hat keine Kenntnis über den Artikel und seinen Preis – es ist nicht in seiner Zuständigkeit. Gleichzeitig ist es denkbar, dass ein anderer Artikel den gleichen Preis hat. Beide Artikel ändern im Laufe der Zeit aber unabhängig voneinander ihren Preis. Die Unveränderlichkeit garantiert also auch, dass das Value Object frei von Seiteneffekten ist.

Weitere Vorteile der Unveränderlichkeit

Durch die Unveränderlichkeit sind Value Objects thread-safe und können z.B. bedenkenlos in Java in parallellen Streams verwendet werden. Darüber hinaus können sie in auf unveränderliche Objekte zugeschnittenen Design Patterns wie dem Flyweight Pattern verwendet werden. Da ein Value Object keine Identität hat, hat es auch keine Lebensspanne und kann jederzeit neu erstellt werden.

Umsetzung der Unveränderlichkeit

Unveränderlichkeit kann in Java zum größten Teil durch den Modifier final für alle Felder des Value Objects gewährleistet werden. Das reicht allerdings nicht aus, wenn das jeweilige Feld selbst wiederum veränderlich (mutable) ist. Bei einer Liste kann z.B. im Getter Collections.unmodifiableList() verwendet werden, um sich gegen schreibenden Zugriff von außen zu schützen. Ein Array oder ein Date kann im Getter geklont werden (clone()-Methode).

Rechenoperationen sollten immer ein neues Objekt zurückgeben. Das bietet auch gleichzeitig den Vorteil eines Fluent Interfaces, so dass beliebig viele Operationen aneinandergereiht werden können. Ein gutes Beispiel dafür ist die Java-Klasse BigDecimal:

BigDecimal result = BigDecimal.TEN
                .add(BigDecimal.TEN)
                .subtract(BigDecimal.ONE)
                .multiply(BigDecimal.TEN);

Wertgleichheit und Einheiten

Da ein Value Object sich über seinen State differenziert, ist es in Java eine gute Idee, die Methoden toString(), equals() und hashcode() zu überschreiben. Wenn verschiedene Einheiten eine Rolle spielen, dann sollten sie Teil des Value Objects sein und über dieses umgerechnet und verglichen werden können. Eine dezentrale Umrechnung von Einheiten außerhalb des Value Objects ist fehleranfällig, da sie nicht erzwungen wird und nicht explizit ist. Sie sollte daher vermieden werden. Einheiten müssen bei der Umsetzung von equals() und hashcode() berücksichtigt werden: 1 Hektar equals 10.000 Quadratmetern, 1 Tag equals 24 Stunden, 1 Zoll equals 2,54 Zentimetern. Ein gutes Beispiel für ein Value Object mit verschiedenen Einheiten ist die Klasse Duration aus dem JDK.

Selbstvalidierung

Sofern keine gewichtigen Gründe dagegensprechen, sollte ein Value Object immer valide sein. Indem das Value Object sich bei der Erstellung selbst validiert, kann gewährleistet werden, dass die Validierung an keiner Stelle umgangen werden kann. Da es grundsätzlich keine gute Praxis ist, Geschäftslogik im Konstruktor unterzubringen und diesen mit einer Exception zu verlassen, bietet es sich bei einem Java-basierten Value Object an, statische Factory-Methoden zu verwenden:

EmailAddress.of("[email protected]");

Die statische Methode of() übernimmt hierbei die Validierung des Strings [email protected] und kann eine IllegalArgumentException werfen, wenn es sich nicht um eine gültige Adresse handelt, bevor eine Instanz von Email über einen privaten Konstruktor erzeugt und zurückgegeben wird.

Die Validierung sollte sich auf die Möglichkeiten des Value Objects selbst beschränken. Eine Email-Adresse kann etwa über einen regulären Ausdruck validiert werden. Es sollten keine Abhängigkeiten zu anderen Klassen oder Ressourcen vorhanden sein, um etwa zu prüfen, ob die Email-Adresse bereits von einem anderen Kunden verwendet wird. Eine solche Validierung läge eher in der Zuständigkeit eines Domain Services.

Weitere Beispiele für Selbstvalidierung von Value Objects sind:

  • Das Alter einer Person in Jahren muss zwischen 0 und 120 liegen
  • Eine Id muss eine Ganzzahl größer 0 haben
  • Eine Prozentzahl muss zwischen 0 und 100 sein
  • Ein Passwort muss mindestens 12 Zeichen lang sein, darunter jeweils mindestens ein Groß-, Kleinbuchstabe, eine Ziffer und ein Sonderzeichen

Primitive Obsession

Primitive Obsession ist ein Anti-Pattern und bezeichnet die übermäßige Verwendung von primitiven Typen und Strings. Dies führt zu schlecht lesbarem Code und Verwechslungsgefahr:

sendEmail("[email protected]", "[email protected]", "betreff", "inhalt");

Die Methode im Beispiel hat vier Parameter und alle sind vom Typ String. Es kann leicht passieren, dass man beim Aufruf versehentlich die Argumente in der falschen Reihenfolge übergibt. Durch den Einsatz von Value Objects wird die Bedeutung der Parameter explizit gemacht:

sendEmail(
                Recipients.of(EmailAddress.of("[email protected]")),
                Sender.of(EmailAddress.of("[email protected]")),
                Message.ofContent(inhalt).withSubject(Subject.of("betreff"))
        );

Gegenüber der String-Variante lässt sich in dieser Implementierung noch ein weiterer Vorteil erkennen: Value Objects sind flexibler. Durch die Kapselung der Empfänger in einem Recipients-ValueObject lässt sich dieses derart gestalten, dass auch mehrere Empfänger unterstützt werden. Das kann z.B. dadurch realisiert werden, dass die Factory-Methode of() einen Varargs entgegennimmt, oder einfach überladen wird. Die Signatur der Methode sendEmail() muss dabei nicht geändert werden und der obenstehende Aufruf bleibt durch eine solche nachträgliche Erweiterung gültig. In der String-Variante müsste die Signatur geändert werden, indem z.B. aus String recipient ein List<String> recipients gemacht wird.

Die Entscheidung, ob man einen primitiven Typen bzw. einen String einsetzt oder ein Value Object, sollte wohlüberlegt sein. Ein zu hohes Maß an Value Objects, insbesondere wenn diese so spezifisch sind, dass sie nur an einer einzigen Stelle eingesetzt werden, bläht den Code auf und kann sehr unübersichtlich werden. Insofern kann man nicht pauschal sagen, dass sämtliche Strings und primitive Typen in Methodenparametern durch Value Objects ersetzt werden sollten.

Value Object vs. Domain Entity

Ein Value Object kann andere Value Objects untergeordnet haben. So kann sich ein Preis (Value Object) aus einem Steuersatz (Value Object) und einem Geldbetrag (Value Object) zusammensetzen. Ein Value Object kann allerdings keine Entities untergeordnet haben, da diese ja eine Identität besitzen und ein Value Object eine solche per Definition nicht hat. Andersrum haben viele Entities Value Objects untergeordnet.

Die Entscheidung, ob eine Modellklasse aus der Domäne nun ein Value Object oder eine Entity ist, kann nicht immer eindeutig beantwortet werden. Ein Gewicht oder eine Zeitspanne sind in der Regel immer Value Objects, wie aber verhält es sich bei einer Adresse oder einer GTIN (Global Trade Item Number)? Um diese Frage zu beantworten ist ein Blick in die Domäne notwendig.

Bei einem Kauf als Gast in einem Online-Shop ist die Adresse nur eine flüchtige Information, die außerhalb der Auftragsverarbeitung keine Rolle spielt. Eine Adresse sollte in dem Fall als Value Object repräsentiert werden. Ein gänzlich anderer Fall ist eine Adresse im Versorgungsgebiet eines Stadtwerkes. Für diese Adresse hat das Stadtwerk einen Versorgungsauftrag, es beliefert sie z.B. mit Strom, Gas und Wasser. Im System der Stadtverwaltung ist die Adresse mit einem Strom- und Wasserzähler verknüpft und es gibt einen Eigentümer dieser Adresse, an den regelmäßige Abschlags- und Jahresrechnungen ausgestellt werden. Die Adresse hat eindeutig eine Identität und ist eine Entity oder ein Aggregate.

Eine GTIN ist in einem Online-Shop möglicherweise nur eine Information, die für Endkunden im Shop mit ausgewiesen wird. Mit ihr ist im Shop keine weitere Funktion verbunden, sie kann daher als Value Object angesehen werden. Die GS1-Gruppe verwaltet Blöcke von GTIN-Nummern und verkauft diese an herstellende Unternehmen, die damit ihre Produkte eindeutig kennzeichnen und die Nummer als Barcode verwenden können. Für die GS1-Gruppe hat jede Nummer – oder zumindest ein Block – eine Identität, denn sie muss gewährleisten, dass kein Block mehrfach vergeben ist und ihre Kunden Lizenzkosten für die Ihnen zugewiesenen Blöcke zahlen. Ein Hersteller, der einen solchen Block lizensiert hat, muss wiederum den Überblick behalten, für welche Produkte er welche GTIN-Nummern verwendet und welche Nummern für neue von ihm auf den Markt gebrachte Produkte infrage kommen. Auch für ihn hat eine GTIN-Nummer eine Identität und einen Zustand.

Ich habe mir übrigens angewöhnt, Value Objects und Entities durch ein Marker-Interface zu kennzeichnen, so dass ich mir immer bewusst bin, was hinter einer Klasse steht.