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.

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 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.

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 pKontenZinsen) {
if (pKontenZinsen > 0 && pKontenZinsen < 4){
kontenZinsen = pKontenZinsen;
}
}
public void setKontenGebuehren(int pKontenGebuehren) {
if (pKontenGebuehren > 0 && pKontenGebuehren < 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 gravierenden 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.
| Klasse | Singleton Teilnehmer |
|---|---|
| BankWerte | Singleton |
Singleton:
"Sichere ab, dass eine Klasse genau ein Exemplar besitzt, und stelle einen globalen Zugriffspunkt darauf bereit."
([GoF], Seite 157)

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.

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
}
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 Singleton2 {
private static Singleton2 instance;
private Singleton2(){
}
public static Singleton2 getInstance(){
if (instance == null){
instance = new Singleton2();
}
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 Singleton2();
}
return instance;
}
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)
Gegenüber globalen Variablen ergeben sich eine Reihe von Vorteilen:
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.
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 <code>Runtime</code> are instance
* methods and must be invoked with respect to the current runtime object.
*
* @return the <code>Runtime</code> 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. )...
}
Kommentare
Seite: 1 -