Das Singleton Design Pattern

Studienprojekt von Philipp Hauer. 2009 - 2010. ©

Inhalt

Einführung

Wir werden gebeten, ein bestehendes Bankverwaltungsprogramm zu überarbeiten, da dieses in der Vergangenheit häufig fehlerhaft und langsam gearbeitet hat. Ein Fehler sei, dass die eingenommenen Kontoführungsgebühren bei Stammkunden immer weniger zu werden scheinen. Beim Durcharbeiten des Quellcodes treffen wir auf folgende Klasse.

Singleton Einführung Negativbeispiel

Bedenkliche Klasse BankWerte:

class BankWerte {

    public static double kontenZinsen = 0.0;

    public static int kontenTransaktionsvolumen = 1000;

    public static int kontenGebuehren = 10;

    public static int kontenDispositionskredit = -500;

    public static Bilanz bilanz = berechneBilanz(){
    }//aufwendige Methode
}			

Diese Klasse stellt global verfügbare Variabeln bereit. Überall im Bankverwaltungscode (von Kontenerstellung bis zum internen Monitoring der Geschäftsprozesse) verstreut, werden diese Variablen verwendet. Natürlich werden die Charakteristika eines Kontos (Zinsen, Gebühren, Dispo etc.) auch verändert. Leider hat die globale Verfügbarkeit die vergangenen Entwickler dazu verleitet, die Modifizierung der Variablen an vielen verschiedenen Stellen durchzuführen - dort wo es gerade am bequemsten war. Nach langen Fehlersuchen im Spagetticode fanden wir folgende Modifikationen:

Manipulation der globalen Variablen ohne Plausibilitätsprüfung:

if (konto.status == "Stammkunde"){
    kontenGebuehren -= 1;
}			

Stammkunden erhalten in regelmäßigen Abständen Rabatt auf ihre Kontoführungsgebühren. Leider hat der Entwickler vergessen, Plausibilitätsprüfungen einzubauen.

  • Es wird nicht sichergestellt, dass eine Variable nicht unter einen bestimmten Wert fällt oder gar unsinnige Werte (negative Zahlen) angenommen werden.
  • An allen Stellen, an denen die Variable verändert wird, muss immer wieder eine Plausibilitätsprüfungen ausprogrammiert werden. Unnötiger, redundanter Code entsteht, der schnell inkonsistent und damit fehlerhaft werden kann.

Es bedarf einem Mechanismus, welche die Werte bereitstellt und den Zugriff auf diese kontrolliert. Um das bestehende Bankverwaltungsprogramm nicht komplett neu entwerfen zu müssen, soll auch dieser Mechanismus global verfügbar sein. Vernünftig ist dies zwar nicht (siehe Nachteile), aber hinsichtlich der beschriebenen Punkte zielführend.

Es wird eine Klasse erstellt, deren Objekte die Variablen zugriffsgeschützt hält und den Zugriff mittels Getter und Setter kontrolliert. Da die Daten zentral verwaltet werden sollen, und damit nicht mehrere Objekte verschiedene Werte tragen dürfen, soll nur ein Objekt auf einmal instanziiert werden können. Es soll ein Einzelstück (engl. Singleton) sein.

Dazu wird der Konstruktor privatisiert, sodass er nur noch im BankWerte-Code selbst aufgerufen werden kann. Der Konstruktoraufruf erfolgt statisch zur Zeit des Klassenladens.

Singleton Einleitung Lösung

Die Klasse BankWerte wird zum statisch verfügbaren Einzelstück und kapselt die sensiblen Werte:

class BankWerte {

    //verhinderte Instanziierung von außen.
    private BankWerte() {

    }

    //die einzigartige Instanz
    private static BankWerte einzigartigeBankwerte = new BankWerte();

    //globale Methode zum Erhalten der einen Instanz.
    public static BankWerte getInstance() {
        return einzigartigeBankwerte;
    }

    //Aus öffentlichen, statischen Variablen wurden private Instanzvariablen
    private double kontenZinsen = 0.0;

    private int kontenTransaktionsvolumen = 1000;

    private int kontenGebuehren = 10;

    private int kontenDispositionskredit = -500;

    private Bilanz bilanz = berechneBilanz() {
        Bilanz bilanz;
        //TEURE Methode
        //...
        return bilanz;
    }
    
    //Setter mit Plausiblitätsprüfung
    public void setKontenZinsen(double kontenZinsen) {
        if (kontenZinsen > 0 && kontenZinsen < 4){
            kontenZinsen = pKontenZinsen;
        }
    }

    public void setKontenGebuehren(int kontenGebuehren) {
        if (kontenGebuehren > 0 && kontenGebuehren < 50){
            kontenGebuehren = pKontenGebuehren;
        }
    }

    //weitere Getter und Setter...
}
			
