Das "Module Pattern" Teil I

Exzellenter Artikel
von Frank Thürigen | 2 | 13594 Aufrufe

Anzeige Hier werben

Dies ist kein absolutes Anfängerthema, oder gar ein detailiertes Javascript Manual – für dererlei Informationen würde ich auf SelfHTML oder den nächsten Buchladen verweisen. Da ich oft nach bestimmten Javascript Techniken und Patterns gefragt werde, möchte ich hier das meiner Meinung nach nützlichste Pattern vorstellen, und da das einige Vorkenntnisse erfordert, werden diese gleich im Detail miterklärt.

Um die in diesem Artikel präsentierten Javascript Beipiele ausprobieren zu können, empfehle ich den Firefox zu installieren, sowie die Firebug Erweiterung. Zum derzeitigen Stand ist es so gut wie unmöglich AJAX Applikationen zu entwickeln ohne diese Kombination.

Wenn Sie Teile der unten vorgestellten Informationen schon kennen sollten, überspringen Sie die einfach. Ich empfehle aber die selbst dann zu überfliegen, um Fehler auszuschließen - die Folgeinformationen basieren nämlich in jedem Fall darauf. Sollten Sie auf einen Fehler stoßen, bitte ich auf jeden Fall um Information.

Runde Klammern

Es mag seltsam klingen aber "(" und ")" sind in Javascript wirklich einen näheren Blick wert. Jeder kann einfachen Code wie diesen hier verstehen:

 
JavaScript
1
    var a=(2+3);


Offensichtlich wird hier ein Variable a mit dem Wert 5 belegt. Aber was tut die Javascript engine der Browser hier genau?

Zuerst wird der "var a=" Teil geparst. Das Schlüsselwort "var" wird die engine dazu auffordern, Speicher für eine zu erstellende Variable zu reservieren. Weiterhin wird in der Liste der Variablen ein Eintrag eingefügt, der den Variablen Namen "a" enthält und einen Zeiger auf den Speicherbereich enthält der den Wert beinhaltet, in den allermeisten Fällen ein Objekt. "=" weist die Engine an, das der Wert der Variablen dann mit dem gefüllt wird, was das Ergebnis der Auswertung des Teils nach dem "=" ist. Dann sieht der Parser den Term "(2+3)". "(…)" ist eine generelle Anweisung und bedeutet "gib zurück, was das Ergebnis des hier enthaltenen Ausdrucks ist". Offensichtlich ist dies die Zahl 5, und die wird dann im Speicher der Variablen abgelegt.

Firebug Console  
JavaScript
1
2
>>> (2+3)
5

Hier sieht man es in Firebug. Im "Console" Tab unten findet man eine Zeile die mit ">>>" beginnt. Dies ist ein Eingabefeld, dessen Eingabe direkt ausgeführt wird sobald sie mit der Eingabetaste abgeschlossen wird. "(2+3)" bringt also das gewünschte Ergebnis.
Das wichtige Detail ist, daß "(…)" zurückgibt, egal was in der Klammer ist, solange es syntaktisch und logisch richtig ist.

(Abstrakte) Funktionen

Firebug Console  
JavaScript
1
2
3
4
5
6
7
>>> function a() { alert('x'); }
>>> var a = function() { alert('x'); }
>>> a
a()
>>> typeof a
"function"
>>> a()


Javascript ist eine funktionale Sprache (für Experten: sie folgt dem Lambda Kalkül wie etwa auch LISP). Daher ist es an jeder Stelle des Programms möglich eine neue Funktion "on the fly" zu erzeugen. Die ersten beiden Anweisungen hier sind equivalent und erzeugen eine Funktion namens "a" die, wenn aufgerufen, eine Message Box mit dem Inhalt "x" ausgibt. Eine Funktion kann in einer Variable gespeichert werden und verhält sich dann wie eine Variable. Wenn man in der Konsole "a" eingibt, erhält man also "a()" zurück, was eben die Funktion darstellt. "typeof a" macht es noch klarer. "a()" wird den Alert erzeugen, genau als hätte man es in irgendeinem Code ausgeführt.

