GoF-Entwurfsmuster in JavaScript, Teil 1: Singleton

Tales from the Web side  –  31 Kommentare

Die Entwurfsmuster der Gang of Four sind eine Sammlung bewährter Lösungen zu häufig auftretenden Problemstellungen, soweit dürfte allgemein bekannt sein. Doch wie sieht es mit der Relevanz dieser Entwurfsmuster für die Sprache JavaScript aus?

Dank funktionaler und prototypischer Aspekte lassen sich viele der Problemstellungen, die durch die Entwurfsmuster adressiert werden, häufig anders lösen beziehungsweise entsprechende Entwurfsmuster auf andere Arten und Weisen implementieren.

In dieser neuen Blog-Artikelreihe möchte ich auf die einzelnen GoF-Entwurfsmuster eingehen und dabei zum einen Lösungsansätze aufzeigen, zum anderen aber auch die Relevanz des jeweiligen Musters in der JavaScript-Entwicklung diskutieren. Den Anfang macht das Singleton-Entwurfsmuster.

Beschreibung des Entwurfsmusters

In klassenbasierten Sprachen lassen sich standardmäßig von jeder Klasse mehrere Objektinstanzen erzeugen. Doch nicht immer möchte man das erlauben. Manchmal möchte man bestimmte Funktionalitäten an einer zentralen Stelle verwalten, beispielsweise Utility-Methoden, die viele andere Komponenten verwenden. An dieser Stelle kommt das Singleton-Entwurfsmuster ins Spiel: Es bewirkt, dass man von einer bestimmten Klasse nur eine Instanz erzeugen kann.

In der Regel geht man dabei so vor, dass die Singleton-Objektinstanz von der jeweiligen Klasse in einer statischen Variable verwaltet wird. Der Zugriff geschieht dann über eine öffentliche Klassenmethode getInstance(), wobei mitunter die Objektinstanz nicht schon beim Laden der Klasse, sondern erst beim ersten Aufruf von getInstance() erzeugt wird (Lazy Instantiation).

Relevanz in JavaScript

Da es in JavaScript kein Konzept von Klassen gibt, ist implizit jedes Objekt, was Entwickler erstellen, zunächst einmal schon per Definition ein Singleton. Folgendes Listing zeigt daher die einfachste Form eines Singletons in JavaScript:

var singleton = {};

beziehungsweise besser:

let singleton = {};

beziehungsweise wenn man denn so will und so konsequent ist:

const singleton = {}; 

Das hat natürlich mit dem Entwurfsmuster Singleton erst mal wenig gemeinsam, weil es zumindest keine globale Methode gibt, um auf die Objektinstanz zuzugreifen.

Singletons mit getInstance()-Methode

Häufig wird daher das Konzept der getInstance()-Methode auch auf JavaScript übertragen, wobei es verschiedene Ansätze gibt. Dazu zunächst folgender Unit-Test, der die Anforderungen definiert. Die Methode getRandomNumber() soll immer die gleiche (zu Beginn zufällig generierte) Zahl liefern:

'use strict';
const assert = require('assert');
describe('Singleton', () => {
describe('getInstance', () => {
it('should always return the same instance', () => {
assert.equal(Singleton.getInstance(), Singleton.getInstance());
});
describe('getRandomNumber', () =>
{
it('should always return same number', () =>
{
let firstResult = Singleton.getInstance().getRandomNumber();
let secondResult = Singleton.getInstance().getRandomNumber();
assert.equal(Singleton.getInstance(), Singleton.getInstance());
assert.equal(firstResult, secondResult);
});
});
});
});

Um die beiden fehlenden Aspekte des Zugriffs auf die Instanz per Methode und der Lazy Instantiation in JavaScript nachzubilden, bedient man sich in der Regel wie in folgendem Listing gezeigt dem Module-Entwurfsmuster (seinerseits eine Kombination aus einer Closure und einer Immediately-Invoked Function Expression, kurz IIFE). Die Variable Singleton stellt in diesem Fall die "Klasse" dar, init() eine private und getInstance() eine öffentliche Methode. Ist die Variable instance noch nicht definiert, wird sie beim ersten Aufruf von getInstance() über init() berechnet beziehungsweise initialisiert.