Benutzung der neuen, robusten BankWerte-Klasse:

//globaler Zugriff auf einzigartige Instanz
BankWerte bankWerte = BankWerte.getInstance();
//Zugriff über Methoden
bankWerte.setKontenGebuehren(15);
//Zentrale Plausiblitätsprüfung
bankWerte.setKontenZinsen(-20);			

Damit ist das Problem der groben Fehleranfälligkeit behoben. Bleibt das Performanceproblem. Es stellt sich heraus, dass viele Applikationssitzungen von der BankWerte-Klasse keinen Gebrauch machen. Trotzdem werden komplizierte und ressourcenintensive Bilanzberechnungen bei jedem Start durchgeführt. Das liegt darin, dass das Einzelstück zum Zeitpunkt des Klassenladens erstellt wird (static). Ob nun die Bilanz später gebraucht wird oder nicht - sie wird immer im Zuge der BankWerte-Instanziierung miterstellt.

Wir optimieren den Code dahingehend, dass erst beim ersten Aufruf von getInstance() das Einzelstück instanziiert wird.

BankWerte mit verzögertem Laden:

class BankWerte {

    //solange nicht benutzt, wird das Einzelstück nicht instanziiert.
    private static BankWerte einzigartigeBankwerte;

    //Instanziierung bei erstmaligem Aufruf (nicht threadsafe).
    public static BankWerte getInstance() {
        if (einzigartigeBankwerte == null) {
            einzigartigeBankwerte = new BankWerte();
        }
        return einzigartigeBankwerte;
    }

    //restlicher code
}			

Bilanzen werden fortan nur bei initialer Benutzung der BankWerte berechnet.

Die angetragenen Probleme im Verwaltungsprogramm wurden dank Zugriffskontrolle und verzögertem Laden behoben.

Allerdings erfolgte keine Berücksichtigung von Multithreadingproblematiken, siehe Variationen.

Weiterhin sei hier auf die Nachteile des Singletons Patterns hingewiesen. In unserem Fall wäre eine komplette Neumodellierung der Banksoftware mit sauberer Trennung von Schichten und Verantwortlichkeiten sinnvoll.

Nach dieser Einführung wird im folgenden Abschnitt das Singleton Design Pattern formalisiert, näher analysiert und diskutiert.

Das Bankbeispiel mit Singleton Pattern Termini

Klasse Singleton Teilnehmer
BankWerte Singleton

Analyse und Diskussion

Gang Of Four-Definition

Singleton:
"Sichere ab, dass eine Klasse genau ein Exemplar besitzt, und stelle einen globalen Zugriffspunkt darauf bereit."
([GoF], Seite 157)

Beschreibung

Singleton

Das Singleton Entwurfsmuster sorgt dafür, dass es von einer Klasse nur eine einzige Instanz gibt und diese global zugänglich ist.

Damit es nur eine einzigartige Instanz gibt, muss eine Instanziierung durch den Client verhindert werden. Dafür wird der Konstruktur privat deklariert. Nun kann einzig der Singletoncode selbst das Singleton instanziieren.

Weiterhin definiert die Singletonklasse eine global verfügbare Methode, in der diese einzigartige Singletoninstanz zurückgegeben wird. In Java wird dies mit den Modifiern public und static erreicht. Der Singletoncode muss (in der Methode) sicherstellen, dass immer nur ein und dasselbe Objekte an den Client gelangt. Die verschiedenen Varianten, dies zu realisieren, werden im Kapitel Variationen diskutiert.

Singleton Klassendiagramm

Beispielimplementierung eines Singleton:

public class Singleton {
    
    //Field hält Referenz auf einzigartige Instanz
    private static Singleton instance;
    
    // Privater Konstruktur verhindert Instanziierung durch Client
    private Singleton(){
    }
    
    //Stellt Einzigartigkeit sicher. Liefert Exemplar an Client.
    //Hier: Unsynchronisierte Lazy-Loading-Variante
    public static Singleton getInstance(){
        if (instance == null){
            instance = new Singleton();
        }
        return instance;
    }

    //logic code
}			

Variationen

Eager vs. Lazy Loading

Besonders einfach zu implementieren, ist das Eager Loading, das vorgezogene Instanziieren des Singletons. Dabei findet die Objekterstellung beim Laden der Klasse statt.

Eager Loading: Instanziierung während die Klasse geladen wird:

public class Singleton {

    private static final Singleton instance = new Singleton();

    private Singleton() {
    }
    
    public static Singleton getInstance(){
        return instance;
    }

}			

Ein großer Vorteil dieser Variante ist, neben der Einfachheit, die Threadsicherheit und Performance. Bevor die Applikation überhaupt startet und Threads parallel auf das Singleton zugreifen können, wird das Objekt erstellt. Eine teure Synchronisierung von getInstance() ist nicht nötig.

