Groovy und das Singleton-Pattern

Groovy tritt an, das Programmieren im Java-Umfeld einfacher und effizienter zu machen - und löst dieses Versprechen auch im Großen und Ganzen in beeindruckender Weise ein. Aber nicht alles wird einfacher durch Groovy, und manche gewohnten Lösungswege müssen erst einmal völlig neu durchdacht werden. Das weit verbreitete Singleton-Pattern ist hierfür ein Beispiel.

Einfache Singletons im Java-Stil

Wenn Sie ein einfaches Singleton haben wollen, also eine Klasse, von der es garantiert nur eine Instanz gibt, gehen Sie beispielsweise so vor: Sie

  • verpassen der Klasse ein privates statisches Feld, das genau die eine Instanz hält,
  • fügen eine öffentliche Methode hinzu, mit der man sich die singuläre Instanz holen kann und
  • deklarieren den Konstruktor als privat, damit er nur aus der Klasse selbst heraus, nämlich zum Erzeugen der einen Instanz, aufgerufen werden kann.

Das Ganze sieht in Groovy so aus, wie das folgende Beispiel für eine Zähler-Klasse zeigt. Sie enthält eine Integer-Variable, die bei jedem Aufruf der Methode count um eins erhöht wird:

class SomeCounter {
    def count = 0
    private static final theInstance = new SomeCounter()
 
    static getInstance(){ return theInstance }
 
    private SomeCounter() {}
 
    void count() { println "${this}: ${++count}" }
}

Das funktioniert - in Groovy wie in Java - zunächst einmal ganz wunderbar. Die Ausgabe der Zählmethode zeigt uns, dass jedes Mal dieselbe Instanz aufgerufen wird und der Zähler auch immer hochzählt.

groovy> SomeCounter.instance.count()
SomeCounter@d5c653: 1
groovy> SomeCounter.instance.count()
SomeCounter@d5c653: 2
groovy> SomeCounter.instance.count()
SomeCounter@d5c653: 3

Also alles OK? Mitnichten. Der private Konstruktor soll ja verhindern, dass ein Programmierer oder eine Programmiererin nichts Böses ahnend versehentlich die Singleton-Klasse direkt instanziiert. Aber Groovy kennt keine Privatsphäre bei Objekten (und bis zur Version 2.0 wird sich dies wohl kaum ändern). Checken wir dies:

groovy> new SomeCounter().count()
SomeCounter@cfb11f: 1
groovy> new SomeCounter().count()
SomeCounter@17577f9: 1

Da haben wir‘s. Nichts hindert Sie daran, neue Instanzen mit new zu erzeugen, anstatt den einen Zähler zu erhöhen (und nicht immer ist der Fehler so leicht zu erkennen wir hier). So geht es also nicht. Aber wie denn sonst?

Eine Verbesserung für Groovy

Eine einfache Gegenmaßnahme besteht darin, den Konstruktor sicherheitshalber prüfen zu lassen, ob bereits eine Instanz vorhanden ist, und gegebenenfalls eine Exception auslösen zu lassen.

private SomeCounter() {
  if (theInstance) throw 
    new IllegalStateException("Singleton, verwende getInstance() statt new")
}

Jetzt lässt es sich zwar immer noch nicht verhindern, dass jemand versucht, den Zähler zu instanziieren, aber zur Laufzeit gibt es eine unmissverständliche Fehlermeldung:

groovy> new SomeCounter().count()
Exception thrown: java.lang.IllegalStateException: Singleton, verwende getInstance() statt new

Damit kann man durchaus schon einmal Leben. Dass Programmierfehler, die bei Java schon den Compiler protestieren lassen, bei Groovy erst zur Laufzeit erkennbar werden, sind wir wegen des dynamischen Charakters dieser Sprache gewohnt. Exzessive Unit-Tests - die zu verwenden ohnehin sinnvoll ist - gleichen diesen Mangel aus. Wichtig aber ist, dass der Fehler überhaupt erkennbar wird, und dafür sorgt hier die ‚‚IllegalStateException‘‘.

Etwas mehr Groovy bitte

Die Lösung funktioniert, ist aber nicht so richtig groovy. Viel Eleganter geht es, wenn wir die Möglichkeiten der Groovy-Metaprogrammierung nutzen. Mit recht einfachen Mitteln können wir erreichen, dass der Konstruktor-Aufruf keine neue Instanz anlegt, sondern einfach immer dasselbe Singleton-Objekt liefert. Dafür gibt es verschiedene Möglichkeiten, wir entscheiden uns für die Verwendung der ExpandoMetaClass. Sie ermöglicht es uns, die Modifikation direkt inline vorzunehmen und erspart uns das Anlegen einer eigenen Metaklasse. Die ganze SomeCounter-Klasse sieht dann beispielsweise so aus:

