29.05.2019 von Olaf Sebelin

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:

Erhöhte Lesbarkeit

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.

Logik da, wo sie hingehört

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 Vavrs 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.

Der Autor

Olaf Sebelin
ist vorwiegend in der Backend-Entwicklung zu finden. Das liegt neben der Blindheit auf dem Design-Auge auch an der Vorliebe für statisch getypte Sprachen, Terminals und Themen wie Spring, DDD, Kubernetes, Security, PKIs usw.
blog comments powered by Disqus