Singleton Pattern

Warum viele Entwickler das beliebte Pattern hassen und es trotzdem unverzichtbar ist.
Veröffentlicht: vor 16 tagen · Lesedauer ca. 21 Minuten

Das Singleton Pattern ist ein Erzeugungsmuster, nach Definition der „Gang of four“ und ist dafür gedacht, dass ein Objekt maximal einmal existiert.

Die „Gang of Four“ schreibt in dem Buch „Design Patterns: Elements of Reusable Object-Oriented Software“ folgendes darüber:

Ensure a class only has one instance, and provide a global point of access to it.

Übersetzt heißt es soviel wie:

Stellt sicher, dass nur eine Instanz einer Klasse vorhanden ist und stellt einen globalen Zugriff bereit.

Der Aufbau eines Singletons

Singleton pattern UML Diagram

Im Grunde genommen ist ein Singleton eine Klasse, die zwei Kriterien erfüllen muss:

  • Es muss ein privater Konstruktor definiert sein (um eine Instanzbildung von außerhalb zu verbieten)
  • Es muss eine Methode (typischerweise GetInstance()) definiert werden, um den Zugriff auf die Instanz zu ermöglichen.

Dies erreicht man wie folgt:

public class Singleton
{
	private static Singleton _instance;
	
	private Singleton() { }
	
	public static Singleton GetInstance()
	{
		if (_instance == null)
		{
			_instance = new Singleton();
		}
		return _instance;
	}
}

Der Zugriff kann auf zwei Arten erfolgen.

// Objekt Orientiert
var singleton = Singleton.GetInstance();
singleton.DoSomething();

// Prozedural
Singleton.GetInstance().DoSomething();

Abhängig von den Vorlieben und dem aktuellen Anwendungsfall kann entweder das eine oder das andere Sinn ergeben.

Singletons im wahren Leben

Ein klassisches Singleton wird bei Bedarf erzeugt und darf den Zustand nie ändern. Es muss sichergestellt werden, dass die Ergebnisse an jeder Stelle und zur jeder Zeit identisch (oder zumindest vorhersehbar) sind.

Ich musste echt eine lange Zeit nachdenken, um ein geeignetes Beispiel dafür zu finden. Jedoch ist das nicht ganz leicht. Das Leben ist im ständigen Wandel. Alles um uns herum verändert sich.

Ein Beispiel könnte die Regierung sein. Schließlich kann es immer nur eine Regierung gleichzeitig geben. Allerdings darf man die Neuwahlen nicht vergessen. Spätestens dann, ändert sich die Regierung. Es werden Minister entlassen, Abgeordnete kommen und gehen. Es ändern sich einfach zu viele Parameter.

Selbst unsere Gesetze können sich ändern. Die Gesetze werden entweder gelockert oder auch verschärft. Je nach Bedarf.

Was wirklich Bestand hat, sind wahrscheinlich nur physikalische, mathematische und chemische Gesetze. Diese haben sich jedenfalls seit Jahrtausenden nicht mehr geändert.

Newton würde selbst heute noch ein Apfel auf den Kopf fallen 🙂

Die Schwachstellen eines Singletons

Früher oder später kommt man bei der Verwendung an den Punkt, da möchte man einfach noch etwas mehr aus der aktuellen Klasse rausholen. Eigentlich sind es keine großen Wünsche. Mit einem Singleton kann man diese jedoch nicht abbilden.

Konstruktor Parameter

In gewissen Situationen muss man ein Singleton mit diversen Parametern erzeugen. Beispielsweise ein Logger um den Datei-Pfad dynamisch zu halten.

Das Pattern sieht jedoch keinen Einsatz eines parametrisierten Aufrufs der GetInstance() Methode vor. Dafür muss der Anwender bei jedem Aufruf den die genauen Parameter kennen.

Um dies trotzdem zu ermöglichen, kann man eine Init() oder auch Create() Methode implementieren.

public class Logger
{
	private static Logger _instance;
	private string _pathToLogfile;
	
	private Logger(string pathToLogfile)
	{
		_pathToLogfile = pathToLogfile;
	}
	