Anmerkungen:
(1) "var a= function()..." und "function a()..." sind nicht 100% equivalent, es funktioniert auch "var a = function a()...". Dies ist hier aber nicht Gegenstand der Betrachtung.
(2) Außer zum debuggen sollte auf die Verwendung von "alert()", "prompt()" etc. in Ajax Programmen generell verzichtet werden.
(3) Sowohl "Function" als auch "Array" als Objekte sind direkte Abkömmlinge des "Object" Objekts. Dies nur nebenbei.

Firebug Console  
JavaScript
1
2
3
4
>>> (a)
a()
>>> ( function() { alert('x'); } )
function()

Vereinigen wir das nun mit den Betrachtungen über die runden Klammern: wenn man den Namen einer existierenden Funktion in den runden Klammern schreibt, erhält man als Ergebnis diese Funktion zurück. Wenn man die Funktion nicht vorher definiert, sondern einfach in die runden Klammern hineinschreibt, passiert etwas Seltsames: man erhält "function()" zurück. Der Unterschied zwischen beiden Resultaten ist, daß die Funktion im ersten Beispiel einen Namen hat ("a"), im Zweiten aber nicht. Alles was man darüber sagen kann, ist daß es eine Funktion ist. In beiden Fällen werden die "(…)" aber einen validen Pointer auf diese Funktion zurückgeben. Also sind dann auch folgende Zeilen absolut gültig:

Firebug Console  
JavaScript
1
2
3
>>> (a)()
>>> ( function() { alert('x'); } )()
>>> ( function( pParm ) { alert(pParm); } )( 13 )

In allen Fällen wird die alert() Box angezeigt, also die Funktion ausgeführt. Warum ist das so? Javascript wird von links nach rechts ausgeführt, also so wie man es lesen würde, also geben die ersten runden Klammern den Pointer auf die Funktion zurück - auf dem wir dann einfach "()" ausführen, was man einfach als "tu es jetzt" lesen kann. Optional können wir auch Parameter an die Funktion übergeben, wie die dritte Zeile zeigt. Der Teil "( function() { … } )" wird "Abstrakte Funktion" genannt, da wir zwar die Funktion haben - aber keinen Namen dafür.

Scope

Firebug Console  
JavaScript
1
2
3
4
5
6
7
>>> var a=5
>>> a
5
>>> var b = function() { var c=5; }
>>> b()
>>> c
c is not defined

Javascript kennt nur zwei Gültigkeitsbereiche für Variablen, hier liegt eine der häufigsten Fehlerquellen in Javascript Programmen.

Globaler Scope:

Jede Variable, die nicht innherhalb einer Funktion mit dem "var" Schlüsselwort erzeugt wird, ist eine globale Variable. Im obigen Beispiel sind also "a" und "b" global.

Functional Scope:

"c" im obigen Beispiel im obigen Beispiel ist an den funktionalen scope der Funktion "b" gebunden. Das bedeutet, daß nach der Ausführung der Funktion "b()" die Variable "c" zerstört wird und nicht mehr existiert. "c" ist nur sichtbar in der Funktion "b()".

Anmerkung hier: "Globals Are Evil" (engl.) "Globale Variablen sind böse" und sollten immer eliminiert werden.

In diese Falle kann man leicht hineinstolpern:

Firebug Console  
JavaScript
1
2
3
4
var b = function() { c=5; }
>>> b()
>>> c
5

Im Gegensatz zum vorigen Beispiel ist "c" noch da wenn die Funktion "b()" ausgeführt wurde. Dies geschah, weil das Schlüsselwort "var" bei der Definition der Variablenn vergessen wurde. Hiermit ist sie global.

Komplexere Applikationen bestehen aus einer größeren Anzahl von Variablen und Funktionen, aber die meisten Programmierer nutzen x,y,z und i,j,k standardmäßig als Zähler in Schleifen wie hier:

Beispielschleife  
JavaScript
1
2
3
4
5
function a() {
 for ( var i=1; i < 20; i++ ) {
  // tu was
  }
 }

