Aaron Fischer Ingenieur, Vater, Heimwerker, Problemlöser

29 November, 2007

Lively BestOf, Volume #1

Software Engineering

Im Rahmen der Vorlesung MSWA mussten wir den Code vom Lively Kernel (ein Research-Projekt von Sun) analysieren und einen Teilaspekt bis ins kleinste Detail untersuchen und verstehen. Florian und ich nahmen uns Morph als Spezialgebiet, was sich im Nachhinein ziemlich in Arbeit ausartete. Um die heutige Abgabe zu zelebrieren, beendeten wir dieses Projekt mit einem Bierchen (danke Andreas).

Bei der Analyse des Codes (>10k Zeilen Javascript) sind uns ein paar lustige und/oder merkwürdige Stellen aufgefallen, die ich euch nicht vorenthalten möchte :) Hier also die BestOf Codezeilen von Lively.

Da alle Javascript-Objekte auch wie eine Hashmap angesprochen werden können, ist es z.B. möglich die Superklasse des Objekts zu löschen oder aber dieses tolle Konstrukt (ähnliches geht übrigens auch in PHP):

addVariable: function(varName, initialValue) {
    // functional programming is fun!
    this[varName] = initialValue;
    this[getter(varName)] = function(name) { 
        return function() { return this[name]; } 
    }(varName); // let name = varName ()

    this[setter(varName)] = function(name) {
        return function(newValue, v) {
            this[name] = newValue;
            this.changed(getter(name), v);
        }
    }(varName);
},

Auch ziemlich cool fand ich folgende Funktion:

// my kingdom for a Smalltalk block!
applyFunctionToShape: function() {
    var args = $A(arguments);
    var func = args.shift();
    func.apply(this.shape, args);
    if (this.clipPath) {
        console.log("clipped to new shape " + this.shape);
        this.clipToShape();
    }
    this.adjustForNewBounds();
}.wrap(Morph.onLayoutChange("shape")),

Der Funktion wird ein Funktionsblock und eine beliebige Anzahl Parameter übergeben (Funktionsparameter zu definieren oder zumindest in einem Kommentar zu hinterlassen was denn evtl. in die Funktion reinkommt ist was für Weicheier). Der Funktionsblock wird mit einem zusätzlichen Parameter und den restlichen Argumenten aufgerufen ($A() ist übrigens eine Prototype-Funktion die alle Argumente in ein Array schiebt) und am Ende noch mit der Funktion wrap() noch mit dem Aspekt von onLayoutChange() ausgeführt.

Allerdings war nicht alles so genial wie es jetzt den Anschein hat.

for (var i = 1; i <= vertices.length; i++) {
    var p2 = vertices[i % vertices.length];
    if (p.y > Math.min(p1.y, p2.y)) {
        if (p.y <= Math.max(p1.y, p2.y)) {
            if (p.x <= Math.max(p1.x, p2.x)) {
                if (p1.y != p2.y) {
                    var xinters = (p.y-p1.y)*(p2.x-p1.x)/(p2.y-p1.y)+p1.x;
                    if (p1.x == p2.x || p.x <= xinters)
                        counter ++;
                }
            }
        }
    }
    p1 = p2;
}

Oder das hier:

/* ca. 5 Seiten von diesen Zuweisungen */
this.images[1][0] = this.images[0][0];
this.images[1][1] = this.images[0][1];
this.images[1][2] = this.images[0][2];
this.images[1][3] = this.images[0][3];
this.images[1][4] = this.images[0][4];
//images[2] = images[1]; // DO NOT DO THIS, -> javascript nice copy-features
//images[1] = images[0];

Manche Dinge waren aber auch total sinnlos:

setToggle: function(flag) {
    this.setAttributeNS(Namespace.LIVELY, "toggle", !!flag);
},

Nachtrag: Wie sich heute in MSWA herausgestellt hat, ist die doppelte Negation doch nicht so sinnlos wie es offensichtlich scheint. Sie sorgt in diesem Fall für eine Typkonvertierung. Damit die Funktion setAttributeNS() auf jeden Fall einen Boolean als dritten Parameter bekommt, wird der Wert zuerst negiert (also zu einem echten boolschen False gemacht) und anschließend zu einem echten True. (Hintergrund: In dynamisch typisierten Sprachen wird alles was kein False ist automatisch als True interpretiert.)

Erst nach dem fünften Durchsehen des Codes wurde mir klar dass das hier eine Art Interfacedefinition sein muss:

recordChange: function(fieldName/*:String*/) {
    // Update sever or change log or something
    return;
},

Auch eine Ellegante Art Copy&Paste zu betreiben - einfach von einer anderen Prototype-Klasse klauen:

// poorman"s traits :)
bounds: PolygonShape.prototype.bounds,
vertices: PolygonShape.prototype.vertices,
inspect: PolygonShape.prototype.inspect,
setVertices: PolygonShape.prototype.setVertices,
reshape: PolygonShape.prototype.reshape,
/* ... */

... oder einfach mal bei jedem Schleifendurchgang das Array neu definieren und die i-te Position rausziehen:

for (var i = 0; i < 12; i++) {
    ... ["XII","I","II","III","IV","V","VI","VII","VIII","IX","X","XI"][i];
    /* ... */
}

Bei manchen Funktionen waren sich die Programmierer auch nicht so ganz sicher was sie da gemacht haben.

compositionWidth: function() {
    if (this.wrap === WrapStyle.NORMAL) return this.shape.bounds().width - (2*this.inset.x);
    else return 9999; // Huh??
},
Function.prototype.inspect = function() {
    return this.toString().substring(8, 88);
};
// yes yes.. so its a little laggy to add the current line and delete it...
parent.setIMText("");

... und kleine Zankereien unter den Entwicklern ist auch zu finden:

// KP: note layoutChanged will be called on addition to the tree
// DI: ... and yet this seems necessary!
this.layoutChanged();
// this code is all copied -- should be factored or, better, removed

Das leidige Thema Browserkompatibilität ...

if (newShape.pathSegList.numberOfItems != this.pathSegList.numberOfItems) {
    // ARGH, Safari doesn"t clone lists properly???
    for (var i = 0; i < this.pathSegList.numberOfItems; i++) {
        // How annoying, no way of cloning path segments
        var seg = this.pathSegList.getItem(i);
// KP: add the top morph to the world first, to make firefox happy
WorldMorph.current().addMorphAt(WindowMorph(engine, "A Lively Engine"), pt(250, 5));
engine.openAllToDnD();  // have a little fun...

Performance ist auch so ein Thema für sich. An einer ziemlich zentralen Stelle:

// TODO: there MUST be a better way to do this
// there "might" be some performance issues with this :)
while (sin < 0) sin += 360; // Can be slow...

Und wer genau hinschaut, findet sogar ein Easteregg :)

// uncomment for extra icon fun
/*
sign = NodeFactory.create("use").withHref("#GearIcon");
sign.setAttributeNS(null, "transform", "translate(-10, -10) scale(0.040)");
menuButton.addChildElement(sign);
*/

In diesem Sinne:

this.state = "shutdown"; // no one will ever know...