Mehr Spaß mit TypeScript: Klassenschnittstellen

Man kann in TypeScript ja über die new() Funktion Konstruktoren insbesondere auch in Schnittstellen referenzieren. Schauen wir uns dazu einmal folgendes Beispiel an:

interface IAnyFactory<TAny> {
new (param: string): TAny;
greeting: string;
}

class Any {
static greeting = "Hi";
constructor(private _param: string) {}
}

function create<TAny>(factory: IAnyFactory<TAny>): TAny { return new factory("test"); }
function testAny(): Any { return create(Any); }
Ich kann an die Funktion create erst einmal irgendeine Klasse übergeben, wo dann tatsächlich der Konstruktor als Parameter verwendet wird. Alle statischen Felder und Methoden einer Klasse sind aber in JavaScript einfach nur die Eigenschaften dieser Funktion. Über die Schnittstelle IAnyFactory verlangt create, dass nur solche Klassen verwendet werden dürfen, die zusätzlich auch ein statisches Feld greeting der Art string haben. Ich kann mir vorstellen, dass dieses Verhalten ganz praktisch sein kann: man hat hier quasi so etwas wie eine Schnittstellendefinition für eine Klasse!

Happy Coding

Jochen

Optionale Parameter in Methoden von TypeScript Schnittstellen

Wie immer: wenn man nachher darüber nachdenkt ist alles klar, aber manchmal ist man dann einen Moment lang doch überrascht. Nehmen wir einmal folgende TypeScript Schnittstelle:

interface ISample {
do(name?: string): void;
}

Die Methode do kann nun mit oder ohne Parameter aufgerufen werden:

extra(test: ISample): void {
test.do();
test.do(undefined);
test.do('ho');
}

Um in der Implementierung festzustellen ob ein Parameter übergeben wurde verwendet man im Allgemeinen einen Vergleich gegen undefined (oder alternativ void 0) – tatsächlich müsste man arguments.length zu Rate ziehen, um die ersten beiden Varianten sicher unterscheiden zu können. Das kann TypeScript einem aber auch abnehmen:

do(name: string = 'default'): void {
}

Genau dieser Vergleich auf undefined wird hier (ohne Rücksicht auf arguments.length, i.e. die beiden ersten Aufrufvarianten werden gleich behandelt) durchgeführt und der Parameter entsprechend belegt.

Alles das war mir im Einzelnen schon klar. Schön ist aber bei genauer Betrachtung, dass im Gegensatz zu Default Werten für Parametern in C# et al nicht der Aufrufer sondern die Implementierung entscheidet, was für ein Default verwendet werden soll. In meinem konkreten Anwendungsfall gibt es dann tatsächlich unterschiedliche Implementierungen der Schnittstelle mit unterschiedlichen Bedürfnissen, wo sich dieses Verhalten dann als sehr praktisch erwiesen hat.

Netter Seiteneffekt

Jochen

Spass mit CSS – eine Zeitlinie

Ich wollte für mich einmal versuchen, eine kleine Aufgabenstellung ohne Code (JavaScript) nur mit CSS Bordmitteln zu lösen – ok, tatsächlich ist das Beispiel hier aus einem AngularJS Projekt und etwas abstrahiert, aber vielleicht hilft ja schon die Idee. Gegeben ist eine Zeitlinie mit Datumswerten (im Beispiel auf Jahre vereinfacht) mit einer mehr oder weniger langen Beschreibung pro Datum (hier habe ich einfach aus dem deutschen Wiki ein bißchen was zu Windows zusammengesucht, die Links ins Wiki sind natürlich als Quellenreferenz im Beispiel mit drin). Erreichen will ich im Wesentlichen eine vertikale Zeitlinie mit Kurztexten (hier einfach immer nur die erste Zeile des vollen Textes), die man auf Wunsch aufklappen kann (hier ziemlich sinnfrei und dumm über ein CSS :hover gelöst – nicht praxistauglich). Wichtig ist, dass das Ein- und Ausblenden der Langtexte die Zeitlinie ohne Programmcode einigermassen erhält. Also so soll das aussehen:

Zeitlinie

Im HTML sollen dabei aber nur die eigentlichen Daten stehen, möglichst wenig Styling Schnick-Schnack, so was wie das hier:

Daten

Die Lösung sieht man dann hier – einfach mit der Maus über die Texte fahren. Der kleine Knubbel und die Linie sind einfach SVG Hintergründe (backgound-image) im CSS, geschickt positioniert und skaliert. Wird ein Text expandiert, so bleibt der Knubbel stehen und die Linie dehnt sich über die gesamte Höhe aus.

Nett und gar nicht so schwer.

Happy Coding

Jochen

Kleines Internet Explorer Ups

Ich tippe da so einfach in TypeScript eine statische Klassenvariable ein:

class Test {
   static name = "Jochen";
}

Benutzt man dann aber Test.name so erlebt man eine kleine Überraschung: im IE11 lautet der Wert Jochen, FireFox und Chrome sagen aber Test dazu. Und das gehört auch so, wie die Spezifikation sagt. Eine Warnung e.g. von TypeScript (hier 1.7) gab es aber nicht – Schade auch.

Happy Coding

Jochen

JavaScript und Schutz vor unerwünschtem Zugriff auf Objekte

