Fortgeschrittene OO-Programmierung in Javascript

Closures

Kapselung mit Closures

Die bisher vorgestellten Konzepte erlauben schon ein weitgehend objektorientiertes Arbeiten. Allerdings ist ein wichtiges Kernprinzip noch nicht berücksichtigt: Die Kapselung (Information Hiding). Von Haus aus kann Javascript nicht die Sichtbarkeit auf bestimmte Attribute einschränken. Weist man einem Objekt ein Attribut zu, ist es für alle lesbar und veränderbar. Um bei komplexen Objekten ein definiertes Verhalten garantieren zu können, ist eine Kapselung bestimmter Daten jedoch wichtig. Man möchte eine API definieren können, über die das Objekt benutzbar ist, während jeder weitere Zugriff technisch verboten sein soll.

Auch wenn oft Gegenteiliges behauptet wird, lässt sich dieses Prinzip in Javascript umsetzen, und zwar mit den sogenannten Closures. Eine Closure ist eine Verbindung zwischen einer Funktion und dem Kontext, der sie erzeugt hat. Konkret bedeutet das: Wenn innerhalb einer Funktion eine weitere Funktion definiert wird, ist der lokale Kontext der äußeren Funktion in der inneren zugänglich, selbst wenn die äußere schon verlassen wurde. Listing 5 zeigt dies: Die Funktion outer definiert eine lokale Variable x. Außerdem weist sie der globalen Variablen inner eine Funktion zu. Diese kann selbst dann noch auf x zugreifen, wenn outer schon lange verlassen wurde. Dieses Prinzip funktioniert auch mit noch weiter verschachtelten Funktionsdefinitionen: Eine innere Funktion kann auf alle außerhalb liegenden Kontexte zugreifen.

Mit den gerade vorgestellten Closures kann man eine echte Kapselung erreichen. Dazu werden Variablen, die privat sein sollen, als lokale Variablen im Konstruktor definiert. Alle Methoden, die auf diese Variablen zugreifen, werden ebenfalls im Konstruktor definiert. Durch das Closure-Prinzip können sie – und nur sie – auch nach Verlassen des Konstruktors auf die privaten Variablen zugreifen (weitere Details bietet David Crockford, siehe Onlineressourcen).

Listing 6 zeigt dieses Verfahren: Die Parameter und lokalen Variablen des Konstruktors sind im gleichen Kontext definiert, wie die Methoden, die darauf zugreifen. Damit sind sie für die Methoden sichtbar, nach außen hin jedoch sind sie gekapselt.

Closures machen auch das Überschreiben von Methoden eleganter. Statt umständlich den Prototyp der Superklasse zu bemühen, kann man sich nun die Supermethode in einer privaten Variablen merken und aufrufen (siehe Listing 7).

Analog zu privaten Membervariablen lassen sich private Methoden umsetzen: Dazu definiert man eine Funktion und weist sie einer lokalen Variable des Konstruktors zu. Durch die Closure können alle im Konstruktor definierten Methoden auf die private Methode zugreifen, wie Listing 8 zeigt. Dabei ist jedoch eines zu beachten: Private Methoden werden direkt aufgerufen und nicht mit dem Punkt-Operator auf einem bestimmten Objekt. Dadurch ist die implizite Variable this nicht gesetzt (sie zeigt auf das globale window-Objekt).

Dieses Problem lässt sich jedoch leicht umgehen, indem man die Referenz auf die eigene Instanz in einer privaten Variablen hält. Damit ist die Instanz in allen Methoden – inklusive der privaten – zugänglich. In Anlehnung an Smalltalk bezeichnet man eine solche Variable üblicherweise mit self. Konsequenterweise sollten Programmierer überall statt this die neue Variable self nutzen. Einziger Nachteil: self ist in Javascript außerdem ein Alias für das aktuelle Fenster (window), den die lokale self-Variable versteckt. In den Methoden ist es daher nicht mehr möglich, auf das aktuelle Fenster mit self zuzugreifen – man muss window verwenden (was sowieso der allgemein übliche Weg ist).

