[ Impressum ]

Zebras Variante der Objekt-Orientierung

www.Rozek.de > Zebra > OOP
Zebra [1] bildet Teile der von Java her bekannten Objekt-Orientierung nach [2][3] - und ermöglicht dadurch insbesondere das explizite Überladen von Methoden. Für Java-Programmierer mag dies hilfreich sein, für JavaScript-Experten ist diese Variante allerdings zumindest gewöhnungsbedürftig.

Definition neuer Klassen

Anders als JavaScript unterscheidet Zebra (zumindest semantisch) zwischen Klassen und Objekten. Eine neue Klasse wird wie folgt angelegt:

var myClass = zebra.Class([
function () { /* this is a constructor */ },
function myMethod () { /* this is an instance method */ },
function $clazz () {
this.staticVariable = 0; /* this is a class property */
this.staticMethod = function () {
/* this is a class method: "this" refers to the class */
}
}
]);

Zentrale Bedeutung kommt dabei den Namen zu, die den Funktionsdefinitionen mitgegeben wurden:
  1. namenlose Funktionen werden als "Konstruktoren" angesehen,
  2. benannte Funktionen werden zu Methoden der aus der Klasse instanzierten Objekte,
  3. die spezielle Funktion $clazz wird noch während der Klassendefinition ausgeführt und legt etwaige Klassen-Variablen und -Methoden fest (im Java-Jargon heißen diese "statische" Variablen bzw. Methoden)
Wie nachstehend erläutert, dürfen Konstruktoren und Methoden "überladen" werden, d.h. es dürfen mehrere Konstruktoren bzw. mehrere Methoden desselben Namens definiert werden, solange sich diese in der Anzahl der Parameter (im JavaScript-Jargon heißt dies "arity") unterscheiden.

Anders als ("statische") Klassen-Variablen werden Objekt-Variablen nicht explizit deklariert! Sie "entstehen" erst bei Verwendung (z.B. im Rahmen der Ausführung eines Konstruktors).

Überladen von Methoden

Die vermutlich größte Besonderheit von Zebra dürfte die Möglichkeit des Überladens von (Konstruktoren und) Methoden sein. Dadurch werden z.B. folgende Definitionen möglich:

var myClass = zebra.Class([
function myMethod () { /* ... */ },
function myMethod (arg) { /* ... */ }
]);

und je nachdem, ob myMethod nun mit oder ohne Argument aufgerufen wird, kommt die zweite oder die erste Variante der Funktion zur Ausführung.

Intern legt Zebra zu diesem Zweck eine ProxyMethod an, die anhand der Anzahl der übergebenen Argumente entscheidet, welche Funktionsvariante tatsächlich aufgerufen werden soll.

Diese Vorgehensweise kostet ein wenig Rechenzeit - vor allem aber verhindert sie, daß man Methoden mit einer variablen Anzahl von Argumenten definieren kann!

Kaskadierung von Konstruktoren

Verfügt eine Klasse über mehrere Konstruktoren, so können sich diese gegenseitig aufrufen. Dies geschieht über die Hilfsmethode this.$this(...), die anhand des Namens der aufrufenden Methode (und der Anzahl der übergebenen Argumente) entscheidet, welche Oberklassen-Methode tatsächlich zur Ausführung kommen soll:

var myClass = zebra.Class([
function () { /* ... */ },
function (arg) { this.$this(); /* ... */ }
]);

Dabei ist unerheblich, an welcher Stelle eines Konstruktors der Aufruf des jeweils anderen Konstruktors erfolgt. Im Gegensatz zu Java darf der Aufruf also sogar am Ende einer Konstruktorfunktion stehen.

Allerdings darf Zebra dazu nicht im JavaScript "strict mode" [4] ausgeführt werden - ein Problem, das z.B. auch schon von Enyo her bekannt ist, und damit zusammen hängt, dass im "strict mode" nicht mehr auf die aufrufende Funktion zugegriffen werden kann.

Aufruf überladener Methoden

Auch überladene Methoden dürfen sich mithilfe von this.$this(...) gegenseitig aufrufen:

var myClass = zebra.Class([
function myMethod () { /* ... */ },
function myMethod (arg) { this.$this(); /* ... */ }
]);

Der interne Mechanismus zum Aufruf einer überladenen Methode ist derselbe wie zuvor für (überladene) Konstruktoren beschrieben - nur mit dem Unterschied, dass Konstruktoren eben keinen Namen tragen, Methoden aber sehr wohl.

Ableitung von Unterklassen

Soll eine neue Klasse von einer bestehenden Oberklasse abgeleitet werden, sieht die entsprechende Definition wie folgt aus:

var myClass = zebra.Class(BaseClass, [
/* subclass-specific constructors, methods and class variables and methods */
]);

Intern werden dabei alle zum Zeitpunkt der Unterklassen-Definition bereits bekannten Methoden der Oberklasse in die neue Unterklasse übernommen - nachträgliche Änderungen an der Oberklasse wirken sich also nicht auf deren Unterklassen aus: Zebra unterbricht die Prototyp-basierte differentielle Vererbung von JavaScript!

Konstruktoren und Methoden der Oberklasse können allerdings - wie gewohnt - in der Unterklasse überschrieben werden.

Nota bene: Klassen-Methoden und -Eigenschaften werden nicht an Unterklassen vererbt, sondern verbleiben in der Oberklasse.

Zebra unterstützt (wie Javascript auch) nur eine "einfache Vererbung", d.h., eine Unterklasse hat stets genau eine (direkte) Oberklasse. Daran ändert auch das weiter unten beschriebene Prinzip der "Schnittstellen" nichts.

