Value-Objekte sind eines der DDD-Entwurfsmuster, die ich auch in jedem nicht-DDD-Projekt einsetzen würde. Denn sie sind eine sehr bedeutende Veränderung im Kleinen mit enormer Wirkung auf die Codequalität.
Ein Value-Objekt beschreibt einen Aspekt der Domäne ohne eigene Identität. Das bedeutet, es hat keine ID und ist kurzlebiger als Entities. Man könnte statt eines Value-Objekts also auch einen Basis-Datentyp benutzen. Allerdings bringt die Verwendung von Value-Objekten deutliche Vorteile:
Die Lesbarkeit des Codes wird erhöht
Die Logik zu einem Aspekt ist da, wo sie hingehört
Der Code wird robuster
Werfen wir einen genaueren Blick auf diese Aspekte:
Die Lesbarkeit wird durch Value-Objekte erhöht, weil die Eigenschaft der Domäne, die sie repräsentieren, bereits im Typ kodiert ist. Betrachten wir folgendes, zugegebenermaßen konstruiertes, Beispiel:
customer.billingAddress(
"Max",
"Mustermann",
"Konsul-Smidt-Str.",
"8g",
"28217",
"Bremen"
);
Diese Methode hat dann die verwirrende Signatur
Customer billingAddress(
String, String, String, String, String, String
)
bei der man sich auf die Parameter-Namen verlassen muss, und die insbesondere auch den folgenden, falschen Aufruf akzeptiert:
customer.billingAddress(
"Mustermann",
"Max",
"Konsul-Smidt-Str.",
"8g",
"28217",
"Bremen")
Viel klarer hingegen ist diese Signatur:
Customer setAddress(
FirstName,
LastName,
Street,
HouseNumber,
PostalCode,
City)
Hier wird sich bereits der Compiler über ungültige Eingaben beschweren. Natürlich ist die zu hohe Anzahl der Paramter Teil des Problems, aber ist ja auch nur ein Beispiel. Mit vier String-Parametern wäre die Situation nicht wirklich besser.
Ein Value-Objekt erlaubt mir, die Fachlogik und die Daten gemeinsam zu halten. Betrachten wir das Beispiel eines Einkaufs per SEPA-Mandat. Dazu könnte die folgende Methode am Warenkorb aufgerufen werden:
basket.payPerDirectDebit(
"DE94 2919 0024 0012 3456 00",
"GENODEF1HB1"
);
Auch hier gilt natürlich, dass `Basket payPerDirectDebit(IBAN, BIC)` besser zu lesen ist, als `Basket payPerDirectDebit(String, String)`.
Darüber hinaus kann so aber auch die Validierungs-Logik für eine gültige IBAN in der IBAN-Klasse selbst (statt in irgendeinem Service) abgelegt werden:
public IBAN(String value) {
if (!valid(value)) {
throw new InvalidValueException(value);
}
}
Natürlich kann man auch hier von der reinen Lehre abweichen und in Value-Objekten ungültige Werte erlauben. Das ist aber ein Thema für einen anderen Artikel und soll auch in einem anderen Artikel behandelt werden.
Ein weiterer Vorteil der Kapselung der IBAN in einem Value-Objekt: Man begegnet immer wieder Web-Formularen, bei denen man die IBAN nur maschinenlesbar (also nicht in Vierer-Blöcken gruppiert) eingeben kann - schon ist die UX ruiniert. Auch die Aufgabe der Formatierung und Normalisierung einer IBAN kann diese Klasse übernehmen: Sie akzeptiert Eingaben mit Leerzeichen und bietet die Ausgabeformate
public String toHumanReadableString()
public String toMachineParseableString()
an.
Robustheit
Die Robustheit folgt eigentlich nur daraus, dass die Logik dort ist, wo sie sein sollte. Nehmen wir an, ein Shop speichert den Warenkorb ganz klassisch in einer relationalen Datenbank und arbeitet nicht mit Value-Objekten. Die Entity für Warenkorb-Positionen könnte dann so aussehen:
public class LineItem {
Long lineItemId;
String skuCode;
int quantity;
}
Für den Use Case, die Anzahl einer Warenkorbposition zu ändern, gäbe es dann beispielsweise diese Methode:
Basket updateLineItem(Long lineItemId, int quantity)
Nun stellt sich die Frage, *wo* die Eingabe validiert wird? Sprich, an welcher Stelle überprüfe ich, dass die `quantity` größer Null ist? Die naheliegende Antwort ist, in der `updateLineItem`-Methode. Was ist aber, wenn im weiteren Projektverlauf aus welchen Gründen auch immer im `LineItemRepository` ebenfalls eine Methode zur Änderung der Anzahl eingeführt wird? Die Wahrscheinlichkeit, dass es dort vergessen wird, ist hoch. Wahrscheinlich kann man deshalb auch erstaunlich vielen Shops immer noch eine negative Anzahl für eine Warenkorb-Position unterjubeln.
Wenn die Entity für die Warenkorb-Positionen so aussähe, wäre das Problem zentral gelöst:
public class LineItem {
LineItemId lineItemId;
SkuCode skuCode;
Quantity quantity;
}
Das Quantity-Objekt kann im Konstruktor überprüfen, dass der Wert nicht kleiner als 1 ist. So ist *immer* sichergestellt, dass eine Warenkorb-Position eine gültige Anzahl besitzt.
Ein weiterer wichtiger Punkt für Robustheit ist die Vermeidung von `null`-Werten und somit von `NullPointerExceptions`. Damit erspart man sich die `null`-Checks, die an irgendeiner Stelle bestimmt vergessen werden, dafür aber an anderen Stellen überflüssig sind.
Dies kann man zum Einen durch `Optional` oder (link:http://www.vavr.io/ text: Vavrs target: _blank) `Option` vermeiden, zum Anderen aber auch dadurch, dass ein Value-Objekt einen leeren Wert haben kann. Die Adresse aus dem Beispiel weiter oben könnte beispielsweise einen Adresszusatz haben, der meistens leer sein wird. Statt hier `null` zu verwenden, kann man lieber einen Empty-Wert definieren:
class Address {
...
StreetAppendix streetAppendix = StreetAppendix.EMPTY;
...
}
Dieser kann dann ohne `null`-Checks verwendet werden, indem er z.B. zu einem Leerstring serialisiert wird.