Der entscheidende Nachteil beim vorgezogenem Laden (Eager Loading) ist die Gefahr von verfrühter oder gar unnötiger Instanziierung. Diese Problematik ist besonders bei Singletons, deren Erstellung mit einem umfangreichen und ressourcenintensiven Vorgang einhergehen, relevant. Ebenfalls spricht gegen das vorzeitige Laden, dass zur statischen Initialisierungszeit noch nicht alle nötigen Informationen zur Initialisierung des Singletons bereitstehen können. Das Singleton kann Werte benötigen, die erst im Zuge des Programmablaufs verfügbar sind.

Sinnvoll ist das Eager Loading, wenn man relativ kleine Singletons mit einfachem Erstellungsprozess hat, die mehrfach gebraucht werden.

Das Lazy Loading löst das Problem der pauschalen Erstellung durch verzögerter Instanziierung. Das Singleton wird erst erstellt, wenn es das erste Mal gebraucht wird, also beim ersten Aufruf von getInstance().

Lazy Loading: Instanziierung beim ersten Bedarf:

public class Singleton {
    
    private static Singleton instance;
    
    private Singleton(){   
    }
    
    public static Singleton getInstance(){
        if (instance == null){
            instance = new Singleton();
        }
        return instance;
    }
}			

Synchronisierung

Das Lazy Loading schafft allerdings ein neues Problem im Bereich des Multithreadings. So kann es zur Erstellung zweier Singletons kommen, wenn ein Thread nach der null-Prüfung - direkt vor der Instanziierung - den Fokus abgibt und ein anderer Thread getInstance() durchläuft. Dieser andere Thread erstellt dann ein Singleton. Der erste Thread erhält nun den Fokus wieder und weiß nicht, dass das Singleton bereits erzeugt wurde, und erstellt es noch einmal. Die Einmaligkeit des Singletons ist verletzt.

Daher bedarf es einer Synchronisation. Synchronisationen sind allerdings teuer und erzeugen einen Overhead bei jedem Aufruf von getInstance(). Bei einer performancekritischen Applikation mit zahlreichen Aufrufen von getInstance() sollte von dieser Variante Abstand genommen werden.

Lazy Loading mit einfach synchronisierter getInstance()-Methode:

public synchronized static Singleton getInstance(){
    if (instance == null){
        instance = new Singleton();
    }
    return instance;
}			

Allerdings lässt sich auch dies noch weiter optimieren, sodass der normale getInstance()-Aufruf nicht mehr synchronisiert werden muss. Stattdessen wird nur die Erstellung synchronisiert und ein doppelter Null-Check verwendet. Dadurch wird (nach der einmaligen Erstellung) keine Performance im Normalbetrieb (getInstance()-Aufruf) verloren und trotzdem die Einmaligkeit der Singleton-Instanz gewährleistet.

Lazy Loading mit unsynchronisierter getInstance()-Methode, aber mit synchronizierter Instanziierung und doppelten Null-Check:

public static Singleton getInstance() {
    if (instance == null) {
        synchronized (instance) {
            if (instance == null) {
                instance = new Singleton();
            }
        }
    }
    return instance;
}			

Initialize()-Methode

Ist die Initialisierung des Singletons von seiner ersten Verwendung explizit trennbar, so kann die Instanziierung mit Hilfe einer initialize()-Methode separat durchgeführt werden. Diese Lösung ist besonders robust, da Exceptions geworfen werden können, wenn initialize() mehrfach oder getInstance() vor initialize() aufgerufen wird. ([PK], Seite 33)

Anwendungsfälle

  • Einmalige und zustandslose Strukturen, wie Factorys oder Utilityklassen. Bei Factorys sollte die Instanziierung verzögert erfolgen, da sie unter Umständen teuer sein kann. Beispielsweise durch das dynamische Suchen nach passenden und verfügbaren Factoryimplementierungen.

Vorteile

  • Einfache Anwendung. Eine Singletonklasse ist schnell und unkompliziert geschrieben.

Gegenüber globalen Variablen ergeben sich eine Reihe von Vorteilen:

  • Zugriffskontrolle. Das Singleton kapselt seine eigenen Erstellung und kann damit genau kontrollieren, wann und wie Zugriff auf das Singleton erlaubt wird. Setter und Getter können Plausibilitätsprüfungen beinhalten.
  • Sauberer Namensraum. Der Namensraum wird nicht mit unzähligen globalen Variablen überfrachtet, sondern gekapselt in einem Singleton bereitgestellt.
  • Spezialisierung. Ein Singleton kann abgeleitet werden, um ihm neue Funktionalität zuweisen zu können. Die Integration in bestehenden Code gestaltet sich einfach. Welche Unterklasse genutzt werden soll, kann dynamisch zur Laufzeit entschieden werden.
  • Lazy-Loading. Singletons können erst erzeugt werden, wenn sie auch wirklich gebraucht werden.

