Category: JavaScript


Module Pattern in JavaScript

Juni 7th, 2009 — 4:37pm

Je weiter man sich mit JavaScript beschäftigt, desto mehr bekommt man mit was für eine interessante und vielseitige Sprache man doch mittlerweile hat. Diese Entwicklung wurde vor allem durch verschiedene JavaScript Frameworks vorangetrieben. Neben den vielen Effekthaschereien, welche Scripaculous und co. mittlerweile bieten, sind es doch vor allem die neuen Ideen und Möglichkeiten des Objekt-orientierten Programmierens, welche JavaScript für mich interessanter gemacht haben.

Im Folgenden stelle ich ein paar Optionen vor, wie man seine Objekte in JavaScript aufbauen kann. Sofern nötig, verwende ich die YAHOO User Interface Library (kurz: YUI) als Framework in den Beispielen.

Der Anfang

Ich denke, wie viele habe ich meine “Karriere” in JavaScript Programmierung mit dem Ansammeln von Funktionen begonnen:

var isClicked;

function foo() {
}

function bar() {
}

Die Probleme, die ich an diesen Ansatz für mich erkannt habe: es gibt zu wenig Struktur, man erkennt nicht, welche Funktionen Teil eines größeren, Ganzen sind und die Abbildung von komplexen JavaScript Abläufen wird sehr unübersichtlich. Außerdem fördert dieser rein Funktionsbasierende Ansatz auch die Verwendung von globalen Variablen, um z. B. den Status von Elementen festzuhalten.
Alles in allem nicht wirklich gut geeignet, es sei den für sehr triviale Anwendungen.

prototype basierende Objekte

Nachdem ich mich eine ganze Zeit lang mit o. g. Ansatz herumgeplagt habe, kam ich recht bald auf eine Art, Programme in JavaScript mittels Objekten zu strukturieren. Der erste Ansatz nutzt die prototype Eigenschaft einer Funktion aus, mit der man eine Art Klasse in JavaScript erstellen kann:

function myClass(foo, bar) {
	// das ist der "Konstruktor".
	// hier kann man die Properties eines Objekts setzen.
	this.foo = foo;
	this.bar = bar;
}

	myClass.prototype.doSomething = function() {
		// eine öffentliche Methode der Klasse
	}

	myClass.prototype.handleRollover= function(x, y) {
		// eine öffentliche Methode der Klasse
	}

//
// Verwendung:
//
var instance = new myClass("arg 1", "arg 2");
instance.doSomething();

Das Ganze ähnelt doch einem “klassischen” Objekt wie aus PHP oder C# bekannt etwas mehr. Zumindest was die Erzeugung mittels new und dem Aufruf von Methoden angeht. Man hat sogar einen Klassen-Konstruktor.

Allerdings ist auch diese Version nicht ohne Nachteile, wie ich im Laufe der Zeit feststellen musste. Zum einen ist die Syntax an sich nicht sonderlich eingängig und erfordert einiges an Tipp-Arbeit. Zum anderen sind alle Methoden einer Klasse öffentlich.

Außerdem ist es beim Hinzufügen von Event Handlern nicht ganz so einfach, den Scope, in dem die Handler Funktion ausgeführt wird, beizubehalten.
Damit eine Handler Funktion im Scope der aktuellen Instanz ausgeführt wird, muss man in der YUI folgendes verwenden:

function myClass() {
	// Hinzufügen des Event Handlers
	// Angeben, in welchem Kontext/Scope der Handler
	// ausgeführt wird (nämlich die Instanz von myClass)
	YAHOO.util.Event.on("link_to_click", "mouseover", myClass.prototype.handleRollover, null, this);
}

	myClass.prototype.handleRollover= function(e) {
		// der Event Handler
	}

Man muss also den kompletten Namen der Methode inklusive des Klassennamens und “prototype” angeben sowie mit “this” eine Referenz auf die Instanz, damit der Handler im richtigen Scope ausgeführt wird. Verwendet man statt dessen eine der folgenden Notationen:

