Facebook
Twitter
Google+
Kommentare
13

Value Object Pattern

Im Artikel „Defensives Programmieren mit PHP“ hat Nils eine Lanze dafür gebrochen, Methodenparameter ausgiebig zu validieren und gegebenenfalls eine aussagekräftige Fehlermeldung zu werfen.

Nachfolgend werde ich das Beispiel „Zeit“ verwenden, um nicht Recherche- und Diskussionszeit auf die Umrechnung verschiedener Temperaturskalen zu verschwenden.  Im abgewandelten Beispiel wird folglich

function daytimeToSeconds($time) {
	if(!is_string($time)) {
		throw new InvalidArgumentException('$time should be string');
	}
	if(!preg_match("/^[0-9]{2}:[0-9]{2}:[0-9]{2}$/", $time)) {
		throw new InvalidArgumentException('$time does not match basic pattern hh:mm:ss');
	}
	$exp = explode(":", $time);
	if($exp[0]>23) {
		throw new InvalidArgumentException('hour part of $time is out of range');
	}
	if($exp[1]>59) {
		throw new InvalidArgumentException('minute part of $time is out of range');
	}
	if($exp[2]>59) {
		throw new InvalidArgumentException('second part of $time is out of range');
	}
	$seconds = $exp[0]*3600+$exp[1]*60+$exp[2];
return $seconds;
}

anstelle von

function daytimeToSeconds($time) {
	$exp = explode(":", $time);
	$seconds = $exp[0]*3600+$exp[1]*60+$exp[2];
return $seconds;
}

verwendet.

Nun ist es nicht unwahrscheinlich, dass ein entsprechendes Projekt viele Methoden hat, die eine Zeitangabe erwarten. Möchte man an all diesen Stellen defensiv vorgehen, muss man die oben genannte Validierungslogik kopieren. Dem erfahrenen Programmierer sollten aber beim Griff nach die Alarmglocken schrillen: Don’t repeat yourself!
Ein erster Ansatz wäre, die Validierung in eine separate Funktion auszulagern:

function assertIsValidTime($time) {
	if(!is_string($time)) {
		throw new InvalidArgumentException('$time should be string');
	}
	if(!preg_match("/^[0-9]{2}:[0-9]{2}:[0-9]{2}$/", $time)) {
		throw new InvalidArgumentException('$time does not match basic pattern hh:mm:ss');
	}
	$exp = explode(":", $time);
	if($exp[0]>23) {
		throw new InvalidArgumentException('hour part of $time is out of range');
	}
	if($exp[1]>59) {
		throw new InvalidArgumentException('minute part of $time is out of range');
	}
	if($exp[2]>59) {
		throw new InvalidArgumentException('second part of $time is out of range');
	}
}

Diese könnte dann an allen benötigten Stellen verwendet werden:

function daytimeToSeconds($time) {
	assertIsValidTime($time);
}

function setDaytime($time) {
	assertIsValidTime($time);
}

function daytimeToIndustrialTimeUnit($time) {
	assertIsValidTime($time);
}

function extractHourFromDaytime($time) {
	assertIsValidTime($time);
}

Im Laufe der Zeit wird man sich zudem diverse Funktionen oder Methoden schreiben, die Zeitwerte verarbeiten oder umrechnen. Sonderlich objektorientiert ist das aber nicht.

Gehen wir einen Schritt weiter und erstellen eine Klasse, die „Zeit“ enthält, ein sogenanntes „Value Object“:

class Daytime {
	private $seconds;
	function __construct($seconds) {
		if(!is_integer($seconds)) {
			throw new InvalidArgumentException('$seconds must be integer');
		}
		if($seconds<0) {
			throw new InvalidArgumentException('$seconds must be positive');
 		} 
		if($seconds>84600) {
			throw new InvalidArgumentException('$seconds must not exceed 84600');
		}
	$this->seconds = $seconds;
	}
}

Um weiterhin mit dem typischen Format arbeiten zu können, definieren wir eine statische Methode, um die Klasse von einem String instantiieren zu können und eine Klassenmethode, um sie wieder als String zurückzugeben:

