Einhaltung von Invarianten mit dem Value Object Pattern

Seite 3: Value Object - Einsatz

Inhaltsverzeichnis

Kaufvertrag mit Value Object (Abb. 2)

Die Klasse ist jetzt relativ lang und enthält neben der Validierungslogik keinerlei Code. In einem realen Projekt müsste der Entwickler wahrscheinlich noch etliche Zeilen Code für die eigentlichen Aufgaben des Objekts integrieren. Mit einem Value Object kann man einige Zeilen der Validierungslogik in eine neue Klasse auslagern. Weil dies einen Namen benötigt, erhält das Projektteam gleichzeitig die Chance, die Sprache des Projekts (bei Evans "Ubiquitous Language" genannt) um einen neuen Begriff zu erweitern:

Käufer, Verkäufer und Preis sind in ein Value Object namens "Vertragsinhalt" auszulagern. Weiterhin lassen sich die Invarianten verfeinern:

Invarianten für "Kaufvertrag":

  1. Jeder Vertrag hat einen Vertragsinhalt.
  2. Nach Unterzeichnung lässt sich der Vertragsinhalt nicht mehr verändern.

Invarianten für "Vertragsinhalt":

  1. Käufer und Verkäufer sind vorhanden.
  2. Käufer und Verkäufer sind nicht dieselbe Person.

Da sich Value Objects nach ihrer Instanzierung nicht mehr ändern lassen, ist der Code nur wenige Zeilen lang:

public final class Vertragsinhalt {
private final Person kaeufer;
private final Person verkaeufer;
private final int kaufpreis;

public Vertragsinhalt(Person kaeufer, Person verkaeufer, int kaufpreis) {
if (kaeufer == null) {
throw new KeinKaeuferException();
}

if (verkaeufer == null) {
throw new KeinVerkaeuferException();
}

// Identitätsvergleich zwischen kaeufer und verkaeufer
if (kaeufer.equals(verkaeufer)) {
throw new KaeuferGleichVerkaeuferException();
}

this.kaufpreis = kaufpreis;
this.kaeufer = kaeufer;
this.verkaeufer = verkaeufer;
}
public Person getKaeufer() {
return kaeufer;
}
public Person getVerkaeufer() {
return verkaeufer;
}
public int getKaufpreis() {
return kaufpreis;
}
@Override
public boolean equals(Object obj) {
// Der Vergleich von Value Objects
// erfolgt auf Basis der Dateninhalte.
...
}
@Override
public int hashCode() {
// Auch die Berechnung des Hashcodes erfolgt
// anhand der Dateninhalte.
...
}
}

Lediglich der Konstruktor enthält Code zur Prüfung der Invarianten. Die Methoden hashCode() und equals(...) implementiert das Value Object auf Basis der Dateninhalte. Im Falle von equals(...) bedeutet dies, dass die Methode "true" zurückgibt, wenn die zu vergleichenden Objekte vom selben Typ sind und man sie mit denselben Daten initialisiert hat.

Auch die Klasse "Kaufvertrag" ist nun deutlich kürzer:

public final class Kaufvertrag {
private final long vertragsnummer;
private Vertragsinhalt vertragsinhalt;
private Date unterzeichnungsdatum;

public Kaufvertrag(Vertragsinhalt vertragsinhalt) {
setVertragsinhalt(vertragsinhalt);

// Eindeutige ID generieren
this.vertragsnummer = IDGenerator.newId();
}
public long getVertragsnummer() {
return vertragsnummer;
}
public Vertragsinhalt getVertragsinhalt() {
return this.vertragsinhalt;
}
public void setVertragsinhalt(Vertragsinhalt vertragsinhalt) {
// Der Vertragsinhalt kann nicht mehr geändert werden,
// sobald der Vertrag unterschrieben ist.
if (unterzeichnungsdatum != null) {
throw new VertragBereitsUnterzeichnetException();
}

if (vertragsinhalt == null) {
throw new KeinGueltigerVertragsinhaltException();
}

this.vertragsinhalt = vertragsinhalt;
}
public Date getUnterzeichnungsdatum() {
return unterzeichnungsdatum;
}
public void setUnterzeichnungsdatum(Date unterzeichnungsdatum) {
this.unterzeichnungsdatum = unterzeichnungsdatum;
}
}

Die beiden Invarianten für "Kaufvertrag" lassen sich durch zwei einfache if-Anweisungen in der Methode setVertragsinhalt(...) implementieren. Weitere Prüfungen sind nicht notwendig, weil nur diese eine Methode den Vertragsinhalt eines Kaufvertrags ändern kann. Eine von der Klasse "Kaufvertrag" unbemerkte Änderung des Vertragsinhalts ist demnach nicht realisierbar.