'use strict';
let Singleton = ( function () {
let instance;
function init() {
let randomNumber = Math.random();
return {
getRandomNumber: function() {
return randomNumber;
}
};
};
return {
getInstance: function () {
if(!instance) {
instance = init();
}
return instance;
}
};
})();

Singletons mit einer Self-Overwriting Function

Alternativ zu der gezeigten Technik lässt sich Lazy Instantiation unter Verwendung einer Self-Overwriting Function realisieren. Zur Erinnerung: Dabei überschreibt sich eine Funktion bei Aufruf selbst. Zu sehen ist das in folgendem Listing. Hier wird beim ersten Aufruf von getInstance() zunächst die Variable instance instanziiert und anschließend getInstance() neu definiert. Weitere Aufrufe der Funktion resultieren danach in keiner weiteren Objektinstanz, da direkt die Variable instance zurückgegeben wird.

'use strict';
let Singleton = (
function () {
return {
getInstance: function () {
// Die Instanz wird nur einmal initialisiert
let instance = function(){
let randomNumber = Math.random();
return {
getRandomNumber : function() {
return randomNumber;
}
}
}();
// Neudefinition der Funktion
this.getInstance = function() {
return instance;
}
return this.getInstance();
}
};
})();

Singletons mit Klassensyntax

Unter Verwendung der neuen Klassensyntax lässt sich ein Singleton wie in folgendem Listing implementieren. Die statische Methode getInstance() erzeugt bei erstem Aufruf die "statische" Eigenschaft instance und definiert sich selbst neu (wieder nach dem oben beschriebenen Prinzip der Self-Overwriting Function). Alle folgenden Aufrufe der Methode liefern anschließend direkt die Eigenschaft zurück.

'use strict';
class Singleton {
constructor() {
if(typeof Singleton.instance === 'object') {
return Singleton.instance;
} else {
this.randomNumber = Math.random();
Singleton.instance = this;
}
}
static getInstance() {
Singleton.instance = new Singleton();
Singleton.getInstance = function() {
return Singleton.instance;
}
return Singleton.instance;
}
getRandomNumber() {
return this.randomNumber;
}
}

Der Code innerhalb der constructor()-Methode bewirkt dabei übrigens, dass selbst für den Fall, dass versucht wird, eine Instanz über new Singleton() (und nicht über die Methode getInstance()) zu erzeugen, dennoch – falls vorhanden – die bereits erzeugte Objektinstanz verwendet wird (siehe auch folgender Unit-Test). Das sollte verhindert werden, da sich Methoden in der neuen Klassensyntax (unter anderem eben auch die constructor()-Methode) nicht als privat markieren lassen.

it('should always return the same instance', () => {
assert.equal(new Singleton(), new Singleton());
assert.equal(Singleton.getInstance(), Singleton.getInstance());
assert.equal(Singleton.getInstance(), new Singleton());
assert.equal(new Singleton(), new Singleton());
});

Fazit

Tiefer einsteigen in die GoF-Patterns und JavaScript: "Professionell entwickeln mit JavaScript: Design, Patterns, Praxistipps"

Im Falle von JavaScript sind streng genommen alle Objekte per se schon Singletons, weil es das Konzept von echten Klassen nicht (bzw. seit ES2015 auch nur oberflächlich) gibt. Dennoch gibt es verschiedene Techniken, Singletons nachzubilden. Singletons machen in JavaScript durchaus Sinn, beispielsweise um genannte Utility-Methoden an zentraler Stelle zu verwalten.

In den nächsten Artikeln dieser Reihe möchte ich nach und nach auch auf die anderen GoF-Entwurfsmuster eingehen, Implementierungen zeigen und die Bedeutung für JavaScript diskutieren. Wer sich schon jetzt für dieses Thema interessiert, dem empfehle ich mein Buch "Professionell entwickeln mit JavaScript – Design, Patterns, Praxistipps", das im Rheinwerk Verlag erschienen ist und in dem ich ein ganzes Kapitel den GoF-Entwurfsmustern gewidmet habe.