static function fromTimestring($time) {
		if(!is_string($time)) {
			throw new InvalidArgumentException('$time should be string');
		}
		if(!preg_match("/^[0-9]{2}:[0-9]{2}:[0-9]{2}$/", $time)) {
			throw new InvalidArgumentException('$time does not match basic pattern hh:mm:ss');
		}
		$exp = explode(":", $time);
		if($exp[0]>23) {
			throw new InvalidArgumentException('hour part of $time is out of range');
		}
		if($exp[1]>59) {
			throw new InvalidArgumentException('minute part of $time is out of range');
		}
		if($exp[2]>59) {
			throw new InvalidArgumentException('second part of $time is out of range');
		}
		$seconds = $exp[0]*3600+$exp[1]*60+$exp[2];
	return new Daytime($seconds);
	}

	function getTimestring() {
		$hours = floor($this->seconds/3600);
		$rest = fmod($this->seconds,3600);
		$minutes = floor($rest/60);
		$seconds = fmod($rest,60);
	return sprintf("%02d:%02d:%02d", $hours, $minutes, $seconds);
	}
}

Wenn wir schon dabei sind, können wir Berechnungen in dieser Klasse unterbringen:

function addDaytime(Daytime $time) {
		$newseconds = $this->seconds+$time->$seconds;
		if($newseconds>86400) {
			throw new Exception("resulting daytime exceeds 86400 seconds");
		}
	$this->seconds = $newseconds;
	}

	function subtractDaytime(Daytime $time) {
		$newseconds = $this->seconds-$time->$seconds;
		if($newseconds<0) {
			throw new Exception("resulting daytime is negative");
		} 	$this->seconds = $newseconds;
	}

Am Beispiel der Berechnungsmethoden zeigt sich auch der enorme Vorteil dieses Ansatzes: da seit PHP 5 Type Hinting für Objekte und Arrays unterstützt wird, kann ich an dieser Stelle erzwingen, dass mir ein sinnvoller Wert übergeben wird. Kann ich mich einmal auf die Zuverlässigkeit von Daytime und ihrer Validierungen verlassen, kann ich fortan Daytime verwenden, ohne mir weitere Gedanken über die Validität der Zeitangabe machen zu müssen.
Ein weiterer Vorteil ist, dass die Zeit betreffende Funktionen in dieser Klasse an zentraler Stelle untergebracht werden können, zudem ist die Funktionalität gekapselt.
Nachfolgend noch einmal die bis jetzt programmierte Klasse:

class Daytime {
	private $seconds;
	function __construct($seconds) {
		if(!is_integer($seconds)) {
			throw new InvalidArgumentException('$seconds must be integer');
		}
		if($seconds<0) {
			throw new InvalidArgumentException('$seconds must be positive');
		}
		if($seconds>84600) {
			throw new InvalidArgumentException('$seconds must not exceed 84600');
		}
	$this->seconds = $seconds;
	}

	static function fromTimestring($time) {
		if(!is_string($time)) {
			throw new InvalidArgumentException('$time should be string');
		}
		if(!preg_match("/^[0-9]{2}:[0-9]{2}:[0-9]{2}$/", $time)) {
			throw new InvalidArgumentException('$time does not match basic pattern hh:mm:ss');
		}
		$exp = explode(":", $time);
		if($exp[0]>23) {
			throw new InvalidArgumentException('hour part of $time is out of range');
		}
		if($exp[1]>59) {
			throw new InvalidArgumentException('minute part of $time is out of range');
		}
		if($exp[2]>59) {
			throw new InvalidArgumentException('second part of $time is out of range');
		}
		$seconds = $exp[0]*3600+$exp[1]*60+$exp[2];
	return new Daytime($seconds);
	}

	function getTimestring() {
		$hours = floor($this->seconds/3600);
		$rest = fmod($this->seconds,3600);
		$minutes = floor($rest/60);
		$seconds = fmod($rest,60);
	return sprintf("%02d:%02d:%02d", $hours, $minutes, $seconds);
	}