Der genaue Hintergrund meiner Fragestellung führt jetzt zu weit, aber letztlich bin ich auf diesen Artikel gestoßen. Nehmen wir einmal an, dass ich eine JavaScript Klasse aus einer Bibliothek in meinem Programm unverändert verwenden möchte. Im Beispiel könnte das etwa so aussehen:

var SomeClass = function () {
    function SomeClass(value) {
        this.value = value;
    };

    SomeClass.prototype.getValue = function () {
        return this.value;
    };

    SomeClass.prototype.setValue = function (newValue) {
        this.value = newValue;
    };

    return SomeClass;
}();

Das Programm selbst bietet eine Erweiterungsschnittstelle an, über die ein Anwender sich in bestimmte Abläufe einklinken kann. Allerdings soll er im Beispiel nur lesend auf den in einer Instanz gespeicherten Klasse zugreifen können (getValue), Änderungen (setValue, value) sind alleine meinem Programm erlaubt – gerade ein direkter Zugriff auf Eigenschaften kann natürlich einiges durcheinanderwerfen.

Der oben erwähnte Artikel erklärt sehr schön, welche Alternativen man hat, um tatsächlich das gewünschte Ergebnis zu erreichen. Am mächtigsten scheint der Einsatz von Closures (JavaScript Scopes) pro Objektinstanz zu sein. Allerdings erwähnt der Autor des Artikels dabei auch, dass es zu Geschwindigkeitseinbußen kommen wird – zudem der Artikel davon ausgeht, dass man die verwendete Bibliothek verändern kann. Hier einmal ein Alternativvorschlag respektive eine Ergänzung.

Im Endeffekt soll das Programm in der Lage sein, eine Kapselung (hier als Proxy bezeichnet) zu erzeugen, die auf keinem Weg einen Zugriff auf die Objektinstanz der Bibliothek zulässt. Die Erstellung der Kapselung könnte man sich so vorstellen – Details gleich:

// Einmalig ganz am Anfang
var someClassProxy = Context.registerProxyType(SomeClass, "getValue");

// Das wäre unser Bibliotheksobjekt, vielleicht auch nur einmalig erzeugt
var lib = new SomeClass(42);

// Hier brauchen wir irgendwo tief im Code die Kapselung
var proxy = Context.createProxy(test, someClassProxy);
try {
    extensionMethod(proxy);
} finally {
    proxy.destroy();
}

Ziel ist es, dass über die Kapselung nur die Methode getValue angeboten wird und zudem die Eigenschaften des JavaScript Objektes keine Rückschlüsse auf das Bibliotheksobjekt zulassen. Das wäre dann eine mögliche Lösung:

var Context = function () {
    function Context() {
    };

    var index = 0;
    var map = {};

    Context.createProxy = function (instance, factory) {
        if (instance === null)
            return null;
        if (instance === undefined)
            return undefined;

        var proxy = factory(index++);

        map[proxy.index] = instance;

        return proxy;
    };

    Context.registerProxyType = function () {
        var args = arguments;
        var type = args[0];

        var Proxy = function () {
            function Proxy(index) {
                this.index = index;
            };

            Proxy.prototype.destroy = function () {
                delete map[this.index];
            };

            for (var i = 1; i < args.length; i++) {
                var name = args[i];
                var fn = type.prototype[name];
                if (fn === undefined)
                    throw "no method " + name;

                Proxy.prototype[name] = function (fn) {
                    return function () {
                        var instance = map[this.index];
                        if (instance === undefined)
                            throw "proxy already disconnected";

                        return fn.apply(instance, arguments);
                    };
                }(fn);
            }

            return Proxy;
        }();

        return function (index) {
            return new Proxy(index);
        };
    };

    return Context;
}();

Mit der statischen registerProxyType Methode kann ich zu einer beliebigen Klasse einen Kapselungserzeuger (Factory) erstellen, der in der Kapselung nur die gewünschten Methoden offenlegt. In der Kapselung wird auch keine Referenz auf das Bibliotheksobjekt gespeichert, sondern nur eine laufende Nummer einer aktiven Kapselung - da könnte der Anwender in seiner Erweiterung dran herumspielen, erreicht dadurch aber maximal andere aktive Kapselungen. Durch das Closure (den Scope) ist vor allem map für den Anwender unerreichbar. createProxy erzeugt zu einem beliebigen Bibliotheksobjekt eine neue Kapselung - alternative Kapselungen ein und der selben Klasse für unterschiedliche Erweiterungsszenarien sind denkbar. Durch einen laufenden Index (ebenfalls im Closure) wird die Kapselung aktiviert, ein destroy Aufruf auf der Kapselung hebt die Aktivierung wieder auf - auch hier kann die Erweiterung pfuschen aber letztlich nur ihre eigene Aufgabe erschweren, niemals aber die Infrastruktur der Bibliotheksobjekte antasten.

Ok, das ist als Studie nur ein erster Gedanke. Sollte ich es denn wirklich einmal brauchen, kann man sicher noch vieles verfeinern - e.g. muss destroy wirklich eine Methode der Kapselung sein? Aber ich hoffe, dass auch in diesem groben Ansatz zumindest die Idee insbesondere als Erweiterung des eingangs erwähnten Artikels verständlich wird.

Happy Coding

Jochen