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:
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:
$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:
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.