	public static Logger GetInstance()
	{
		if (_instance == null)
		{
			throw new ArgumentNullException("Bitte Logger erst mit 
				Create() initialisieren.");
		}
		return _instance;
	}
	
	public static void Create(string pathToLogfile)
	{
		if (_instance == null)
		{
			_instance = new Logger(pathToLogfile);
		}
	}
	
	public void Log(string message) 
	{
		File.Append(_pathToLogfile, message+"\r\n");
	}
}

// Verwendung
Logger.Create(@"C:\temp\log.txt");
var logger = Logger.GetInstance();
logger.Log("Es ist etwas passiert...");

Eine andere Möglichkeit, die aufgerufenen Methode zu parametrisieren sehe in etwa so aus:

var session = Session.GetInstance();
session.Save("global", "variable", "value");

Objekt zurücksetzen

Möchte man die Instanz zurücksetzen, liegt die Versuchung doch sehr nah solch eine Methode zu implementieren:

public static void ResetInstance()
{
	_instance = null;
}

Das ist jedoch eine ziemlich schlechte Idee.

Bei einem erneuten Zugriff mittels GetInstance() wird eine zweite Instanz des Objekts erzeugt, die alte ist dann aber null. Theoretisch existieren also zwei Objekte und einer davon ist sogar „instabil“.

Speichert man sich die Instanz irgendwo zwischen und verwendet es auf dem objektorientierten Weg, so ist das Objekt hinterher null. Selbst ein erneuter Zugriff auf die GetInstance() Methode behebt das Fehlverhalten nicht, da das Objekt immer noch auf die alte Instanz verweist.

Destruktor

Im Grunde genommen ist der Singleton eine ganz normale Klasse und kann de facto auch ein Destruktor haben.

Zu beachten ist jedoch, dass der Destruktor erst am Ende der Lebenszyklus der Applikation (etwa dann, wenn die Applikation beendet wird) aufgerufen werden soll.

Singleton und statische Klassen

Ganz kurz möchte ich noch das Thema static behandeln.

Technisch gesehen macht es kaum ein Unterschied, ob man ein Singleton oder eine statische Klasse verwendet.

In beiden Fällen hat man jeweils nur ein Objekt zur Verfügung.

Singleton hat jedoch (meiner Meinung nach) im Rennen die Nase etwas weiter vorne, da man auf das Objekt direkt zugreifen kann und auch die Möglichkeit besitzt eine Instanz einer Singleton Klasse an eine andere Klasse zu übergeben (auch wenn es die wenigsten machen).

Des weiteren ist bei der Verwendung von statischen Klassen der prozedurale Programmierstil schon in Stein gemeißelt.

Anwendungsbeispiele für Singletons

In einigen Fällen, könnte man überlegen ein Singleton zu verwenden. Die Betonung liegt hier ganz klar auf „könnte“ 🙂

Logger

Das ist das erste, was bei der Verwendung eines Singletons in den Kopf kommt.

Hat man bereits viel Business Logik implementiert und es kommt die Anforderung (oder Wunsch), ein Logging nachzuziehen, muss man den Logger überall „mitschleifen“.

Einfacher ist hierbei ein SimgletonLogger, der bei Bedarf aufgerufen wird.

Jedoch sollte man hier Vorsicht walten lassen. Ein permanent geöffneter FileStream (häufigste Art zu loggen) sperrt die Datei für die Dauer der Anwendung. Mit anderen Worten: nicht nur der Logger, sondern auch die komplette Applikation wird zum Singleton.

Session

Sessions beinhalten Einstellungen des aktuellen Benutzers. Diese möchte man ggf. auch überall parat haben.

SingletonSessions sind teilweise die einzige Möglichkeit um andere Singletons zu initialisieren (z.B. Datenverbindungen).

Cache

Schwergewichtige Objekte zu laden, beansprucht sehr viel Zeit. Benötigt man diese an mehreren Stellen, kann man darüber nachdenken die Objekte zwischenzuspeichern.

Man sollte jedoch den Speicher nicht vernachlässigen und dafür sorgen, dass nicht verwendete Objekte wieder aufgeräumt werden.

Eine Applikation, die über einen sehr langen Zeitraum ausgeführt wird, kann auf diese Weise den Speicher mit Daten zumüllen. Es kommt zu einem „momery leak“.

Verbindung

Hierzu zählen auch Datenbankverbindungen, Verbindung zum Drucker oder auch eine Verbindung zum Server.

Sollte eine Verbindung permanent bestehen bleiben, kann diese durchaus als Singleton implementiert werden. Allerdings müssen dann auch die Methoden Open() und Close() implementieren werden.

Erzeugung von Objekten

Klingt zwar ein wenig paradox, aber man kann das Erzeugen von Objekten in ein Singleton auslagern. Die Methoden innerhalb der Singleton Klasse haben dann keinerlei Logik und geben nur ein fertiges Objekt zurück.

Dadurch kann man sich etwas Schreibaufwand beim instanziieren von Objekten sparen und geht auch kein Risiko ein, etwas vergessen zu haben.

Gleichzeitig läuft man aber auch in die Gefahr, das man die Implementierung (bzw. die Erzeugung) nicht mehr zur Laufzeit austauschen kann.

Die Vor- und Nachteile eines Singletons

Bereits an dieser Stelle, kann man sich darüber streiten, ob es sinnig ist dieses Pattern zu verwenden.

Weitere Nachteile sind

