Frankenstein spielen: Objektmutation mit PHP
Geschrieben am 14. Mai 2009 von Cem Derin
Wagen wir Heute ein Experiment: Ich möchte eine Instanz einer beliebigen Klasse mutieren lassen. Und zwar so, dass am Ende eine Instanz einer vollkommen anderen Klasse dabei herauskommt – die Daten aber immer noch die selben sind.
Wozu man das brauchen könnte? Nehmen wir einfach mal an, wir verwenden eine Bibliothek. Eine Klasse dieser Bibliothek leiten wir ab und fügen ein oder mehrere Methoden hinzu. Korrespondierende Klassen aus der Bibliothek allerdings geben immer eine Instanz der Ursprungsklasse zurück. Wir müssten nun also alle Methoden in denen das der Fall ist kopieren, und die Ursprungsklasse durch unsere Ableitung ersetzen.
Das ist aus mehreren Gründen nicht besonders schlau: Zum einen haben wir eine ganze Menge Duplicate Code (wer das mal in den Untiefen des Zend Frameworks gemacht hat, der wird wissen, was ich meine), zum anderen haben wir ein Problem, sollte genau diese Methode von einem Update betroffen sein. Ohne es zu merken, verwenden wir eine veraltete Methode und sorgen evtl. sogar dafür, dass die Applikation nicht mehr lauffähig ist.
Mögliche Lösung: Mutation
Wäre es nicht wesentlich bequemer, wenn wir die Methode zwar ableiten, aber nur die Rückgabe der Parent-Methode entgegen nehmen und diese mutieren lassen, bevor wir sie weiterreichen?
Ein kleines Beispiel. Ein von uns verwendetes Framework hat eine Klasse Baum. Instanzen dieser Klasse haben eine Methode namens holeBlatt, welche eine Instanz der Klasse Blatt zurückliefert.
class Baum {
public function holeBlatt() {
/**
* Komplexer Algorithmus zum ermitteln des Blattes
*/
return new Blatt();
}
}
Wollen wir nun aber, dass unser Objekt bei besagter Methode eine Instanz der Klasse MeinBlatt zurückgibt, müssen wir auch den Baum ableiten. Viele würden dies wie folgt lösen.
class MeinBaum extends Baum {
public function holeBlatt() {
/**
* kompletter, komplexe Algorithmus erneut hierher kopiert
*/
return new MeinBlatt();
}
}
Wir sehen, dass wir den kompletten (imaginären) Körper der Methode mitkopieren mussten, und nur am Ende eine andere Instanz zurückgeben.
Nehmen wir nun an, wir haben eine Funktion namens mutateTo, der man eine Objektinstanz einer beliebigen Klasse sowie einen Klassennamen übergibt, und die diese dann entsprechend „mutieren“ lässt. Dann wäre folgende Vorgehensweise denkbar.
class MeinBaum {
public function holeBlatt() {
$return = parent::holeBlatt();
return mutateTo($return, 'MeinBlatt');
}
}
Viel cooler. Wir können so gut wie ohne Bedenken das Framework updaten, ohne das unser Code davon beeinflusst wird. Ist das doch der Fall, haben wir wesentlich weniger Code zu durchsuchen. Und wir haben ein paar Wiederholungen verhindert.
Einfach toll. Wo kann ich diese Funktion kaufen?
Serialisierung als Übergangszustand
Um das Objekt mutieren zu lassen, müssen wir es dummerweise erst einfrieren. Dazu serialisieren wir das Objekt. Serialisieren wir eine Instanz der Klasse Blatt, so erhalten wir eine Zeichenkette die ungefähr so aussieht.
O:5:"Blatt":0:{}
Dabei enthält das Objekt keinerlei Informationen. Nehmen wir nun aber einmal an, innerhalb des Objektes wird ein Wert gespeichert, der angibt, ob das Blatt grün oder gelb ist, dann würde unsere Zeichenkette so aussehen.
O:5:"Blatt":1:{s:9:"*_color";s:5:"Gruen";}
Hier ist also die Interne Objektinformation in unsere Ausgabe mit eingefügt worden – sie wurde serialisiert. Der Aufmerksame Leser wird bemerkt haben, dass in dieser Serialisierung ebenfalls festgehalten ist, um was für eine Objekt-Instanz es sich handelt.
Ersetzen wir Blatt doch einfach mal gegen MeinBlatt, deserialisieren die Zeichenkette wieder und sehen was passiert.
Notice: unserialize() [function.unserialize]: Error at offset 10 of 46 bytes
Oh, das hat wohl nicht geklappt. Woran könnte es liegen? Untersuchen wir die Fehlermeldung genauer, dann bemängelt die Funktion unserialize Einen Punkt mitten innerhalb unseres veränderten Klassennamens. Um genau zu sein, die Stelle, an der der ursprüngliche geendet wäre. Das liegt daran, dass vor dem Klassennamen festgehalten wird, wie lange der Klassenname ist. Die Klasse Blatt hat 5 Zeichen, MeinBlatt hingegen hat 9 Zeichen. Verändern wir also auch diesen Wert und wagen einen zweiten Versuch. Kein Fehler. Wir übrprüfen das erstelle Objekt: Und siehe da: Es handelt sich um eine Instanz unserer Klasse. Das funktioniert sogar, wenn unsere Klasse MeinBlatt gar keine Ableitung der Klasse Blatt mehr ist.
Frisch aus Dimension X: Mutagen
Man verzeihe mir den Verweis auf Helden meiner Kindheit. Wie dem auch sei. Eine konkrete Implementation einer Mutationsfunktion könnte so aussehen.
function mutateTo($object, $newClass) {
$originalClass = get_class($object);
$freezed = serialize($object);
$lookFor = 'O:'. strlen($originalClass). ':"'. $originalClass .'"';
$replace = 'O:'. strlen($newClass). ':"'. $newClass .'"';
$manipulated = substr_replace($freezed, $replace, 0, strlen($lookFor));
return unserialize($manipulated);
}
Ausprobiert, geht. Cool, das war einfach.
Das große Aber
Tja, leider gibt es immer einen Haken: Alle Referenzen auf dieses Objekt gehen flöten. Falls jemand hier eine Möglichkeit kennt: Ich würde mich freuen, wenn er mir sie nennt. Auch so würde ich mich über Feedback freuen – noch mehr sogar über aktive Mitwirkung
#001
hi cem,
ich bin ja nicht der design pattern experte, aber mir kommt das schwer nach hack vor.
Hab jetzt nicht so lange drüber nachgedacht aber spontan würd ich sagen in meiner methode wird die parent:: ausgeführt, das dann rückgegebene “Blatt” object übergebe ich an den constructor von “MeinBlatt” und dieser überschreibt einfach $this und liefert es zurück. also in code ausgedrückt:
class Baum {
function etwas(){
return $this;
}
}
class meinBaum extends Baum {
function etwas(){
$blatt = parent::etwas();
$meinblatt = new MeinBlatt($blatt);
}
}
class meinBlatt extends Blatt {
function __constructor(Blatt $blatt){
$this = $blatt;
return $this;
}
}
Wie gesagt kein anspruch auf richtigkeit bzw durchführbarkeit, nur so eine gedanke zu später stunde.
Ludwig