Wie alle, die Teil 4 meine Mootools-Tutorials gelesen haben, wissen, kann man mit dem Klassensystem von Mootools sehr komfortabel objektorientiert Javascript programmieren. Und wer auch Teil 5 gelesen hat, der weiß was einem das nützt – wenn man immer schön mootoolig denkt und arbeitet, kann man sich viel Mühe ersparen indem man immer weiter auf schon vorhandene Klassen aufbaut. Das geht auch recht problemlos, sofern man nicht den Inhalt einer in der Basisklasse schon vorhandenen Methode verändern will.
Ein Beispiel: Wir haben eine Klasse Huhn
mit einer legeEi()
-Methode, die den Ei-Counter der Huhn-Instanz um 1 erhöht. Wollte man nun die legeEi()
-Methode verändern um zum Beispiel die neue Anzahl der Eier nach dem Legen auszugeben, ist man versucht das wie folgt zu machen:
// Mein Huhn legt Eier...
var Huhn = new Class({
eier: 0,
legeEi: function(){
this.eier++;
}
});
// ..soll mir aber auch mitteilen, wie viele schon gelegt sind!
Huhn.implement({
legeEi: function(){
alert(this.eier);
}
});
var meinHuhn = new Huhn();
meinHuhn.legeEi(); // Geht schief, hier kommt immer 0 raus
Das geht aber schief (Demo), weil Huhn.implement
die alte Methode komplett überschreibt – this.eier++
findet also gar nicht mehr statt. Nun könnte man natürlich die Funktionen der alten Methode einfach in sein Huhn.implement
hineinkopieren …
Huhn.implement({
legeEi: function(){
this.eier++; // Einfach aus der Basisklasse hinüberkopiert - auch doof.
alert(this.eier);
}
});
… was aber auch keine besonders gute Idee ist, denn schließlich könnte sich ja die Basisklasse ändern. Wenn das Power-Huhn in der Version 2.0 pro Arbeitsschritt zwei oder mehr Eier legen würde, würde unsere Copy-Page-Reimplementierung dieses Upgrade komplett kaputtmachen, da wir ja die komplette legeEi()
-Funktionalität ersetzen und bei uns immer nur ein Ei gelegt wird.
Die Lösung für dieses Problem ist Class.Refactor. Hiermit verschwinden überschriebene Methoden nicht einfach, sondern sind unter this.previous
weiter erreichbar. Damit löst sich unser Eierlegemethoden-Erweiterungsproblem in Rauch auf:
// Modifikation der legeEi-Methode via Class.Refactor
Huhn = Class.refactor(Huhn, {
legeEi: function(){
this.previous(); // Führe das ursprüngliche legeEi() aus
alert(this.eier);
}
});
Jetzt gibt meinHuhn.legeEi();
wie gewünscht 1 aus (Demo), da via this.previous()
die ursprüngliche legeEi()
-Methode ausgeführt und der Ei-Zähler um 1 erhöht wird … ohne jedes Copy-Paste-Gewürge! Und egal wie sich legeEi()
-Methode der Basisklasse nun in Zukunft auch ändern mag, unsere Erweiterung ist immer auf dem aktuellen Stand. Class.Refactor sei Dank.
Kommentare (8)
Thomas ¶
8. April 2010, 09:54 Uhr
Interessant, aber wo genau liegt der Unterschied zu Extend - bis auf den etwas anderen Syntax?
Könnte ich das gleiche nicht auch folgendermaßen lösen:
Peter ¶
8. April 2010, 10:03 Uhr
Zitat Thomas:
Richtig, aber mit Class.Refactor kann man auch die bestehende Klasse verändern ohne eine neue zu erstellen:
Ich werde mal das Beispiel im Artikel anpassen, das ist wirklich etwas suboptimal. Danke für den Hinweis :)
Tim Baumann ¶
8. April 2010, 18:25 Uhr
Und jetzt in bloßem JavaScript:
Demo
Und was ist da jetzt der Vorteil von MooTools?
Und warum verschluckt das Kommentarfeld Pluszeichen?
Peter ¶
8. April 2010, 19:46 Uhr
Zitat Tim Baumann:
Ist schon klar dass man auch ohne Frameworks Sachen bauen kann. Aber warum sollte man das (sofern es komplexer als das Huhn-Beispiel ist) wollen? Oder warum mixen?
Das hingegen ist in der Tat eine interessante Frage. Hm... vermutlich nur ein Bug in meinem Vorschau-Plugin. Werde das mal erforschen.
molily ¶
8. April 2010, 20:14 Uhr
Vielleicht sollte man dazu sagen, was Mootools intern macht. Es wrappt die Funktion mit einer weiteren, um this.previous() für die Dauer des Aufrufs bereitzustellen. Jetzt muss man wissen, dass Mootools ohnehin schon jede Funktion einer »Klasse« wrappt, um this.super() zu ermöglichen (selbst wenn man gar nicht vererbt). Das heißt, dass mit Class.refactor überschriebene Methoden gleich zweimal gewrappt sind. Wenn man jetzt noch Binding mit method.bind/bindAsEvent benötigt, etwa bei Event-Handlern, setTimeout/setInterval oder Callbacks, ist man bei drei Wrappern und zwei Payload-Funktionen, also fünf Funktionsaufrufen. Solch gewrappte Methoden sollte man nicht häufig ausführen, sonst leidet die Performance darunter.
Dieser Overhead ist bei (pseudo-)klassenbasierter OOP in JS halt nötig. Für einfache Anwendungen, bei denen nicht viele Eier gelegt werden, ist es natürlich kein Problem, wenn aus einem Funktionsaufruf drei werden. Bei einer größeren, performancekritischen Anwendung hingegen habe ich mit solchen OOP-Hilfsmitteln angefangen, aber habe sie dann nach und nach herausgeworfen, da Funktionsaufrufe immer noch unheimlich kosten. Closures lege ich nur noch dort an, wo es unbedingt nötig ist. Siehe auch http://molily.de/weblog/closures-performance
Peter ¶
8. April 2010, 21:58 Uhr
Natürlich gibt es da messbare Performance-Unterschiede. Aber in 99,99% aller Fälle ist das Zeitbudget für Performanceoptimierungen beim DOM besser aufgehoben als bei sowas. Wenn ein Script auf einer Website so langsam ist, dass es wirklich auffällt, liegt das nie an zu vielen Closures.
Tim Baumann ¶
9. April 2010, 10:38 Uhr
Ich finde es schade, dass es für viele Frameworks so viele interessante Plugins gibt, die aber eben nur mit dem Framework laufen. Besonders schade ist das, wenn es so einfach wäre, die selbe Funktionalität mit normalen JavaScript zu erreichen.
molily ¶
9. April 2010, 11:04 Uhr
Peter, das stimmt für Scripte, die hauptsächlich DOM-Manipulationen machen. Dort ist das DOM meist auch das Bottleneck. Nun wird JavaScript nicht mehr nur für DOM-Scripting verwendet (und Mootools übrigens auch serverseitig). Bei HTML5-Anwendungen wird rechenintensive Logik und Datenverarbeitung auf den Client ausgelagert. Zum Beispiel bei Canvas-Interfaces: Bei hunderten Objekten, die mit einer annehbaren Framerate gezeichnet werden, macht es einen Unterschied, ob pro Objekt eine oder drei Funktionen pro Frame aufgerufen werden. Das ist natürlich noch der Ausnahmefall.