	function addDaytime(Daytime $time) {
		$newseconds = $this->seconds+$time->$seconds;
		if($newseconds>86400) {
			throw new Exception("resulting daytime exceeds 86400 seconds");
		}
	$this->seconds = $newseconds;
	}

	function subtractDaytime(Daytime $time) {
		$newseconds = $this->seconds-$time->$seconds;
		if($newseconds<0) {
			throw new Exception("resulting daytime is negative");
		}
	$this->seconds = $newseconds;
	}
}

Optimierungsmöglichkeiten sind wie fast immer gegeben. Die Zeit-Klasse, die ich in meiner Bibliothek verwende, lässt beliebige Werte zu, inklusive negativer Zeiten. Daytime wäre dann als abgeleitete Klasse zu realisieren, die den Spezialfall „Zeitangabe innerhalb eines Tages“ abbildet.
Gemäß dieser Definition des Value Objects von Martin Fowler sollte ein Value Object zudem „immutable“, d. h. unveränderlich sein. Somit verstößt das obige Beispiel gegen diese Definition, da addDaytime/subtractDaytime das Objekt ändert.
Die Anforderung ist nicht unbegründet: da Objektinstanzen in PHP und anderen Programmiersprachen bei z. B. Übergabe als Parameter referenziert und nicht kopiert werden, kann eine Änderung des Objekts unerwünschte Seiteneffekte haben.
Auf der anderen Seite ist es für mich eben „typisch OOP“, dass eine Methode die Instanz ändert und nicht eine neue Instanz erzeugt.

Der Ansatz an sich mag manchen übertrieben erscheinen, aber meiner Ansicht nach gewinnt man mittelfristig mehr, wenn man diesen Aufwand nicht scheut:

  • man kann viele zeitbezogene Funktionen an einer Stelle versammeln, anstatt immer wieder gleiche oder ähnliche Funktionen über ein oder mehrere Projekte zu verstreuen.
  • man kann diese einzelne Komponente gut testen und so ihre Zuverlässigkeit steigern.
  • der Type-Hinting-Mechanismus von PHP kann einen gültigen Wert erzwingen; die sonstige Applikationslogik kann frei von Validatoren bleiben und wirkt aufgeräumter
  • die Repräsentation der Zeit wird im Objekt gekapselt, Programmierer müssen sich nicht darauf verständigen, welche Repräsentation sie verwenden wollen
  • der Code ist zum Teil selbstdokumentierend, da sofort deutlich wird, was eine Funktion erwartet / zurückgibt

Da dies Grundlagen der objektorientierten Programmierung sind, mag manch einer diese Ausführungen als redundante Binsenweisheit abtun. Leider habe ich in der Praxis aber viel zu oft erlebt, dass dieser Aufwand dann doch gescheut wird. Bedauerlich, so bleibt ein großer „Schatz“ an Stabilität, Sicherheit und Wartbarkeit oft ungehoben.

Über den Autor

Claus-Christoph Küthe

Kommentare

