PHPGangsta - Der praktische PHP Blog

PHP Blog von PHPGangsta


Extended Method-Chains mit JavaScript

with 8 comments

Gastartikel von Thomas Worm

Der Autor ist 25 Jahre alt, beschäftigt sich bereits seit der Schulzeit mit der Entwicklung von Webanwendungen auf Basis von PHP, (X)HTML, CSS, JavaScript und dem Framework des Typo3 CMS. Nach Abschluss des Bachelor-Studiums der Informatik arbeitet der Autor bei der DATEV eG im Bereich Cloud Services/ASP und studiert berufsbegleitend den Informatik-Master in Hagen.

Was sind Method Chains

Als Method Chain bezeichnet man eine Kette aneinandergehänger Methodenaufrufe auf einem Objekt. Möglich wird dies dadurch, dass die Methoden das Objekt als Rückgabewert wiederliefern. Dies kann zum Beispiel sinnvoll sein, wenn Daten gefiltert, gruppiert und sortiert werden sollen:

var untermenge = datenmenge.filter('name','[ABC]*')
                           .group('umsatz').has('sum>400')
                           .orderBy('name');

Sofern man mit Mengen arbeitet, ist dies durchaus ausreichend, weil man immer eine Menge zurückbekommt und damit auch einen Rückgabewert, der interessant ist (weil er die Ergebnismenge beinhaltet).

Rückgabewert beifügen

Operiert man auf einzelnen Objekten, so könnten einzelne Rückgabewerte interessant sein. JavaScript ermöglicht es durch einen Trick, Rückgabewerte und die Anwendungsmöglichkeit der Method Chain gleichzeitig zu liefern. Dazu wird ein Wrapper-Objekt erstellt, das gleichnamige Methoden wie das ursprüngliche Objekt und den Rückgabewert enthält. Die gleichnamigen Methoden sind wiederum Wrapper in Form einer anonymen Funktion, die auf dem ursprünglichen Objekt die Methoden aufrufen und ihnen dieses ursprüngliche Objekt als this-Scope übergeben.

Zum Erzeugen dieses Wrapper-Objekts wird eine Funktion definiert:

        function methodChainReturn(object, returnValue) {
            var returnObject = function() {
                this.returnValue = returnValue;

                for (var objectMember in object) {
                    this[objectMember] = function() {
                        return object[objectMember].apply(object, arguments);
                    }
                }
            };
            return new returnObject();
        }

Diese Funktion kann nun in den Methoden eines Objektes zum Auswerfen des Rückgabewertes verwendet werden:
Wir nehmen ein – nicht unbedingt sinnvolles – Auto-Objekt als Beispiel:

        function Car(model, color) {
            this.model = model;
            this.color = color;
            this.speed = 0;
            this.gear = 0;

            this.startCar = function(gear) {
                this.gear = gear || 1;
                return methodChainReturn(this, this.gear);
            }

            this.speedUp = function(speed) {
                var newSpeed = this.speed + speed;
                if (newSpeed <= 160 || model === 'BMW')
                    this.speed += newSpeed;
                return methodChainReturn(this, this.speed);
            }

            this.speedDown = function(speed) {
                this.speed += speed;
                return methodChainReturn(this, this.speed);
            }

            this.changeGear = function(gear) {
                this.gear = gear;
                return methodChainReturn(this, this.gear);
            }
        }

Die Methoden des Objektes können nun beliebig aneinandergereicht aufgerufen werden. An der Stelle, an der der Verwender den Rückgabewert möchte, kann er die Eigenschaft returnValue auslesen. Damit muss nicht die Methode entscheiden, ob der Verwender einen Rückgabewert oder die Möglichkeit einer Method Chain bekommt, sondern der Verwender erhält beide Möglichkeiten und kann sich eine aussuchen.

        var bmw = new Car('BMW', 'black');
        var trabi = new Car('Trabi', 'green');

        console.log(
            'BMW: ', bmw.startCar()
                        .speedUp(50)
                        .changeGear(2)
                        .speedDown(20)
                        .speedUp(180)
                        .returnValue
        );

        console.log(
            'Trabi', trabi.startCar(4)
                          .speedUp(50)
                          .speedDown(20)
                          .speedUp(180)
                          .returnValue
        );

Erwartungsgemäß zeigt die Konsole


BMW:   180
Trabi: 180

Wozu sinnvoll?