  • Übertreibt man mit der Verwendung von Singletons, so besteht eine große Gefahr, dass man prozedural programmiert und damit alle Vorteile der objektorientierten Programmierung aushebelt.
  • Wird ein Singleton verwendet, so kann man dies sehr häufig aus der Verwendung der Bibliothek nicht eindeutig erkennen. Die Kopplung zwischen zwei (oder mehreren) Objekten wird erhöht und die Wiederverwendbarkeit reduziert.
  • Das Testen mit einem UnitTest ist kompliziert bis unmöglich. Das Mocken ist sehr zeitaufwändig und teilweise sogar unmöglich (dazu später mehr). Viele Entwickler neigen dann dazu auf diese Tests zu verzichten und laufen Gefahr instabilen Code zu schreiben.
  • Die korrekte Verwendung von Dependency Injection ist mit einem korrekten Singleton nicht möglich. Möchte man dies trotzdem machen, muss man eins von beiden lockern (dazu später mehr).
  • In Systemen mit mehreren Threads, muss sichergestellt werden, dass nicht mehr als eine Instanz vorhanden ist und dass sich die Threads nicht in die Quere kommen (dazu später mehr).
  • Eine Konfiguration ist schwierig. Einzige Möglichkeit sind Konfigurationsdateien, die wiederum mit einem anderen Singleton ausgelesen werden müssen.
  • Bereits durch die Verwendung eines Singletons holt man sich mehr Komplexität „ins Boot“ als in einem Design ohne Singletons (nee, ohne Witz. Alles ist einfacher ohne Singletons).
  • Der Singleton besitzt per Definition einen privaten Konstruktor. Eine statische Methode kann auch nicht in einem Interface definiert werden. Beide Tatsachen haben zur Folge, dass man keine Vererbung verwenden kann. Es gibt zwar Mittel und Wege, die sind aber geschummelt. Außerdem hat man dann kein „sauberes“ Singleton mehr.
  • Ein Singleton erlaubt per Definition keine Kopie/Clone. Somit kann man den Singleton auch nicht beispielsweise serialisieren und in eine Datei speichern oder versenden.
  • Durch eine schlampige Programmierung können memory leaks entstehen. Dies erfolgt jedes Mal, wenn gespeicherte Objekte nicht selber aufgeräumt werden.
  • Es verstößt gegen vier von fünf SOLID Prinzipien (Beitrag von Alain Schlesser)

Allein schon durch diese Tatsachen gilt das Singleton Pattern als Anti-Pattern bzw. als gefährlich und wird von vielen Entwicklern abgeraten.

Denen gegenüber stehen jedoch ein paar Vorteile

  • Man spart sich eine aufwendige Initialisierung und das „durchreichen“ der Instanzen. Auf ein Singleton kann man quasi von überall zugreifen.
  • Die Instanz kann zur Laufzeit bei Bedarf erzeugt werden.
  • Der „De-Implementierung“ (Verwendung mehrerer Instanzen) ist im späteren Einsatz einfacher als der Umstieg auf Singleton.

Singleton und UnitTests

Stellen wir uns mal vor, wir haben eine Datenbankverbindung als Singleton implementiert.

Wir möchten überprüfen ob der User eine bereits gebuchte Rechnung löschen kann. Hierzu soll ein Test geschrieben werden.

Der Test könnte in etwa so aussehen:

Assert.DoesNotThrow(() =>
	var service = new BillingService();
	service->DeleteBillById(42);
});

Der BillingService enthält eine aktive Datenbankverbindung, die bereits auf die produktive Datenbank vorkonfiguriert ist