YAHOO.util.Event.addListener(document, "click", myClass.prototype.doSomething);
// oder
YAHOO.util.Event.addListener(document, "click", this.doSomething);

Dann bezieht sich “this” innerhalb der Handlerfunktion auf document statt auf die akuelle Instanz von myClass!

Natürlich kann man das noch etwas anders machen, in dem man eine anonyme Funktion verwendet, welche dann wiederum den eigentlichen Handler aufruft:

function myClass() {
	var self = this;

	YAHOO.util.Event.on("link_to_click", "mouseover", function(e) {
		self.handleRollover(e);
	});
}

	myClass.prototype.handleRollover= function(e) {
		// der Event Handler
	}

Die Referenz auf die aktuelle Instanz wird in “var self” zwischengespeichert, weil sich “this” in der anonymen Funktion weiter unten sonst auf das document Objekt beziehen würde. Haaaack! Gerade bei vielen Event Handlern führt das einfach nur zu unübersichtlichem Code.

“Statische” Klassen

Eine andere Art, wie man in JavaScript eine Form von Klassen bekommen kann, ist die folgende:

var myClass = {
	count: 1,
	increase: function() {
		this.count++;
		alert(this.count);
	}  // <-- hier kein Komma!
};

Die Vorteile sind hier, dass die Notation recht einfach (man darf nur bei der letzten Definition eines Members das Komma nicht setzen) und auch sehr übersichtlich ist.

Jedoch haben wir genauso wie bei der vorhergenden Variante das Problem, den Scope eines Event Handlers beizubehalten. Außerdem kann keine Instanz der Klasse mit "new" erzeugt werden und es gibt auch keinen Konstruktor. Außerdem sind auch hier alle Members public.

Der wichtigste Punkt jedoch ist das Verhalten der "Klasse" an sich. Folgendes Beispiel:

var myClass = {
	count: 1,
	increase: function() {
		this.count++;
		alert(this.count);
	}
};

var instance = myClass;
instance.increase();

var instance2 = myClass;
instance2.increase();

Zunächst erwartetes Ergebnis von mir, bevor ich es ausprobierte:

Ausgabe von "2"
Ausgabe von "2"

Tatsächliches Ergebnis:

Ausgabe von "2"
Ausgabe von "3"

Die "Instanz" der Klasse ist also jedes mal exakt die Gleiche.

Nicht unbedingt schlecht. Wenn man diese Art von Verhalten erwartet bzw. braucht, z. B. für einen Cache o. ä. dann ist es sogar sehr gut, wenn man sich dieses statische Verhalten des Objekts zu Nutze machen kann.

Im Prinzip ist die Notation von var myClass = { ... } auch keine Definition einer Klasse, sondern die beiden geschweiften Klammern erzeugen bereits ein neues Objekt. var myClass = { ... } ist eigentlich nichts anderes als var myClass = new Object();

Somit erklärt sich auch dieses auf den ersten Blick vielleicht etwas verwirrende Verhalten.

Das Module Pattern

Das Module Pattern ist mein derzeit favorisiertes Muster, wie ich meine Objekte in JavaScript aufbaue. Letztlich ist es fast eine Mischung aus dem Ansatz, die prototype Eigenschaft einer function zu verwenden und der statischen Klasse aus dem vorherigen Abschnitt. Hier ein kurzes Beispiel:

var myClass = (function(aFoo) { // ein Konstruktor Argument
	// setzen von privaten Eigenschaften
	var foo = aFoo;

	function doSomething() {
		// eine öffentliche Methode
	}	

	function somePrivateFunction() {
		// eine private Methode
	}

	// "Veröffentlichen" von Membern im Rückgabewert
	return {
		doSomething: doSomething
	};
});

//
// Verwendung
//

var instance = new myClass();
instance.doSomething();

