Einhaltung von Invarianten mit dem Value Object Pattern

Seite 2: Beispiel Kaufvertrag

Inhaltsverzeichnis

Kaufvertrag ohne Value Object (Abb. 1)

Am Beispiel eines Kaufvertrags zwischen zwei Personen sei verdeutlicht, wie sich die Implementierung des Domänenobjekts "Kaufvertrag" mit einem Value Object verbessern lässt. Abbildung 1 zeigt das Modell des Kaufvertrags ohne Verwendung eines Value Object.

Für die Klasse "Kaufvertrag" sollen folgende Invarianten gelten:

  1. Jeder Vertrag hat immer einen Käufer und einen Verkäufer.
  2. Käufer und Verkäufer sind nicht dieselbe Person.
  3. Ist ein Unterzeichnungsdatum gesetzt, lassen sich die Vertragspartner und der Kaufpreis nicht mehr ändern.

Außerdem sollen alle Verträge und Personen im System über eine Nummer eindeutig identifizierbar sein. Auf technische Aspekte wie Persistenz verzichtet der Artikel bewusst, um das Beispiel nicht unnötig zu verkomplizieren.

In einem anämischen Objektmodell (vgl. [2]) könnte eine erste Version von "Kaufvertrag" so aussehen (Programmiersprache Java):

public final class Kaufvertrag {
private final long vertragsnummer;
private Person kaeufer;
private Person verkaeufer;
private int kaufpreis;
private Date unterzeichnungsdatum;

public Kaufvertrag() {
// Eindeutige ID generieren
this.vertragsnummer = IDGenerator.newId();
}
public long getVertragsnummer() {
return vertragsnummer;
}
public Person getKaeufer() {
return kaeufer;
}
public void setKaeufer(Person kaeufer) {
this.kaeufer = kaeufer;
}
public Person getVerkaeufer() {
return verkaeufer;
}
public void setVerkaeufer(Person verkaeufer) {
this.verkaeufer = verkaeufer;
}
public int getKaufpreis() {
return kaufpreis;
}
public void setKaufpreis(int kaufpreis) {
this.kaufpreis = kaufpreis;
}
public Date getUnterzeichnungsdatum() {
return unterzeichnungsdatum;
}
public void setUnterzeichnungsdatum(Date unterzeichnungsdatum) {
this.unterzeichnungsdatum = unterzeichnungsdatum;
}
}

Dabei ist klar, dass die oben genannten Invarianten nur durch Prüfung von außen einzuhalten sind.

Um die Bedingung der ersten Invariante innerhalb des Objekts zu prüfen, könnte man die Setter für Käufer und Verkäufer wie folgt erweitern:

public void setVerkaeufer(Person verkaeufer) {
if (verkaeufer == null) {
throw new KeinVerkaeuferException();
}

this.verkaeufer = verkaeufer;
}
// setKaeufer(...) wird analog erweitert.

Zusätzlich lässt sich folgender Konstruktor integrieren, um das Objekt in einem gültigen Zustand zu initialisieren:

public Kaufvertrag(Person kaeufer, Person verkaeufer) { 
setKaeufer(kaeufer);
setVerkaeufer(verkaeufer);

// Eindeutige ID generieren
this.vertragsnummer = IDGenerator.newId();
}

Zur Einhaltung der zweiten Invariante sind sowohl der Konstruktor als auch die beiden Setter für Käufer und Verkäufer um einen Identitätsvergleich der beiden Personen zu erweitern:

public Kaufvertrag(Person kaeufer, Person verkaeufer) {
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();
}

// Eindeutige ID generieren
this.vertragsnummer = IDGenerator.newId();

this.kaeufer = kaeufer;
this.verkaeufer = verkaeufer;
}
public void setVerkaeufer(Person verkaeufer) {
if (verkaeufer == null) {
throw new KeinVerkaeuferException();
}

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

this.verkaeufer = verkaeufer;
}
// setKaeufer(...) wird analog erweitert.

Diese Implementierung verhindert das Vertauschen von Käufer und Verkäufer und ist deshalb in der Praxis untauglich (es könnte vorkommen, dass die Vertragspartner nach einer Fehleingabe zu tauschen sind). Stattdessen führt man eine Methode zur gleichzeitigen Änderung der Vertragspartner ein:

public void vertragspartnerAendern(Person kaeufer, Person verkaeufer) {
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.kaeufer = kaeufer;
this.verkaeufer = verkaeufer;
}

Um die dritte Invariante einzuhalten, ist folgende Erweiterung durchzuführen:

public void vertragspartnerAendern(Person kaeufer, Person verkaeufer) {
if (unterzeichnungsdatum != null) {
throw new VertragBereitsUnterzeichnetException();
}
...
}

public void setKaufpreis(int kaufpreis) {
if (unterzeichnungsdatum != null) {
throw new VertragBereitsUnterzeichnetException();
}

this.kaufpreis = kaufpreis;
}