Artikelformat

ValueObjects (Teil 1)

Eine Diskussion mit einem Kollegen hat mein Interesse für das heutige Thema geweckt. Er wollte unbedingt ValueObjects nutzen, da diese sich in dem entsprechenden Zusammenhang für ihn als einzig sinnvolle Möglichkeit darstellten. Die Implementierung war natürlich schnell erledigt, aber die Frage nach dem warum hat mich doch etwas beschäftigt. Und vor allem, ob man das Konzept in PHP implementieren kann und eins vorweg: klar geht das!

Wir alle kennen Werte. Ein Wert ist Beispielsweise die Zahl 3. Diese kann ich vergleichen (3 == 3) und ich kann auch überprüfen ob zwei Zahlen nicht nur gleich, sondern identisch sind (3 === 3). Das ist in PHP vor allem deshalb notwendig, weil wir eine nicht-typisierte Sprache nutzen. So ein fieses Beispiel ist der Vergleich 0 == false und 0 === false. Wobei der erste Vergleich sich als wahr erweist, ist der 2. Vergleich natürlich falsch. Es gibt, um wieder zurück zum Thema zu kommen, einen Wert genau 1 mal.

Jetzt kann ich eine Hausnummer nehmen, bspw 1630 und eine Kundennummer, bspw 1630 und ich kann die beiden Werte vergleichen. $hausnummer === $kundennummer. Das ist ja gar nicht so lustig, weil eine Kundennummer nicht das gleiche wie eine Hausnummer sein kann bzw. sein sollte. Aber wir haben ja noch Objekte und so können wir einfach einen fachlichen Wert definieren, der eine Hausnummer oder eine Kundennummer darstellt. Wir haben 2 Objekte. Vergleichen wir diese, sind vielleicht die Attribute darin zufällig gleich, aber die Klassen unterscheiden sich und somit haben wir 2 verschiedene Werte.

Um das ganze etwas spannender zu machen, vergessen wir Kundennummer und Hausnummer und überlegen und ein Objekt, das eine Person darstellt. Dies könnte ein Kunde sein. Dazu gibt es nun auch eine Klasse, aber wir definieren diese Klasse mit einer Besonderheit. Der Konstruktor ist private und eine statische Methode innerhalb der Klasse liefert eine Instanz zurück.
Total wirr? Dachte ich auch, aber somit kann man unterstreichen, dass wir kein übliches Objekt haben, sondern ein ValueObject. Und so sieht unsere Person jetzt aus:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
final class Person {
 
    private $firstname;
    private $lastname;
 
    private function __construct($firstname, $lastname) {
        $this->firstname = $firstname;
        $this->lastname = $lastname;
    }
 
    public static function valueOf($firstname, $lastname) {
        return new self($firstname, $lastname);
    }
 
    public function getFirstname() {
        return $this->firstname;
    }
 
    public function getLastName() {
        return $this->lastname;
    }
 
}

Wie man nun schnell feststellt ist die Klasse final. Man kann dieses ValueObject nicht ableiten, weil wir ja einen Wert darstellen. 2 kann man auch nicht ableiten, oder genauer einen int-Wert. Weiter gibt es keine Setter. Das hat auch damit zu tun, das wir einen Wert simulieren. Eine 2 ist auch nicht plötzlich 0,5 oder gar ein String. Und hier beziehe ich mich nicht auf eine Variablenzuweisung, sondern wirklich auf die Zahl 2. Diese ist unveränderlich. Und daher ist ein ValueObject auch unveränderlich, was wir durch fehlende Setter simulieren. Ja, man kann per Reflection daran etwas drehen, aber das ist ja nicht im Sinne des Erfinders.

Jetzt erstellen wir 2-mal die Person „Max Mustermann“ und prüfen die Personen gegeneinander:

1
2
3
4
5
6
7
8
9
10
$person1 = Person::valueOf("Max", "Mustermann");
$person2 = Person::valueOf("Max", "Mustermann");
 
if ($person1 == $person2) {
    echo "Both objects are equal\n";
}
 
if ($person1 === $person2) {
    echo "Both objects are the same\n";
}