class SomeCounter {
  def count = 0
  private static final theInstance = new SomeCounter()
 
  private SomeCounter() {
    SomeCounter.metaClass.constructor = {-> theInstance }
  }
 
  void count() { println "${this}: ${++count}" }
}

Im Konstruktor lassen wir uns nun die Metaklasse zu SomeCounter geben und weisen ihr einen neuen argumentlosen Konstruktor in Form einer Closure zu, die nichts weiter tut, als unsere Singleton-Klasse zurückzugeben. Und siehe da…

groovy> new SomeCounter().count()
SomeCounter@111c3f0: 1
groovy> new SomeCounter().count()
SomeCounter@111c3f0: 2
groovy> new SomeCounter().count()
SomeCounter@111c3f0: 3

Obwohl wir wiederholt new verwenden, bekommen wir immer dasselbe Objekt, und der Zähler wird brav hochgezählt.

Singletons für Groovy und Java

Für Groovy ist das mittels Metaprogrammierung implementierte Singleton perfekt. Aber was geschieht, wenn wir SomeCounter auch aus Java-Klassen heraus verwenden wollen? Ein Java-Programm kennt keine Groovy-Metaklassen und „sieht“ demzufolge nur den in der Klasse definierten „richtigen“ Konstruktor, der aber privat und daher für das Java-Programm tabu ist.

Damit man aus Java heraus überhaupt an die Singleton-Instanz herankommt, wird also die Methode getInstance() aus der ersten Version unseres SomeCounter benötigt. Fügen wir diesen ein, erhalten wir eine Singleton-Klasse, deren Instanz man aus Groovy sehr elegant über den Konstruktor und aus Java ganz traditionell über die getInstance()-Methode erhalten kann.

Allerdings könnte der unterschiedliche Zugriff auf das Singleton je nach verwendeter Programmiersprache für Verwirrung sorgen. Daher ist in gemischten Umgebungen wohl doch eher die traditionelle Variante mit der Verbesserung für Groovy zu empfehlen, bei ein versehentlicher Aufruf des Konstruktors aus Groovy heraus durch eine Runtime-Exception verhindert wird.

Die @Singleton-Annotation

Seit der Groovy-Version 1.6 gibt es eine eingebaute AST-Transformation, die genau dasselbe bewirkt wie das traditionelle Pattern mit dem abgefangenen Konstruktoraufruf (also unsere obige Empfehlung), allerdings muss man nun nichts mehr extra programmieren. Es genügt, der Klasse die Annotation @Singleton hinzuzufügen:

@Singleton
class SomeCounter {
    def count = 0
    void count() { println "${this}: ${++count}" }
}

Eine statische Methode getInstance() und ein öffentlicher Konstruktor, der eine Laufzeit-Exception auslöst, wird dann automatisch hinzugefügt.

Diskussion

Mikka, 20.10.2009:
Hi, das zweite Beispiel ist zwar recht pfiffig, aber nur solange man der einzige Benutzer der Klasse ist. Sollte man sowas in einer Lib verbauen macht man es dem Nutzer schwer! Welcher Entwickler rechnet denn damit, dass "new" eben kein neues Objekt erzeugt? Und in die Klassen der Lib schauen zu müssen sollte niemals nötig sein. Sie haben das ja weiter unten schon angedeutet, für meinen geschmack aber zu locker. Nur weil ich etwas in einer Sprache kann, ist es noch lange keine gute Lösung. Ich haben leider häufig mit verqueren Lib zu tun, welche mit der Begründung aufwarten: "weil es geht" MfG Mikka
 
JST, 20.10.2009 (20.10.2009):
Der Einwand hat seine Berechtigung, trifft aber auf viele typische Programmierpatterns (oder Antipatterns?) in Groovy zu. In diesem Fall ist es ja immerhin möglich, die Klasse entsprechend zu kommentieren, so dass die Benutzer zumindest wissen können, welches Verhalten sie von ihr erwarten können. Viel schwieriger ist es, wenn man beispielsweise per ExpandoMetaClass einem Typ oder einem einzelnen Objekt Methoden hinzufügt. Dann hilft auch der Blick in die betreffende Klasse nicht mehr, denn die Erweiterung geschieht ganz wo anders. Dynamische Programmierung kann zwar zu eleganten Programmen führen, erfordert aber erhebliche Disziplin, wenn es im größere Projekte geht und wenn man im Team arbeitet. -JST
 
HTLQG
groovy/blog/20080726_groovy_und_das_singleton-pattern.txt · Zuletzt geändert: 28.10.2009 von jst
Recent changes RSS feed Creative Commons License Donate Driven by DokuWiki