Eine Rechnung (oder Datensatz) aus einer produktiven Datenbank nur zu Testzwecken zu löschen oder abzuändern ist jedoch mehr als ungünstig.

Und das Problem zu lösen, kann man sich für ein Mocking Framework entscheiden.

Ich bin jedoch kein wirklicher Freund solcher Frameworks. Man vergleicht Äpfel mit Birnen.

Der Aufwand für ein einfachen Test steigt überproportional an.

Hätte der BillingServer ein Parameter für die Datenbankverbindung, hätte man noch den Hauch einer Chance das Ding zu testen.

Aber so, nope.

Singleton und Dependency Injection

Mein liebstes DI Framework ist ja bekanntlich ninject.

Singleton Scope

Ninject stellt beim initialisieren vom Kernel (Container) die Methode InSingletonScope() bereit.

Und tatsächlich verwende ich diese Methode sehr gerne. Die Methode hat zwar etwas mit dem Singleton zu tun, ist jedoch anders zu verstehen.

Ninject löst das wie folgt auf:

// Mit SingletonScope
var logger = new Logger();
var userService = new UserService(logger);
var billingService = new BillingService(logger);

// Ohne SingletonScope
var userService = new UserService(new Logger());
var billingService = new BillingService(new Logger());

Verwendet man InSingletonScope nicht, erzeugt ninject immer neue Instanzen. Es gibt Situationen, in denen das es:

  • Keinen Unterschied macht
  • Einen Sinn ergibt
  • Kontraproduktiv ist (z.B. Konfiguration)

Ich habe ja bereits erwähnt, dass ein Singleton und Dependency Injection wie zwei unterschiedlichen Pole eines Magnetes und ein krasser Kontrast sind.

Möchte man in einem System beides verwenden, muss man Abstriche machen. Entweder es wird das Singleton Pattern verletzt oder das Dependency Injection Pattern.

Dependency Injection verletzten

Ninject stellt die Methode ToConstant() bereit. Diese kann verwendet werden, um einem bestimmten Typen eine eindeutige Instanz zuzuweisen.

Im späteren Verlauf arbeitet man mit der Klasse so, als ob nichts wäre.

Singleton verletzten

Möchte man es etwas sauberer lösen, kann man auch ein SingletonAdapter erstellen.

Soll heißen, es gibt eine Klasse, die für den Zugriff auf das Singleton-Objekt zuständig ist. Dabei spielt es keine Rolle wie oft man es neu instanziiert.

public class SingletonAdapter : ISingletonAdapter
{
	public void DoSomething() 
	{
		Singleton.GetInstance().DoSomething();
	}
}

Dieser Singleton ist eigentlich kein Singleton mehr. Auch wenn man nur eine Instanz davon besitzt.

Leider hat man in modernen Systemen keine andere Wahl mehr, wenn man sowohl DI als auch UnitTests haben möchte.

Singleton in Multi-threading Systemen

Bevor wir in dieser Sektion starten, möchte ich vorab ein paar Begrifflichkeiten klären (nur für den Fall, dass 🙂 ).

thread-safe: Gleiche Instanz in allen Threads, ohne sich zu behindern

Lazy (loading): Wird beim ersten verwenden geladen.

Eager (loading): Wird beim Programmstart geladen.

Dann legen wir mal los 🙂

1. Lazy und nicht thread-safe

Die eigentliche Implementierung, die durch die gang of four empfohlen wird, sieht wie folgt aus:

public class Singleton
{
	private static Singleton _instance;
	
	private Singleton() { }
	
	public static Singleton GetInstance()
	{
		if (_instance == null)
		{
			_instance = new Singleton();
		}
		return _instance;
	}
}

Diese Implementierung sollte gemieden werden!

Greifen zwei Threads wirklich gleichzeitig auf die Instanz zu, so überprüfen beide die Bedingung. Noch bevor der erste Thread eine Instanz erzeugen kann, wertet der zweite Thread die Bedingung mit true aus und erstellt ebenfalls eine Instanz. Hurra! Wir haben ein Multiton.

2. Lazy thread-safe mit lock

public class Singleton
{
	private static Singleton _instance;
	private static readonly object _monitor;
	
	private Singleton() { }
	
	public static Singleton GetInstance()
	{
		lock(_monitor)
		{
			if (_instance == null)
			{
				_instance = new Singleton();
			}
			return _instance;
		}
	}
}