Die Vorteile bei dieser Art der Klassendefinition sind doch zahlreich. Es kann eine Instanz mittels "new" erzeugt werden und somit hat man auch die Möglichkeit einen Konstruktor mit Parametern zu nutzen. Es können private Member definiert werden, die nicht von außen aufrufbar sind. Und das definieren von Event Handler Methoden geht sehr einfach von der Hand, ohne dass der Scope des Handlers falsch gesetzt wird:

var myClass = (function() {
	// Hinzufügen des Event Listeners
	YAHOO.util.Event.addListener("link_to_click", "click", eventHandler); 

	function eventHandler(e) {
		// Event Handler
	}	

	// ...
});

Aber wie genau wird das Module Pattern definiert? Hier nochmal die Einzelteile:

Zunächst einmal ist die Klasse eigentlich nichts anderes als eine function mit Rückgabewert:

var myClass = (function(aFoo) {

	// ...

	return {
	};
});

Die Variablen aFoo wäre in dem Fall ein Konstruktorargument.

Alle Variablen, die innerhalb der äußeren Funktion definiert sind, kann man als Eigenschaften des Objekts verwenden:

var myClass = (function(aFoo) {
	// Setzen der Eigenschaft "foo"
	var foo = aFoo;

	function somePrivateFunction() {
		// Diese Eigenschaft ist aus allen Methoden zugänglich
		// ohne "this." zu verwenden.
		alert(foo);
	}

	return {
	};
});

Alle Methoden und Eigenschaften innerhalb dieser äußeren function sind zunächst einmal privat und können nicht von außen aufgerufen werden:

var myClass = (function(aFoo) {

	function doSomething() {
		// diese Methode ist noch privat
	}

	function somePrivateFunction() {
		// diese Methode ist noch privat
	}

	return {
	};
});

Aber mit Hilfe des Rückgabewerts der äußeren Funktion kann man Members "veröffentlichen":

var myClass = (function(aFoo) {

	function doSomething() {
		// diese Methode ist nicht mehr privat!
	}

	return {
		doSomething: doSomething // hier wird die Methode "veröffentlicht"
	};
});

Ein kleiner Kniff, der hilfreich ist, wenn man ein Module Pattern nicht mittels des "new" Operators erzeugen möchte:

var myInstance = (function() {
	// ...
})(); 	// man beachte die () !

Somit wird sofort eine Instanz erzeugt und in myInstance gespeichert - eine Art von statischer Klasse!

Selbstverständlich hat dieses Pattern nicht nur Vorteile. Ein Nachteil ist z. B., dass man Methoden, die öffentlich sein sollen, nochmal im Rückgabewert referenzieren muss.

Dies könnte man wie folgt vermeiden, in dem man die betreffende Funktion einfach inline in den Rückgabewert miteinbezieht:

var myClass = (function(aFoo) {
	var foo = aFoo;

	return {
		doSomething: function() {
			// ...
		}
	};
});

Dieser Fix allerdings bringt das Problem mit sich, dass die Methode nicht mehr von anderer Stelle innerhalb der Klasse aufgerufen werden kann und sieht meiner Meinung nach auch nicht sehr "sauber" aus. Daher vermeide ich die Rückgabe von inline-Methods.

Ein weiterer Nachteil am Module Pattern: der Anblick von umfangreichen Modules ist schon etwas irritieren, sofern man nicht damit gewohnt ist mit diesem Muster zu arbeiten.

Fazit

Trotz unbestrittener Nachteile ist das Module Pattern für mich die derzeit beste Art, Klassen in JavaScript zu emulieren. Es verbindet die Vorteile der verschiedenen Ansätze unter Ausnutzung der Möglichkeiten von JavaScript. Es gibt Kritiker dieses Musters, allerdings konnte ich bisher auch keinen finden, der mir eine wesentlich bessere Alternative aufzeigen konnte, die nicht auch ihre Probleme hätte.

Weitere Informationen:

Comment » | JavaScript

Back to top