am 2. September 2009
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.