Diese einfache Erweiterung macht den Singleton bereits Thread Safe. Wenn zwei (oder mehrere) Threads auf die Instanz zugreifen, beansprucht die erste Instanz ein lock. Alle anderen Instanzen warten jetzt solange, bis der erste damit fertig ist.

Man kann das lock als eine Art Warteschlange ansehen.

3. Lazy thread-safe mit dem „double check“ Verfahren

public class Singleton
{
	private static Singleton _instance;
	private static readonly object _monitor;
	
	private Singleton() { }
	
	public static Singleton GetInstance()
	{
		if (_instance == null)
		{
			lock(_monitor)
			{
				if (_instance == null)
				{
					_instance = new Singleton();
				}
			}
		}
		return _instance;
	}
}

Die Applikation blockiert bei dieser Art von Implementierung keine Threads mehr. Voraussetzung dafür ist jedoch, dass jedem Thread bereits die Instanz bekannt ist. Im schlimmsten Fall blockiert die Anwendung maximal für die Anzahl der Prozesse.

4. Eager thread-safe ohne lock

public class Singleton
{
	private static readonly Singleton _instance = new Singleton();
		
	static Singleton() { }
	private Singleton() { }
	
	public static Singleton GetInstance()
	{
		return _instance;
	}
}

Dieses Beispiel ist extrem simpel, meinst du nicht auch?

Aber wie Thread-Safe ist es wirklich? Statische Konstruktoren werden ausgeführt, sobald eine Instanz erstellt wird oder eine statische Methode aufgerufen wird. Des Weiteren kümmert sich der Compiler darum, dass der Konstruktor nur einmal aufgerufen wird.

Diese Tatsachen, machen die ganzen Überprüfungen und Blockaden unnötig. Man kann einfach davon ausgehen, dass es da ist.

5. Lazy thread-safe mit verschachtelten Klassen

public sealed class Singleton
{
	private Singleton() { }
	
	public static Singleton GetInstance()
	{
		return SingletonCreator.Instance;
	}
	
	private class Lazy
	{
		static Lazy() { }

		internal static readonly Singleton Instance = 
			new Singleton();
	}
}

In diesem Beispiel wird die Instantiierung beim ersten Aufruf der statischen Variable (bzw. Eigenschaft/Property) aus der Unterklasse „Lazy“ durchgeführt.

Die Implementierung ist komplett lazy und stellt auch die Performance Vorteile der vorherigen Methode bereit.

Diese Variante ist die beste (und auch leider die aufwendigste) Implementierung für ein Singleton.

6. Lazy und thread-safe (ab .NET 4.0)

Microsoft hat in der Version 4.0 vom .NET Framework bereits sowas wie die Lazy Klasse aus dem oberen Beispiel implementiert. Die Rede ist natürlich von der System.Lazy<T> Klasse, die solche Implementierungen auf ein Minimum reduziert.

public sealed class Singleton
{
	private static readonly Lazy<Singleton> _lazy = 
		new Lazy<Singleton>(() => new Singleton());
		
	private Singleton() { }
	
	public static Singleton GetInstance()
	{
		return _lazy.Value;
	}
}

Fazit zum Singleton pattern

Ich hoffe, ich konnte ein wenig Licht ins Dunkle bringen und aufzeigen, warum der gute alte Singleton unter den Programmierern nicht sonderlich beliebt ist.

An und für sich steckt ein interessanter Gedanke hinter diesem Entwurfsmuster. Es ist nicht per se schlecht. Es ist einfach nur veraltet. Man muss auch bedenken, dass die Erstausgabe des Buches im Jahre 1994 erschienen ist. Zu dieser Zeit gab es noch keine UnitTests und auch kein dependency injection. Es gab jedoch viele globale Variablen. Der hat diese Variablen gekapselt.

Ich persönlich meide es heutzutage gänzlich. Jedenfalls im klassischen Sinne.

Unverzichtbar wird es aufgrund der Tatsache, dass Objekte persistiert (langlebig) werden müssen.

Jedoch mache ich es teilweise etwas umständlicher und übergebe die nötigen Instanzen im Konstruktor. Mit dieser Vorgehensweise kann ich meinen Code einfacher Testen und kann bei Bedarf auch ein Dependency Injection Framework hinzufügen.

... und was meinst du dazu?
Deine E-Mail-Adresse wird nicht veröffentlicht.