Mit Closures lassen sich öffentliche und private Attribute realisieren. In vielen Fällen ist eine private Sichtbarkeit jedoch zu viel des Guten – oft möchte man zwar eine saubere Kapselung zu den Benutzern einer Klasse hin erreichen, abgeleiteten Klassen jedoch trotzdem Zugriff auf die internen Methoden und Variablen gewähren. Man wünscht sich eine Sichtbarkeit, die dem protected in Java oder in C++ entspricht.

Für ein protected-Äquivalent kommen einem wieder die Closures zugute. Die Grundidee ist, in jedem Konstruktor der Vererbungskette eine private Variable zu definieren, aber mit dem Kniff, dass alle diese Variablen auf das gleiche Objekt verweisen. Dies kann als Container dienen, um Variablen und Methoden aufzunehmen. Dadurch, dass die verweisenden Variablen privat sind, ist das protected-Objekt außerhalb der Hierarchie nicht zugänglich.

Bleibt noch die Frage, wie sich die Konstruktoren das gleiche Objekt teilen können. Dazu kann man sich die Eigenart zunutze machen, dass man im Konstruktor einer Unterklasse explizit den Konstruktor der Oberklasse aufrufen muss. Indem man nun die Konstruktor-Funktionen einen Wert zurückgeben lässt, hat man einen Kommunikationsweg geschaffen: Der oberste Konstruktor kann ein Objekt erzeugen und es als Rückgabewert liefern. Der nächste Konstruktor in der Kette merkt es sich in einer lokalen Variablen und gibt es seinerseits zurück, um es an die nächste Unterklasse weiterzureichen (siehe Listing 9).

Die einzige Schwierigkeit ist, dass der Konstruktor das protected-Objekt nur zurückgeben darf, wenn er von einer Subklasse aus aufgerufen wurde – sonst würde die Erzeugung neuer Instanzen mit dem new-Operator nicht mehr korrekt funktionieren. Diesem Zweck dient in Listing 9 ein Zähler, der für jeden Aufruf in der Konstruktor-Kette erhöht wird. Nur wenn der Zähler auf 0 steht (wenn der Konstruktor-Aufruf von außerhalb der Hierarchie erfolgt) wird kein protected-Objekt zurückgegeben. Dieses Verfahren ist etwas umständlich und nicht ganz wasserdicht, das heißt es ist auf Umwegen möglich, von außen an das protected-Objekt zu gelangen. Eine saubere Lösung lässt sich nur erreichen, indem man die Klassendefinition in eine Hilfsfunktion auslagert, die dem Entwickler die Verwaltungsarbeit abnimmt und das protected-Objekt über eine weitere Closure zuverlässig schützt. Dieser oder ähnliche Ansätze dürften aber über kurz oder lang in den verbreiteten Javascript-Frameworks auftauchen, sodass man sich als Anwendungsentwickler nicht darum zu kümmern braucht.

Closures ermöglichen private- und protected-Sichtbarkeit in Javascript-Klassen. Sie vervollständigen damit die objektorientierten Eigenschaften der Sprache, und man ist nicht mehr auf halbgare Kompromisse wie Namenskonventionen angewiesen. Ein Nachteil sei allerdings nicht verschwiegen: Durch die Definition sämtlicher Methoden im Konstruktor existieren Kopien davon in jeder einzelnen Instanz. Die Methoden müssen aber im Konstruktor definiert sein, sonst erhält man keine Closure. In der Praxis ergeben sich zwar nur bei großen Klassen Schwierigkeiten, und das erst dann, wenn man es mit einer mindestens vierstelligen Anzahl an Instanzen zu tun hat. Dennoch empfiehlt es sich, für Massenobjekte die weiter oben beschriebenen Prototyp-Methoden zu verwenden, die nur einmal pro Klasse Speicher belegen.

Natürlich lassen sich der Closure- und der Prototyp-Ansatz in einer Hierarchie kombinieren. Man kann beispielsweise an der Spitze einer umfangreichen Vererbungskette mit dem Prototyp arbeiten und erst in Unterklassen, die nicht massenhaft instanziiert werden, die Kapselung mit Closures einführen. Die Unterklassen können dabei schlicht auf die (Prototyp-)Methoden der Oberklassen zugreifen.