13 Comments

  1. Der Ansatz ist echt super, beim ZF hat man für diese Aufgaben auch entsprechende Klassen an Bord, so dass man sich nur an die richtige Verwendung halten muss 😉

    Ein paar Kritkpunkte habe ich aber doch 😉
    – PHP4/5 Misch-Masch (function statt public function z.B.)
    – kein richtiger Coding Style erkennbar
    – Validierung als eigene Klasse würde sich besser eignen
    – Type-Hinting wird erwähnt, aber kein Beispiel geliefert

    Ansonsten finde ich das ein sehr gutes Beispiel zu diesem Pattern, danke für den Artikel 🙂

    Reply
  2. @Dennis Da sind wir ja wieder kritisch heute 😉

    Ich finde den Artikel auch gelungen. Mehr fehlt in PHP nur eine Klassensammlung, die zum Beispiel solche einfachen „Typen“ wie Temperatur schon abgedeckt hat, dann könnten wir die alle verwenden.

    Reply
  3. Das Zend Framework bringt mit dem Zend_Measure und dem Zend_Date-Paketen einige Werteobjekte mit. Ist auf jeden einen Blick wert 😉

    Reply
  4. @Dennis:
    Die Formatierung war beschädigt worden, jetzt entspricht es meinem üblichen Standard.
    Ansonsten: das Type Hinting wird am Beispiel erwähnt (Berechnungsmethode).
    Und was meinst Du mit „Validierung als eigene Klasse würde sich besser eignen“?

    Reply
  5. @Claus

    Guter Artikel.
    Vor allem, was das Typehinting und den „intelligenten“ Ansatz von Wertbehandlungen und -berechnungen angeht.

    Auch an alle Kommentatoren: Die Zend_* Klassen sind Funktionsbibliotheken, die einen bestimmten Typ erzeugen und mit ihm rechnen können, z.B. Zend_Date.
    Aber sie sind keine ValueObjects.

    Und da muss ich dich, Claus, korrigieren.

    Fowler et al. sagen nicht, dass ein VO unveränderlich sein sollte. Vielmehr MUSS er das sein.
    Die Gründe dafür hast du schon eigentlich erwähnt, etwas seltsam, dass du sie dann doch noch aufweichst.

    Du kannst gerne eine Klasse Daytime haben, die auch Berechnungen oder Umrechnungen anstellt. Aber mit dem eigentlichen, also eigenen Wert kannst, und vor allem DARFST du nichts mehr machen, sondern musst eine komplett neue Instanz erstellen.

    Auch die Zahl 5 ist im gewissen Sinne ein VO [new Integer(5)]. Würde ich eine 1 hinzuaddieren, bekäme ich einen völlig neuen Wert. Und VO werden gekennzeichnet durch ihren Wert, der unveränderlich sein muss. Ein VO Integer könnte aber Methoden haben, um ihn z.B. in arabischen Ziffern auszugeben. Aber sein Wert ändert sich ja dadurch nicht.

    In deinem Beispiel müsstest du, wenn du addieren willst, eine neue Instanz erzeugen:
    new Daytime($oldDaytime + 340);

    Zumindest in PHP ist es kein Problem von jedem VO eine neue Instanz erzeugen zu müssen. Und empfindet man es als Problem, kann man sich auch mit VO-Registries oder Interning beschäftigen.

    Ich hatte neulich in einem anderen Kommentar auch auf eine andere Art von VOs hingewiesen, Adresse.

    Behandelt man eine Adresse als VO, dann würde die Änderung eines Teilwertes, z.B. der Strasse eine völlig neue Instanz der Adresse nach sich ziehen. Bzw. gäbe es gar keine Möglichkeit nur die Strasse zu ändern, sondern immer nur eine neue Adresse anzugeben.

    Cheers!

    Reply
  6. PS: natürlich nicht

    „Ein VO Integer könnte aber Methoden haben, um ihn z.B. in arabischen Ziffern auszugeben“

    sondern in römischen Ziffern

    😉

    Reply
  7. Hi.

    Eventuell könnte es gewünscht sein, dass man auch einstellige Studen-, Minuten- und Sekundenwerte erlaubt. Dann versagt natürlich die Regular Expression „^[0-9]{2}:[0-9]{2}:[0-9]{2}$“.

    Stattdessen wäre dann „^\d\d?:\d\d?:\d\d?$“ eine Option.

    Ansonsten sehr schön.

    Timo

    Reply
  8. @Don:
    Fowler schreibt im verlinkten Artikel wörtlich:
    „A general heuristic is that value objects should be entirely immutable“, also „sollen“.

    Ausserdem war es so, dass die Diskussion im anderen Text noch lief und ich den Fowler später reingenommen habe; ich habe mich dann dazu entschlossen, das Beispiel nicht zu ändern, sondern statt dessen die Problematik zu erörtern und es dem Leser zu überlassen, eine Position zu beziehen.

    Reply
  9. Naja…wenn man nur ein Wort übersetzt, mag das ja so sein.
    Aber:
    „should be entirely immutable“ = sollte vollständig unveränderbar sein
    Was ungefähr so viel heißt wie – muss.

    Mag sein, dass es Fowler hier höflich formuliert hat. Aber sowohl er als auch andere sind da einer Meinung: mache VOs veränderbar und du kommst in Teufels Küche.
    Hier wird er etwas deutlicher (übrigens gut passend zu deinem Beispiel):
    http://c2.com/cgi/wiki?ValueObjectsShouldBeImmutable

    Ein VO definiert sich idealerweise nur über seinen Wert (der zusammngesetzt sein kann). Ein Integer 5 ist nun mal 5, addiere ich etwas dazu ist es – auf jeden Fall nicht mehr 5.

    Aber ohne Kontext sind VOs in der Regel nicht viel Wert (wie fast alles andere auch). Zwar werden sie oft für Mengen, Zeichenketten, Beschreibungen, Geld oder Einheiten verwendet, aber der Kontext deiner Domain entscheidet darüber, ob es wirklich VOs sind.

    Wenn man sich nicht sicher ist, kann man auch die „Welche(r/s)?“-Frage stellen:

    „Claus liest ein Buch?“
    Welcher Claus?

    „Ich habe noch einen 100 Euro Schein in der Tasche.“
    Welchen denn?

    Je nachdem, wie man die Frage beantwortet oder beantworten können soll, wirst du entweder ein VO oder eine Entity daraus machen.
    Ob man Value Objects braucht, bzw. Objekte derart gestaltet und behandelt, ist fast nie eine strukturelle Frage, sondern eine konzeptionelle.

    Hope that helps.

    Reply
  10. @Don:
    Ändert für mich nichts daran, dass er „sollen“ schreibt und ich es als legitimes Stilmittel ansehe, mit einem Text nicht einfach eine Wahrheit vorzusetzen, sondern mich mit zwei Sichtweisen auseinanderzusetzen.

    Ausserdem führt Fowler einen Seiteneffekt als Beispiel an. Sowas wird man immer wieder haben, gerade in OOP. Eine daraus abzuleitende Empfehlung kann sein, Value Objects unveränderlich zu machen, ich sehe das aber nicht als eine zwingende Notwendigkeit an. Ich würde auch sagen, dass properties immer private, höchstens protected und niemals public sein sollten, aber nicht, dass sie es sein müssen.

    Reply
  11. Ja ja, alles ist relativ, ist mir schon schon klar.
    Ebenso sind absoluten Wahrheiten immer falsch. Das ist die absolute Wahrheit.
    😉

    Sorry, aber ausser der Überschrift und die Verweise auf Fowlers VO bezieht sich der Artikel nicht auf Value Objects (oder nur teilweise) und ein Value Object Pattern kenne ich nur als DTO (siehe en.wikipedia.org/wiki/Data_transfer_object), und das hat mit der Thematik nicht viel zu tun.

    Das sorgt letztendlich leider für weitere Verwirrung, davon gibt es im Buzzword- und Terminologiedschungel schon genügend.

    Dummerweise dachte ich auch, es mit dem Value Object a la Fowler, Evans et al. zu tun zu haben. Hätte mir die ganzen Kommentare sparen können 🙂

    Dennoch: der Artikel ist gut was die ursprüngliche Intention angeht, im Prinzip was ich in meinem ersten Kommentar anfangs sagte.

    Cheers!

    Reply
  12. @Claus: Sorry, hatte gestern keine ZEit mehr gehabt hier rein zu schauen 🙂

    Eine Validierung in einer eigene Klasse finde ich deshalb sinnvoller, weil man diese mehrfach und auch unterschiedlich einsetzen kann und nicht nur zu dem bisher vorgesehenen Zweck. Ist es die Aufgabe dieses Objektes den Wert zu validieren und zu speichern oder ist die eigentliche Aufgabe das Speichern des Wertes?

    Reply

Leave a Comment.

Link erfolgreich vorgeschlagen.

Vielen Dank, dass du einen Link vorgeschlagen hast. Wir werden ihn sobald wie möglich prüfen. Schließen