Aufruf von Methoden einer Oberklasse

Zu den schöneren Besonderheiten von Zebra gehört die Möglichkeit, aus einer Unterklassen-Methode heraus die gleichnamige Methode der Oberklasse aufrufen zu können:

var myClass = zebra.Class(BaseClass, [
function myMethod () {
this.$super(); /* invokes the superclass method with the same name */
...
}
]);

Intern wertet Zebra zu diesem Zweck ebenfalls wieder den Namen der gerade laufenden Methode aus und ruft die entsprechende Methode der Oberklasse auf - auch bei diesem Aufruf greifen wieder die zuvor beschriebenen Mechanismen zur Überladung von Methoden.

Aufruf von Konstruktoren einer Oberklasse

Anders als Java erzwingen weder Zebra noch JavaScript die Initialisierung aller Oberklassen aus einer davon abgeleiteten Unterklasse heraus - Zebra stellt diese Möglichkeit aber zur Verfügung (und es sollte "zum guten Ton gehören", diese Möglichkeit auch zu nutzen):

var myClass = zebra.Class(BaseClass, [
function () {
this.$super(); /* invokes the superclass constructor */
...
}
]);

Schnittstellen

Zebra kennt den Begriff der "Schnittstellen". Anders als in Java dienen Zebra-Schnittstellen allerdings lediglich als "Hinweis" (vor allem für den Programmierer), daß Klassen mit diesen Schnittstellen bestimmte Methoden zur Verfügung stellen sollten:
  1. einem Zebra "Interface" lassen sich keine Methoden zuordnen - die Bedeutung einer Schnittstelle kann nur ihrer Dokumentation entnommen werden
  2. ein "Interface zu implementieren" bedeutet zwar in der Tat, daß man die für das Interface gedachten Methoden bereitstellen sollte - es gibt aber keinen Mechanismus, der dies vor der tatsächlichen Benutzung der Schnittstellen-Methoden überprüft
  3. Zebra "Interfaces" können auch instanziert werden - das Resultat ist aber ein leeres Objekt.
Zebra "Interfaces" werden wie folgt deklariert:

var myInterface = zebra.Interface();

Sie können entweder explizit instanziert werden:

var myInstance = new myInterface();

Oder man streut sie in die Definition einer Klasse ein:

var myClass = zebra.Class(myInterface, [
/* constructors, methods, class variables and methods */
]);

Wird die zu definierende Klasse von einer Oberklasse abgeleitet, muß die Schnittstelle hinter der Oberklasse angegeben werden:

var myClass = zebra.Class(BaseClass, myInterface, [
/* constructors, methods, class variables and methods */
]);

Der Klassen-Definition kann in beiden Fällen beliebig viele Interfaces mitgegeben werden.

Anonyme Klassen

Noch während der Instanzierung einer Klasse (insbesondere vor dem Aufruf eines Konstruktors) können die Methoden dieser Klasse überschrieben werden - Zebra nennt dieses Verfahren in Anlehnung an Java "anonyme Klassen". In der Praxis sieht dies wie folgt aus:

var myClass = zebra.Class( /* ... */ );


var customizedInstance = new myClass([
function customMethod () { /* ... */ }
]);


var anotherInstance = new myClass(/* constructor arguments */, [
function customMethod () { /* ... */ }
]);

Das Verfahren greift immer dann, wenn das letzte Argument einer Klassen-Instanzierung ein Array ist, dessen erstes Element den Typ "function" besitzt. Man sollte also bei der Definition von Konstruktor-Funktionen tunlichst darauf achten, dass
  1. als letzter Parameter kein Array erwartet wird oder
  2. dieses Array zumindest keine Funktion als erstes Element erwartet.
Anderenfalls wird die Instanzierung anders verlaufen als vorgesehen...

Bestimmung von Ableitungsbeziehungen

Da das Klassenkonzept von Zebra die Vererbungsmechanismen von Javascript durchbricht, greift auch der "instanceof" Operator nicht mehr. Möchte man die Beziehungen zwischen Zebra-Klassen ermitteln, muss stattdessen die Methode zebra.instanceOf verwendet werden:

var myClass = zebra.Class(Superclass, myInterface, [
/* ... */
]);


var myInstance = new myClass();


zebra.instanceOf(myInstance, myClass); // yields true
zebra.instanceOf(myInstance, Superclass); // yields true
zebra.instanceOf(myInstance, myInterface); // yields true
zebra.instanceOf(new myInterface(), myInterface); // yields true

Die Methode funktioniert also sowohl für Klassen als auch für Schnittstellen.

Literaturhinweise

[1]
Andrei Vishneuski
HTML5 Rich UI JavaScript Library
Zebra ist eine noch relativ neue JavaScript-Bibliothek für grafische Benutzeroberflächen in Web-Anwendungen. Diese Seite ist der primäre Anlaufpunkt, wenn Sie sich für Zebra interessieren.
[2]
Andrei Vishneuski
Easy OOP
Zebra implementiert seine eigene, an Java angelehnte, Variante der Objekt-Orientierung. Diese Seite gibt einen kurzen Überblick über die Konzepte.
[3]
Andrei Vishneuski
Easy OOP
Zebra implementiert seine eigene, an Java angelehnte, Variante der Objekt-Orientierung. Diese Seite erläutert die zugrunde liegenden Konzepte.
[4]
Eric Shepherd et al.
Strict mode - JavaScript | MDN
Seit Version 1.8.5 ist es möglich, JavaScript in einem "strict mode" auszuführen, der einige Unschönheiten und Sicherheitsmängel der Sprache beseitigt - diese Seite erläutert die Unterschiede gegenüber dem normalen Ausführungsmodus.