Wie zu erwarten ist sind beide Personen gleich, aber nicht identisch. Schließlich haben wir ja auch 2 Instanzen der Klasse generiert. Momentan ist also 3 == 3, aber es gilt auch 3 !== 3. Das ist aber nicht, was wir uns wünschen, also muss für dieses Problem eine Lösung gefunden werden.

Wir benutzen einfach einen ValueObject-Pool. Darin werden erzeugte Objekte abgelegt und bei Bedarf herausgenommen. Das bedeutet dann, es existiert ein spezielles ValueObject (also hier: „Max Mustermann“) genau einmal und falls jemand auch ein Person-Objekt mit diesen Werten hat, ist es genau das gleiche wie unseres. Und weil die Klasse nur Getter hat, kann sie auch nicht direkt geändert werden.

Den Pool implementieren wir als statisches Attribut und überlegen uns noch einen Key für die eingegebenen Werte, damit man das Objekt bei einem 2. Zugriff auch wieder findet. Beispielhaft kann man ein array nutzen und den Key als MD5-Hash von Vor- und Nachname definieren:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
final class Person {
 
    private $firstname;
    private $lastname;
    private static $pool = array();
 
    private function __construct($firstname, $lastname) {
        $this->firstname = $firstname;
        $this->lastname = $lastname;
    }
 
    public static function valueOf($firstname, $lastname) {
        $key = md5($firstname . '_' . $lastname);
        if (!array_key_exists($key, self::$pool)) {
            self::$pool[$key] = new self($firstname, $lastname);
        }
        return self::$pool[$key];
    }
 
    public function getFirstname() {
        return $this->firstname;
    }
 
    public function getLastName() {
        return $this->lastname;
    }
 
}

Wenn wir das 2. Code Fragment nun wieder nutzen, sind die 2 Mustermänner nicht nur gleich, sondern identisch. Somit wäre das heutige Ziel erreicht.