Bei obigem Beispiel könnte nun die Frage aufkommen: Warum brauch ich hier den Wrapper mit einem Rückgabewert. Wäre es nicht sinnvoller ein getGear(), getSpped() im Objekt anzubieten? Ja, beim Autoobjekt hätte dies durchaus Vorteile. Aber es kann auch Objekte geben, bei denen man interne Informationen nicht unbedingt generell als öffentlich verfügbar machen möchte bzw. bei denen es semantische Unterschiede gibt. Betrachtet man beispielsweise den Teil eines StringBuilder-Objekts:

        function StringBuilder(str) {

            var privateScope = {};

            privateScope.str = str;
            privateScope.pos = 0;
            privateScope.lastFuncErrored = false;

            this.find = function(str) {
                var pos = privateScope.str.indexOf(str);
                var success = !(pos < 0);
                privateScope.lastFuncErrored = !success;
                if (success)
                    privateScope.pos = pos;
                return methodChainReturn(this, success);
            }

            this.end = function() {
                var pos = privateScope.str.length;
                privateScope.pos = pos;
                privateScope.lastFuncErrored = false;
                return methodChainReturn(this, pos);
            }

            this.write = function(val) {
                if (!privateScope.lastFuncErrored) {
                    var pos = privateScope.pos;
                    var str = privateScope.str;
                    if (pos > str.length)
                        str += val;
                    else {
                        var part1 = str.slice(0,pos);
                        var part2 = val;
                        var part3 = str.slice(pos + val.length, str.length - 1);
                        str = part1 + part2 + part3;
                    }
                    privateScope.str = str;
                    return methodChainReturn(this, str);
                }
            }

        }

Hier tritt folgender Fall ein: Die Funktion find() soll zurückgeben, ob der String gefunden wird. Wenn der String gefunden wird, soll man ihn überschreiben können. Daher wird intern die Position als Verwaltungsinformation gespeichert und ob die letzte Methode auf dem Objekt erfolgreich war. Man möchte aber nicht generell öffentlich bekannt geben, ob die letzte Methode erfolgreich war. Daher gibt es hierfür keine öffentlichen Getter. Die Funktion find() lässt sich auf zwei Arten verwenden, wodurch für den Verwender die Möglichkeit entsteht gut lesbaren Code zu schreiben:

    var stringBuilder = new StringBuilder('Das');

    console.log('Kompletter String: ',
            stringBuilder.end()
                         .write(' ist ein Test!')
                         .returnValue);

    console.log('Test wurde gefunden? ',
            stringBuilder.find('Test')
                         .returnValue);

    console.log('"ist" durch "war" ersetzt: ',
            stringBuilder.find('ist')
                         .write('war')
                         .returnValue);

Das Ergebnis sieht folgendermaßen aus:

Kompletter String:         Das ist ein Test!
Test wurde gefunden?       true
"ist" durch "war" ersetzt: Das war ein Test

Diskussion erwünscht

Zugegeben, für Manche wird das ganze sehr gekünstelt aussehen und es lässt sich streiten, ob man den Overhead und die Performance-Minderung durch ein Wrapper-Objekt in Kauf nehmen sollte. Ggfs. kann der Ansatz doch an ein oder anderer Stelle sinnvoll sein, er kann aber auch stark kritisiert werden.

Eine zusätzliche Erweiterung der methodChainReturn()-Funktion wäre insofern denkbar, dass nicht nur ein returnValue übergeben wird, sondern ein Objekt-Literal, das mehrere Rückgabewerte bereitstellt. Damit liese sich noch mehr Komfort für den Verwender erzeugen und damit ggfs. der Einsatz vorteilhafter darstellen.

Ich freue mich auf jedenfall auf Eure Kommentare.

Written by Thomas Worm

April 22nd, 2013 at 9:46 am

Posted in Javascript

Tagged with

8 Responses to 'Extended Method-Chains mit JavaScript'