Auch hier kann dieser Fehler auftauchen: "i" ist global falls man das Schlüsselwort "var" davor vergißt. Passiert dies einmal in einer Applikation, so kommt man damit vielleicht durch. Passiert das öfter, dann hat man irgendwann eine sehr ausgedehnte Fehlersuche vor sich. Variablen versehentlich in den Globalen Scope zu setzen ist schwer zu debuggen weil man eine ganze Menge Code durchforsten muß um die Stelle zu finden wo die Variable "ausgebrochen" ist. Natürlich, denn man verwendet ja immer dieselben Namen für die Zähler in Schleifen.

Das „Block Scope“ Pattern

Hier bringen wir wieder zwei Dinge zusammen: Abstrakte Funktionen und den funktionalen Scope. So sieht das dann aus:

Block Scope  
JavaScript
1
( function() { ... } )()

Klar das haben wir schon gesehen: die ersten runden Klammern geben die enthaltene Funktion zurück, die zweiten runden Klammern führen die Funktion aus. Die kleine Änderung hier ist daß wir das Pattern nun auf zwei Zeilen verteilen:

Block Scope mehrzeilig  
JavaScript
1
2
3
4
( function(){
 var a=5;
 // mehr code
 })();

Hätte ich "var a=5;" in normalem JS code ohne die erste und letzte Zeile geschrieben, dann wäre der Scope von "a" global gewesen, weil nicht innerhalb einer Funktion. Da er aber nun innerhalb der Funktion ist, ist a eben an den Scope dieser Funktion gebunden. Nach Ausführung dieser Funktion, also nachdem "})();" in der letzten Zeile geparst und ausgeführt ist, wird "a" nicht mehr gebraucht und zerstört.

Wofür braucht man das nun? Die Regel hier ist wiederum "Globale Variablen sind böse". Der einzige Weg die Variablen nicht global zu haben wäre sie in einem gesonderten Objekt zu speichern. Dann müßte man aber auch jedesmal, wenn man darauf zugreift, den kompletten Objektpfad mit angeben. Schreibt man aber viel Code, dann möchte man gern unnötige Tipparbeit vermeiden. Der "Block Scope" vermeidet also unnötige Tipperei auf einfache Weise, ohne daß irgendwelche Globals erzeugt werden. Weiterhin wird der Code wesentlich lesbarer.

Closures

Funktionen werden in Javascript wie Variablen behandelt. Eine Funktion kann daher beliebige Werte, und also auch ein Funktion zurückgeben. Ein Codebeispiel:

Funktionen können Funktionen zurückgeben  
JavaScript
1
2
3
4
5
6
7
8
function a(){
 return function() {
  alert('x');
  };
 }

var b=a();
b();

Ok, hier wird also eine Funktion "a" erzeugt die eine andere Funktion zurückgibt. Nebenbei, die zurückgegebene Funktion ist offensichtlich abstrakt da sie ja keinen Namen hat. Weil wir das Ergebnis der Funktion "a" der Variablen "b" zuweisen, ist "b" nun offensichtlich eine Funktion. Wenn ich nun "b();" ausführe, wird die alert() Box erscheinen.

Etwas völlig überraschendes sehen wir nun aber hier:

Closure  
JavaScript
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
function a( parm ){
 var p = parm;

 return function() {
  alert(p);
  };
 }

var b=a('test');
b();

Hier rufen wir "a()" mit einem Parameter auf. Wie wir schon wissen, ist "p" nur gültig in der Funktion "a()", weil "p" ja mit dem Schlüsselwort "var" definiert ist.
Funktion "a()" gibt also eine abstrakte Funktion zurück, und beendet sich. Das erwartete Verhalten wäre nun, daß wenn "var b=a(’test’);" ausgeführt wird, "p" den Wert "test" erhielte und dann nach Beendigung der Funktion "a()" zerstört wird. Daher hätte auch die zurückgegebene Funktion "b()" keine Ahnung von der Variable "p" - diese galt ja nur für die Laufzeit von "a()". Der Aufruf von "b()" sollte also einen Fehler erzeugen, da "p" unbekannt ist.