Nachteile

  • Prozedurales Programmieren. Die ausgiebige Verwendung von Singletons führt zu einem ähnlich ungünstigen Zustand wie bei globalen Variablen. Dies entspricht der prozeduralen Programmierung und hat nichts mit Objektorientierung und Kapselung zu tun.
  • Globale Verfügbarkeit. Durch die globale Verfügbarkeit wird das Singleton überall in der Applikation verfügbar. Enthält das Singleton Daten, so ist dies ein sehr fragwürdiges Design. Welche Daten können es sein, die in allen Schichten (wie View, Controller, Remote, Businesslogik, Persistenz oder gar in Beans) verfügbar sein sollen? Kann es nicht sogar gefährlich sein, bestimmte Daten oder Funktionalitäten überall frei verfügbar zu machen? Kann man diese nicht doch einer Schicht sauber zu ordnen und jede Schicht hinter wohldefinierten Schnittstellen und Datenaustausch kapseln? Singletons verleiten zu unsauberen und intransparenten Programmieren. Die Notwendigkeit von Singletons sollte stets hinterfragt werden.
  • Intransparenz. Ob eine Klasse ein Singleton verwendet, wird nicht aus deren Schnittstelle klar, sondern aus deren Implementierung. Diese wird hart ans Singleton gekoppelt. Auf die Definition der Schnittstelle allein kann sich nicht mehr verlassen werden, da die Implementierung unspezifizierte Abhängigkeiten besitzt. Übersichtlichkeit, Wartbarkeit und Wiederverwendbarkeit leiden ernorm. Bei Änderungen am Singleton wird nicht klar, welche Programmteile betroffen sind. Fehlfunktionen können schwer zurückverfolgt werden.
  • Problematisches Zerstören. Um in Sprachen mit Garbage Collection Objekte zu zerstören, darf ein Objekt nicht mehr referenziert werden. Dies ist bei Singletons schwierig sicherzustellen. Durch die globale Verfügbarkeit, passiert es sehr schnell, dass Codeteile noch eine Referenz auf das Singleton halten.
  • Besonders bei Mehrbenutzeranwendungen kann ein Singleton die Performance senken, da er - besonders in der synchronisierten Form - ein Flaschenhals darstellt.
  • Einmaligkeit über physikalische Grenzen. Die Einzigartigkeit eines Singletons über physikalische Grenzen hinweg (JVM) zu gewährleisten, ist schwierig.
  • Konfigurierbarkeit. Oft soll das Singleton mit bestimmten Daten erstellt werden. Die Parametrisierung der getInstance()-Methode ist jedoch keine Lösung, weil ein Aufrufer mit anderen Parametern ein "falsches" Singleton (nämlich das des ersten Aufrufers) erhält. Somit muss auf Registry oder Konfigurationsdateien zurückgegriffen werden, um das Singleton mit Informationen zu versorgen.

Singleton ist ein prozedurales Relikt im vermeidlich glänzendem OO-Gewand. Die oft bedingungslose und globale Verfügbarkeit widerspricht jedoch vielem, was Objektorientierte Programmierung ausmacht (Kapselung, Schnittstellen, Schichten, Wiederverwendbarkeit etc.). Seine Verwendung sollte wohlüberlegt sein und sich auf Fälle mit einmaligen Strukturen (wie Factorys), die keinen Zustand besitzen, beschränken.

Anwendung in der Java Standardbibliothek

Runtime

java.lang.Runtime ermöglicht einer Applikation mit ihrer JVM, in der sie läuft, zu kommunizieren. Es erlaubt unter anderem das Absetzen von Kommandozeilenbefehlen (exec()), das Erfassen des verfügbaren und verbrauchten Speichers, der Prozessoranzahl oder das dynamische Laden von Bibliotheken.

Dabei ist die Runtime-Klasse ein klassischer Vertreter des Singleton Design Entwurfsmusters und zwar in der unsynchronisierten Version mit vorgezogenem Laden.

Auszug aus der Klasse Runtime mit originalen JavaDoc-Kommentaren:

public class Runtime {
    private static Runtime currentRuntime = new Runtime();

    /**
     * Returns the runtime object associated with the current Java application.
     * Most of the methods of class Runtime are instance 
     * methods and must be invoked with respect to the current runtime object. 
     * 
     * @return  the Runtime object associated with the current
     *          Java application.
     */
    public static Runtime getRuntime() { 
     return currentRuntime;
    }

    /** Don't let anyone else instantiate this class */
    private Runtime() {}

    //weiterer Runtimecode (exec(), freeMemory(), availableProcessors(), totalMemory() etc. )...
}