19 Kommentare

  1. Die Frage nach dem „Warum“ beschäftigt mich da allerdings auch. Hättest du mal ein Beispiel, wo man das sinnvoll einsetzen könnte?

    Antworten
  2. Habe eine kleine Anmerkung zum letzten Code-Fragment: Dort kannst du dir auch das MD5-Hasing sparen und als Key direkt $vorname.’_‘.$nachname nehmen, da der Hash, wenn diese Werte gleich sein sollten, ja auch der gleiche ist. Von daher bringt das Hashen eigentlich nichts außer, dass es ein wenig länger braucht;)

    Antworten
  3. Seh jetzt den Vorteil zu einer Equalsmethode, à la

    $person1->equals($person2)

    nicht wirklich. Deswegen die Wiederholung der Frage: Wo ist der Anwendungsfall eines ValueObjects?

    Antworten
  4. Man erschafft sich damit statische Abhängigkeiten, die man ja eigentlich vermeiden will, und man kommt mit dem Garbage Collector in die Quere, weil die Objekte nie frei gegeben werden. Für mich ist eine equals()-Methode auch die bessere Alternative.

    Ich kenne Value-Objekte auch so, dass sie nur Werte besitzen und keinerlei Verhalten (also ausgenommen Getter/Setter keine Methoden), mehr nicht (und zumindest die englische Wikipedia gibt mir da Recht ;)). So, wie es hier erklärt wird, handelt es sich eher um das Multiton-Pattern.

    Antworten
  5. @ovnn

    Da ein Array als Keys jedoch beliebige Strings oder Integer nimmt, sollte das eigentlich kein Problem sein, oder?

    Antworten
  6. Die Prüfung über die Equals-Methode ist ein sauberer und sehr flexbiler Weg, da das jeweilige Objekt selber entscheiden kann, was als gleich anerkannt wird. Zum Beispiel wenn zwei Person.Objecte den selben Vor- und Nachnamen haben, sich aber nur die für den Vergleich unwichtigen Hobbies unterscheiden.

    Antworten
    • Man kann ein ValueObject als fachlichen Wert betrachten. D.h. in einer Anwendung besteht eine Person, eine Adresse oder ein anderer Wert zwar technisch aus Strings, ints etc. aber die Fachabteilung arbeitet nicht auf dieser Ebene. Mit einem valueObject habe ich ein Pattern an der Hand, um neue Werte zu erzeugen und mit diesen zu arbeiten. Hobbies sind bei einem Objekt irrelevant, aber wenn diese zu dem Wert Person gehören – und die Fachabteilung wird diese Definition festlegen – so sind zwei Personen mit verschiedenen Hobbies (aber gleichem Namen) 2 verschiedene Werte. Genauso wie für einen Entwickler auf der technischen Ebene 2 != 3 ist.

      Schwierig für einen Entwickler ist es zuweilen sich von der technischen Denkweise zu lösen, insbesondere wenn die Programmiersprache keine Hilfestellung gibt.

  7. Ist ein Person-Objekt dann nicht schon zu komplex und zu weit entfernt vom eigentlichen Ansatz? Müssten die Namen nicht auch „verpackt“ werden, damit sie als „Vor-“ bzw. „Nachname“ statt als String existieren?

    Antworten
    • Grunsätzlich hast du Recht. Aber die Frage ist eben, ob diese Fachwerte existieren. Wenn es den Vornamen als eigenen Wert nicht gibt, dann muss man diesen ja auch nicht als ValueObject modellieren. Beim TypeHinting sind verpackte Strings aber vielleicht ganz geschickt.

  8. Don Zampano

    09/03/2011 @ 22:20

    Value Objects definieren sich NUR über ihre Werte, daher der Name.
    Ein VO Person(‚Mustermann‘, ‚Max‘) immer identisch zu einem anderen VO Person(‚Mustermann‘, ‚Max‘), weil der Vergleich immer nur über die Werte geht. Man braucht hier keinen Pool oder irgendwelche sonstigen technischen Hilfsmittel.

    new Number(2) ist immer new Number(2), egal wie oft ich eine Instanz erzeuge.

    @KingCrunch
    VOs können niemals setter besitzen, da sie unveränderlich sind.
    Will ich aus dem VO Number(5) ein VO mit dem Wert 6 machen, so habe ich keine Methode $number->add(1) oder setValue(6), sondern ich erzeuge einfach ein neues VO mit Number(6).

    Vor allem sind ValueObjects nützlich, wenn es darum geht, Werte zu einer logischen Einheit zu verbinden.
    Eine Adresse macht i.d.R. nur Sinn, wenn alle Bestandteile vorhanden sind.
    Statt eine Adresee mit z.B. setStreet, setHousenumber, setZipcode, setCity einzeln zusammen zu setzen (und dabei evtl. die PLZ oder die Hausnummer zu vergessen) übergibt man nur die „komplette“ Einheit Adresse.
    Ändert sich die Strasse (und Hausnummer), so erzeugt man nur ein neues VO Adresse.
    Ein VO Adresse könnte sogar aus anderen VOs zusammengesetzt werden, z.B. ein eigenes VO StreetAddress($streetname, $housenumber) oder Zip($country, $zipcode).

    Die Vorteile sind vielfältig:
    – Die logische und fachliche Einheit bleibt immer erhalten.
    – Man reduziert die Anzahl der Argumente bei vielen Methoden.
    – Man reduziert bzw. ersetzt eine Vielzahl von gettern und settern (noch besser, da expliziter: addAdress, changeAddress)
    – Man arbeitet mit Typen.
    – VOs validieren sich in der Regel selbst. Man sollte niemals ein VO Number(‚xy‘) erzeugen können. Ein VO, dass nicht erstellt werden kann, weil die Werte fehlerhaft sind, wirft eine Exception.
    – Das wiederum stellt sicher, dass man eben immer mit gültigen Wertobjekten arbeitet.
    – Da VOs jederzeit erzeugt und ausgetauscht werden können, lässt sich damit sehr einfach arbeiten.

    Antworten
  9. Jörg Ohnheiser

    10/03/2011 @ 00:01

    MD5 kannst du dir wirklich sparen für das Array, Arrays sind in PHP Hashmaps also bildet PHP schon selber einen Hash.

    Antworten
    • @Jörg, Adrian: md5 kann man sich vielleicht sparen, aber ist es denn aktuell ein Nachteil? Wenn die Performance so schlecht ist, kann man das ganze ja gerne überdenken.

      @Don: Ja, VO definieren sich über ihre Werte. Aber wenn ich ohne Pool vorgehe, habe ich prinzipiell nur immutable Objects. Und ich muss dem Entwickler erklären, dass es zwar heißt 2 === 2 aber auch VO == VO. Es ist im wahrsten SInne des Wortes vorprogrammiert, dass ein Entwickler früher oder später VO === VO vergleicht und damit die Anwendung auf die Nase fällt, oder „nur“ falsche Daten zurückgibt. Nutzt man einen Objektpool, so ist das Verhalten konsistent. Wenn man nur alleine programmiert, dann passiert der Fehler vielleicht vor dem ersten Kaffee, aber VOs sind ja Domänenobjekte, die über die verschiedenen Schichten einer Anwendung genutzt werden. Und somit vielleicht sogar von unabhängigen Teams.
      Ansonsten ausführliche Ausführung mit Ausblick auf Teil 2 🙂

  10. Zu dem Hashing:

    Ich halte es tatsächlich in diesem Fall besser zu hashen als (vorname + „_“ + nachname) als key zu nehmen. Ansonsten wären folgende Fälle als Identisch angesehen:

    1: vorname: „abc_“ nachname: „def“
    2: vorname: „abc“ nachname: „_def“

    In beiden Fällen würde der key „abc__def“ entstehen, obwohl sich sowohl Vorname als auch Nachname unterscheiden. In einem solch einfachem Beispiel mit Vor- und Nachnamen sicherlich kein ernstes Problem, aber wenn man durchgängig VOs benutzt, kann es schon irgendwann zu einem solchen Sonderfall kommen.

    Ein einfaches Hashen allein löst dieses Problem aber nicht, weshalb ich meistens geschachtelt hashe. bei einem objekt mit 3 Werten beispielsweise:

    key = md5(md5(wert1) + wert2) + wert3

    Ein wirklicher Performance-Nachteil entsteht dadurch nur im Ausnahmefall, da die md5 Funktion in PHP sehr schnell arbeitet. Es sind große Iterationen nötig, damit ein echter Performancenachteil entsteht.

    Falls ich Unsinn rede lasse ich mich auch gerne eines Besseren belehren.

    Antworten
  11. Don Zampano

    10/03/2011 @ 17:19

    @Norbert
    Mit irgendetwas Irregulärem oder falscher Anwendung fällt man immer auf die Nase, das ist immer ein sehr schwaches Argument.
    Da es Domänenobjekte sind, sollte auch nicht ein Wald-und-Wiesen oder getter/setter-Entwickler [;-)] drangelassen werden.

    Abgesehen davon sollte ein VO eben auch eine Methode equals($otherVo) implementieren müssen, die den Vergleich durchführt. Entweder komplett objektorientiert oder eben nicht.
    Das kann man jedem Entwickler erklären und daran er sich nun mal zu halten.
    Nur weil man etwas vergewaltigen kann, muss man dies nicht noch zusätzlich auf technischer Ebene auf Teufel komm raus zu verhindern suchen.
    Viel wichtiger ist die Semantik eines Value Objects.

    Antworten
  12. Jörg Ohnheiser

    11/03/2011 @ 11:09

    addiks:

    Ich finde es ja besser wenn man ein Seralizierung des Objects nimmt und daraus kann man dann gerne auch einen Hash bilden.
    Dieses gibt einen eindeutigen Hash, und selbst wenn die Hash Funktion schnell ist es immer besser sie nur einmal aufrufen zu müssen.

    Entweder serialize() benutzen oder selber eine Methode dafür implementieren

    Antworten
  13. René Woltmann

    15/03/2011 @ 21:22

    ist das nicht alles ein schrei nach einem model?!
    das model kann sich selbst validieren und eine equals methode ist auch schnell implementiert..

    Antworten

Schreibe einen Kommentar

Pflichtfelder sind mit * markiert.