Tatsächlich wird der Aufruf von "b()" wunderbarerweise eine alert() Box aufmachen mit dem Inhalt "test".

Dieses Verhalten nennt man eine Closure. Die Regel ist hier, daß solange noch eine Referenz auf eine Variable existiert, diese nicht zerstört werden kann. In unserem Fall gibt "a()" eine Funktion zurück, die eine Referenz auf "p" in der "alert(p)" Anweisung enthält. Daher kann "p" nach Ablauf der Funktion "a()" nicht zerstört werden, sondern bleibt sozusagen als Anhängsel an der zurückgegebenen Funktion hängen.

Auf diese Art kann man Funktionen sozusagen konfigurieren. Genau genommen handelt es sich um eine Möglichkeit einer Funktion statische Variablen mitzugeben, d.h. Variablen die von einem Aufruf der Funktion zum nächsten Ihren Wert behalten.

Closures sind ein mächtiges Konzept, und für das nächste Pattern von großer Bedeutung…

Das „Module Pattern“ / "Singleton"

Das "Module Pattern" besteht aus zwei Techniken / Patterns die wir schon gesehen haben: dem "Block Scope" und "Closures".

Zuerst schauen wir uns ein einfaches JS Pattern namens "Singleton" an:

Singleton  
JavaScript
1
2
3
4
5
6
7
var meinObjekt = { // neues Objekt
 a: 1,
 b: 2,
 c: function(){
  return meinObjekt.a*meinObjekt.b;
  };
 }

Hier wird auf einfachste Weise, mit "object literals", ein Objekt erzeugt. Es heißt "meinObjekt", und der Rest sind einfache Zuordnungen über Name / Wert Paare, voneinander durch Kommas getrennt. "meinObjekt" kann man als simplen Container begreifen dem man zu jeder Zeit etwas hinzufügen kann oder aus dem man auch etwas löschen kann. Würde ich später "meinObjekt.d = 4;" ausführen, dann hätte ich auf diese Weise eine neue Eigenschaft "d" mit dem Wert 4 im Objekt erzeugt und verankert. Diese Art von Objekten kann man nicht als Klassen bezeichnen, denn sie haben keinen Konstruktor und können nicht instantiert werden - daher nennt man sie ein "Singleton" (engl. etwa: alleinstehend) - und nur dieser eine Container davon existiert.

Es gibt allerdings einen Nachteil dieser Betrachtungsweise: alle Methoden und Eigenschaften dieses Singleton Objektes sind von außen sichtbar. Wenn man also während der Applikationsentwicklung immer mehr Daten und Fähigkeiten in dieses Objekt einfüllt, kann man im Laufe der Zeit mit Firebug (zur Laufzeit) überhaupt nicht mehr erkennen, was jetzt nun die nach außen wichtigen Fähigkeiten sind und was eher interne Methoden. Genau dasselbe gilt für die Lesbarkeit des Codes in dieser Beziehung.

Was man also eigentlich möchte, ist daß das Objekt zwischen internen und nach außen sichtbaren Methoden und Eigenschaften unterscheiden kann. In OOP orientierten Sprachen nennt man das "private" und "public" - "privat" und "öffentlich".

Die Frage ist nun, wie fügen wir einen "Block Scope" und "Closures" zusammen, um genau diese Fähigkeit zu erhalten?

Endlich: das "Module Pattern"

Module Pattern  
JavaScript
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
var meinObjekt=( function(){
 //privat
 var b=5;

 return { // oeffentlich
  a: 1,
  c: function(){
   return this.a * b;
   }
  };
 })();

"var" erzeugt die neue Variable, "meinObjekt" ist unser Name für das Objekt. Die erste und die letzte Zeile unseres Codes enthalten den "Block Scope", und was auch immer dieser zurückgibt wird wohl der Inhalt unserer Variable.

Wie man sieht gibt der "Block Scope" ein Objekt zurück, mit der Eigenschaft "a" und der Methode "c()".