Subscribe to comments with RSS or TrackBack to 'Extended Method-Chains mit JavaScript'.

  1. Da ich selber oft den Fluent Interface einsetze frage ich mich in den beispielen wozu man dazu eine eigene Wrapperklasse schreiben soll.

    Was zu einem durch diese technik flöten geht ist die Codevervollständigung der IDE’s. man bekommt überall nur noch das methodChainReturn Objekt, aber nicht das wirkliche dahinterliegende Objekt, was ich einfach sehr schade finde.
    Zudem könnte man von einer Klasse erben, die eine Methode besitzt returnValue() und diesen besagten Wert zurückgibt. Ist natürlich jeweils eine Zeile mehr zum schreiben, da man den returnValue vor dem return this; angeben müsste, dafür weiß man aber in jedem Fall was für ein Objekt man nun zurückbekommen hat.

    Marcel

    22 Apr 13 at 14:41

  2. Die dadurch verunglimpfte Codevervollständigung in den IDEs (sofern diese bei JavaScript überhaupt gut machbar ist; dynamische Objekte lassen sich halt nun einfach mal nicht gut mit Codevervollständigung ausstatten) ist natürlich etwas ärgerlich. Eventuell kann man das aber noch beheben, indem man prototype.constructor des Wrapper-Objekts neusetzt und damit der IDE einen anderen Objekttyp vorgaukelt.

    Eine Elternklasse mit returnValue()-Funktion ist vom softwarearchitektonischen Ansatz ein komplett anderer Ansatz, denn damit wird eine Verwaltungslogik in Domänenobjekte reingesetzt, die diese Verwaltungslogik garnicht als Objekteigenschaft/-verhalten tragen. Das widerspricht wohlgeformten Onjekten. Zum anderen wird dadurch der Rückgabewert von der Funktion entkoppelt und ihr nicht mehr direkt zuordenbar (vor allem wenn Nebenläufigkeit mit reinkommt. Beispiel: Thread 1 führt A->func1() aus, diese legt den Rückgabewert im Objekt ab, Thread 2 führt A->func2() aus, diese legt den Rückgabewert ab, Thread 1 ist mit A->returnValue() an der Reihe und erhält den falschen Rückgabewert.

    Da hilft das Wrapperobjekt dann doch deutlich besser an den objektorientierte Prinzipien und Konzepten festzuhalten.

    Thomas

    22 Apr 13 at 15:12

  3. Ein bisschen mehr zum Thema gibt’s hier: Designing Better JavaScript APIs

    Man möchte es in JavaScript tunlichst vermeiden seine Methoden im Constructor zu definieren. Dabei wird jede Methode für jede Instanz erneut in den Speicher geworfen. Neben initialisierungszeit kostet das, je nach Anzahl der Instanzen, auch ganz ordentlich RAM. Man möchte die Methoden stattdessen lieber an den Prototype hängen. Dabei existiert die Methode nur einmal:

    Car.prototype.speedUp = function(){};

    Dein Method-Chaining-Wrapper methodChainReturn() macht das ganze noch um einiges schlimmer. Bei jedem Aufruf wird jede Property des Objekts (sei es nun eine Funktion oder nicht) in einer weiteren Funktion gewrappt.

    Wenn ich das richtig interpretiere, machst du das alles, um „private Eigenschaften“ zu simulieren. An der Stelle darf man sich dann aber fragen, ob das wirklich notwendig ist. Dein Beispiel ist nicht falsch, aber zum Nachmachen kann ich das auch nicht empfehlen…

    Rodney Rehm

    23 Apr 13 at 00:46

  4. Der Artikel ist auf jeden Fall sehr raffiniert gedacht. Die beschriebene Technik bringt aber wenig bis nichts bei einem sehr hohen Aufwand. Besser ist es, Methoden, die das Objekt verändern zu trennen von Methoden, die einen Wert zurückgeben. Oder man erzeugt jedes Mal ein komplett neues Objekt (im funktionalen Stil).

    methodChainReturn() ist außerdem problematisch:

    1. Es ist hier kein Konstruktor nötig – statt diesem Code…
    var returnObject = function() {
    this.returnValue = returnValue;
    // etc.
    };
    return new returnObject();
    …ist dieser Code besser:
    var obj = {};
    obj.returnValue = returnValue;
    // etc.
    return obj;

    2. Per for-in-Schleife werden *alle* Properties kopiert: auch die geerbten [1] (z.B. Object.prototype.toString) und die, die keine Methoden sind.

    Bei den Beispielen kommt man leider nicht umhin, Methoden in die Instanz zu stecken, aber generell sind sie bei einem Konstructor Constr besser in Constr.prototype aufgehoben. Das spart Speicherplatz und ist schneller.

    [1] http://www.2ality.com/2011/04/iterating-over-arrays-and-objects-in.html
    [2] http://www.2ality.com/2012/01/js-inheritance-by-example.html

    Axel Rauschmayer

    23 Apr 13 at 03:21

  5. Noch ein Tipp: Statt…

    this[objectMember] = function() {
    return object[objectMember].apply(object, arguments);
    }

    …ist folgendes einfacher (nachdem man geprüft hat, ob man object[objectMember] überhaupt kopieren will):

    this[objectMember] = object[objectMember].bind(object);

    Axel Rauschmayer

    23 Apr 13 at 09:34

  6. Also ich sehe das Method Chain noch etwas kritisch. Den einzigen Vorteil der sich daraus ergibt, ist dass man das Object gleich weiter benutzen kann.
    start().machwas().stop()
    Das Problem ist, dass man als Entwickler wissen muss in welcher Reinfolge die Methoden aufgerufen werden müssen. Theoretisch könnte man auch folgendes aufruden:
    start().stop().machwas().
    Fehler kommen erst zur Laufzeit und dort auch nur, wenn dieser Codezweig angesprungen wird.
    Sinnvoller wäre ein Controller der Prüft ob die Methoden in der richtigen Reinfolge aufgerufen werden. Es wäre echt super wenn einem ein Fehler entgegen spring, wenn nach stop() noch eine Methode kommt.

    Gruß
    kritischer
    T-Rex

    T-Rex

    23 Apr 13 at 10:56

  7. Hier wäre es sinnvoller eine Instanzmethode zu erstellen die „result()“ heißt und die man immer aufrufen kann und die intern das aktuelle Ergebnis hält.

    Diese Verallgemeinerung mittels „returnValue“ macht wenig Sinn, wenn sie keinen konkreten Bezug zum Objekt hat.

    Dann muss man auch nicht diesen aufgeblasenen Wrapper aufrufen.

    Und die Methoden würde ich an das Prototype hängen sonst hast Du für jede Instanz Duplikate.

    Sam

    26 Apr 13 at 06:56

  8. Danke diese Methoden konnte ich gut gebrauchen.

    Snookertisch

    29 Nov 16 at 16:01

Leave a Reply

You can add images to your comment by clicking here.