Wenn man "c()" etwas näher betrachtet, so findet man darin eine Referenz auf die Variable "b". Diese, eigentlich an die äußere Funktion gebundene Variable wird dadurch zur "Closure" (und bleibt daher für das zurückgegebene Objekt verfügbar). Dasselbe würde für Funktionen gelten, die ich in dem mit "// privat" kommentierten Teil des Codes definieren würde.

Etwas scheint hier aber seltsam. Warum muß ich "this.a" schreiben, brauche dies aber für "b" nicht?

Um dies zu erklären, muß man verfolgen was die JS Engine an dem Punkt, an dem "return this.a * b;" ausgeführt wird, weis.

(1) Beim Parsen dieses Codes: Die Engine führt gerade eine anonyme Funktion aus. Die Variable "b" wurde schon in der äußeren Funktion erzeugt, also ist sie verfügbar. Gerade wird ein Objekt generiert, das "a" und "c()" enthält. Stünde hier nun "return a * b;", so würde die Engine nach der Variable "a" in Funktion "c()" suchen - ohne Erfolg. Dann würde sie "a" im Globalen Scope suchen – auch ohne Erfolg. An der Stelle würde ein Fehler erzeugt - "a" ist offensichtlich undefiniert.

(2) Zur Laufzeit: auf der anderen Seite kann "this." zur Laufzeit ( für die Engine unkontrollierbar) auf jedes beliebige Objekt zeigen, vor allem weil man "apply()" mit jeglicher Funktion ausführen kann und so den Zeiger "this." beliebig auf ein anderes Objekt verbiegen kann. Daher findet hier überhaupt keine Prüfung zur Zeit des Parsens statt, sondern erst zur Laufzeit wenn die Funktion wirklich ausgeführt wird.

Die Hauptsache ist, daß "b" weder in Firebugs DOM Tab, noch für andere Programmteile je sichtbar sein wird, und es gibt keine Möglichkeit es zu verändern oder darauf zuzugreifen außer duch die öffentlichen Methoden. Das gilt sowohl für innere Variablen, als auch für innere Methoden.

Worauf man achten sollte...

 
JavaScript
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
var meinObjekt=( function(){
 //privat
 var b=5;

 function d(){
  return meinObjekt.a * b;
  }

 return { // oeffentlich
  a: 1,
  c: function(){
   return d();
   }
  };
 })();

Funktion "d()" ist nicht wirklich Teil des zurückgegebenen Objektes. Daher weiß "d()" zur Laufzeit nicht was der Scope von "this.*" ist. Aber, wenn "d()" ausgeführt wird, existiert "meinObjekt" ja sicher schon. Daher können wir seine Objekt Eigenschaft "a" durch "meinObjekt.a" adressieren.

apply()

Um das zu vereinfachen, kann man zum Beispiel die "apply()" Funktion nutzen:

Apply Beispiel  
JavaScript
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
var meinObjekt=( function(){
 //privat
 var b=5;

 function d(){
  return this.a * b;
  }

 return { // oeffentlich
  a: 1,
  c: function(){
   return d.apply( this );
   }
  };
 })();

Wenn die Objekt Methode "c()" aufgerufen wird, wird "this" automatisch auf das übergeordnete Objekt, also in diesem Fall auf "meinObjekt" gesetzt. "c()" weis also was "this" ist im Gegensatz zu "d()" - es sei denn wir schieben das der Funktion "d()" unter. Hierzu dient die Funktion "apply()". Das obige Beispiel bedeutet also nichts anderes als daß "d()" mitgeteilt wird was der Scope von "this" ist, und das ist das was die Funktion "c()" für "this" hält, also das Objekt. Nicht vergessen, jeder Aufruf einer Methode die in einem Objekt gespeichert ist setzt automatisch "this." auf eben dieses Objekt.

Über den Autor: Frank Thürigen
System Architekt für komplexe Javascript und FullAJAX Anwendungen in Berlin, Entwickler der twoBirds Bibliothek
Profilseite betrachten