Was ist ein Singleton – Und was kann man damit in der Praxis genau anstellen?

Veröffentlicht: vor 3 monaten · Lesedauer ca. 24 Minuten

In einigen Fällen ist es erforderlich genau eine Instanz einer Klasse zu haben.

Stell dir mal so ein Betriebssystem vor. (Fast) jedes Betriebssystem arbeitet in irgend einer Art und Weise mit Fenstern. Und obwohl viele Fenster gleichzeitig geöffnet werden können, gibt es genau ein Window Manager.

Solche einzigartige Instanzen nennt man „Einzelstück„, besser bekannt als Singleton.

Singleton Pattern: Definition

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

Gang of four (Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides)

Frei nach Connor MacLeod „Es kann nur einen geben“ kann eine Instanz, die als Singleton definiert wurde, nur einmal in der kompletten Applikation existieren.

Dabei kümmert sich diese „Highlander“-Klasse sowohl um die Erzeugung, die Bereitstellung als auch um die individuelle Geschäftslogik, die ein jeder Singleton mit sich bringt.

Singleton Pattern: Real-World

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 🙂

Singleton Pattern: Struktur

singleton pattern UML diagram

Rollenverteilung

Singleton: Beinhaltet und erzeugt die Instanz der einzigartigen Klasse. Diese Klasse ist sowohl für die Erzeugung als auch für die weitere Logik zuständig.
Je nach Logik ist es aber ratsam, weitere Pattern hinzuzuziehen um unnötig viel Quellcode zu vermeiden.

Client: Repräsentiert die Geschäftslogik und ruft den Singleton (bzw. teile davon) auf.

Singleton Pattern: Pseudocode

class Singleton
	// eine Singleton-Klasse beinhaltet immer eine Klassenvariable,
	// die eine Instanz vom eigenen Typ hält.
	field instance : Singleton
	
	// damit die Klasse nicht mit dem Schlüsselwort 'new'  neu instantiiert
	// werden kann, muss der Konstruktor privat sein.
	// somit kann eine neue Instanz nur aus der eigenen Klasse erzeugt
	// werden
	private constructur()
	
	// ein Singleton muss immer eine statische Methode beinhalten, die die
	// Instanz bereitstellt
	static method GetInstance() : Singleton
		// beim ersten Aufruf wird eine neue Instanz der Singleton-Klasse
		// erzeugt und anschließend immer wiederverwertet
		if this.instance is null
			this.instance = new Singleton()
		return this.instance;
	
	// ein Singleton sollte auch Geschäftslogik beinhalten, die ab dieser
	// Stelle definiert werden kann
	method Operation(text) : any

program Client
	// an dieser Stelle wird eine neue Instanz erzeugt
	Singleton foo = Singleton.GetInstance()
	foo.Operation("Hallo")
	// ...
	Singleton bar = Singleton.GetInstance()
	foo.Operation("Welt")
	// die Variable 'bar' beinhaltet dasselbe Objekt, wie auch die Variable 'foo'

Singleton Pattern: Implementation

  1. Erstelle eine statische Instanzvariable um das Singleton-Objekt zwischenzuspeichern.
  2. Definiere eine statische Zugriffsmethode (meist GetInstance) für die Instanzvariable.
  3. Gib den Wert der Instanzvariable zurück. Wenn diese noch nicht instanziiert wurde, dann erstelle eine Instanz davon.
  4. Definiere den Konstruktor „private“ um sicherzustellen, dass keine weitere Instanz gebildet werden kann.
  5. Optional: Definiere die Klasse als final bzw. sealed, damit man davon nicht ableiten kann.
  6. Ersetze im Client Code jeden Aufruf durch die Methode, die du in Schritt 2 definiert hast.

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:

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

Singleton Pattern: Use cases

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

Objekterzeugung

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.

Singleton Pattern: Vorteile

Solltest du dich dafür entscheiden, eine einzigartige Klasse frei nach dem Singleton Pattern zu verwenden, bekommst du folgende Vorteile geboten:

Es ist nur eine Klasse

Manchmal benötigt man wirklich genau eine Instanz einer Klasse. Ohne ein Singleton, müsste man diese Instanz durch den kompletten Code „schleifen“ um es global verfügbar zu machen.

Ein Singleton würde diese gezielt an dieser einen Stelle verwenden.

Des Weiteren wird diese Instanz erst später erzeugt und auch wirklich nur dann, wenn nötig.

Es gibt einen globalen Zugriff

Seien wir doch ehrlich. Das Beste am Singleton ist doch, dass man an jeder Stelle im eigenen Quellcode auf die gleiche Art und Weise zugreifen kann.

Alle Parameter sind gleich und die Instanz ist auch immer die gleiche.

Es wird bei Bedarf erzeugt

Ein weiterer Vorteil ist, dass nicht alle Instanzen beim Systemstart erzeugt werden. Wenn die gebraucht werden, sind die da. Wenn nicht, dann eben nicht.

Das ist auch eine Art von Zuverlässigkeit 🙂

Singleton Pattern: Nachteile

Leider ist aber auch nicht alles Gold, was glänzt. Die oberen Vorteile erkaufen wir uns durch einige Nachteile.

Teilweise sind diese so gravierend, dass es einige Entwickler gibt, die ein Singleton als Anti-Pattern betiteln (dazu später mehr).

Es gibt keine Konstruktor Parameter

Es gibt wenige Anwendungsbeispiele, die auch wirklich ohne Parameter auskommen.

Um dies zu lösen, muss man dem Singleton bereits die Konstruktor Parameter im Vorfeld bekannt machen.

Und wie lösen wir das ganze? Richtig! Mit einem weiteren Singleton. Am besten so einen, der eine Konfiguration ausliest und die Parameter an unseren gewünschten Singleton bereitstellt.

Du siehst also, das kann beliebig komplex werden.

Es werden SOLID-Prinzipien verletzt

Schauen wir mal kurz an, welche Kriterien erfüllt werden müssen, damit die SOLID Prinzipien nicht verletzt werden:

Single responsibility: Leider nein. Es hat mehr als eine Aufgabe. Nämlich: Erzeugung, Bereitstellung und Geschäftslogik.

Open-Closed: könnte man denken, aber nein. Man muss die Klasse immer erweitern bei neuen Anforderungen.

Liskov substitution: keine Vererbung – keine Substitution!

Interface segregation: keine Interfaces – keine Trennung!

Dependency inversion: Es kann mit Interfaces umgehen, aber keine sein.

Es ist kaum testbar

Hast du schon einmal versucht ein UnitTest für eine Klasse zu schreiben, die mit Zeitangaben arbeitet?

Dann ist dir bestimmt auch aufgefallen, dass diese Tests irgendwann mal automatisch fehlschlagen, weil nur ein Teil der Logik abgedeckt ist.

Gutes Beispiel hierzu wäre:

var teenies = repository.GetTeenager();
Assert.That(teenies.Count, Is.EqualsTo(4));

Und was geschieht, wenn jetzt einer von den 4 Personen 18 wird? Genau, der ist kein Jugendlicher mehr und die Methode gibt eine 3 zurück.

Folge: Der Test wird „rot“, also fehlerhaft.

Genau sowas kann auch mit einem Singleton passieren.

Das Mocken ist eine Qual sag ich euch 🙂

Du hast es immer an der Backe

Wurde die Instanz einmal erzeugt, so ist diese für den Rest der Ausführung im Speicher.

Egal ob man es braucht oder nicht. Das Ding ist einfach nur da.

Man könnte zwar eine Reset (oder Destroy) Methode implementieren. Doch was hätten wir dann im Anschluss? Richtig! Eine zweite Instanz und somit kein Singleton mehr.

Es hat den Scheuklappen-Blick

In einer modernen multithreading Architektur kann der klassische Singleton nicht eingesetzt werden.

Die Gefahr ist einfach viel zu groß, dass man während der Erzeugung sich aus Versehen gleich zwei Instanzen erzeugt.

Während der Ausführung müssen alle Prozesse auf die aktuelle Ausführung warten. Ein Singleton wird im kompletten System zum Flaschenhals.

Wie du das in den Griff bekommst, liest du weiter unten.

Es verführt zum prozeduralen Programmierstil

Das Wunderbare an der Objektorientierten-Programmierung ist doch die Wiederverwendbarkeit von Objekten und Klassen, oder?

Mit einem Singleton gibt man diesen Vorteil auf und hat anstelle von Methoden (wie die in OOP heißen), nur noch Prozeduren die durchlaufen werden.

Es hat eine starke Kopplung und geringe Kohäsion

Wenn wir schon beim Thema Wiederverwendung sind, ein Singleton ist in den allermeisten Fällen so spezifisch, dass man diesen gar nicht recyceln kann. Noch nicht einmal in der gleichen Applikation. Die Kopplung zu anderen Objekten ist so stark, dass man diese nicht „mal eben“ austauschen kann.

Singleton Pattern: Anti-Pattern?

Die „Erfinder“ der Pattern schrieben 1994 in ihrem Buch „Design Patterns. Elements of Reusable Object-Oriented Software“ folgendes:

A better solution is to make the class itself responsible for keeping track of its sole instance. The class can ensure that no other instance can be created (by intercepting requests to create new objects), and it can provide a way to access the instance. This is the Singleton pattern.

15 Jahre später äußerte sich Erich Gamma dazu wie folgt:

When discussing which patterns to drop, we found that we still love them all. (Not really—I’m in favor of dropping Singleton. Its use is almost always a design smell.)

Clean C++ : Sustainable Software Development Patterns and Best Practices with C++

Damit steht Erich mit seiner Meinung nicht alleine.

Gibt man bei Google den Suchbegriff „Singleton Anti-Pattern“ in die Suchleiste ein, bekommt man 3.86 Millionen Suchergebnisse (Stand: 11. März 2019).

Ich hoffe, ich konnte dich durch die genannten Nachteile auch etwas zum Nachdenken bewegen 🙂

Singleton Pattern 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 das Objekt an eine andere Klasse zu übergeben (auch wenn es die wenigsten machen würden).

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

Ist ja auch klar, es können ja auch keine Objekte gebildet werden und somit hat die „Prozedur“ an jeder Stelle im eigenen Quellcode die identische Funktionalität.

Singleton Pattern und Unit Testing

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 tödlich.

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 Pattern und Dependency Injection

Mein liebstes DI Framework ist ja bekanntlich ninject.

Allein schon wegen der ganzen Ninjas 🙂 Aber auch die Funktionalität, die dahintersteckt ist einfach nur genial. Ich kenne da noch ein paar andere Frameworks, allerdings kommen die an ninject nicht wirklich ran.

Singleton Scope

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

Und tatsächlich verwende ich diese Methode sehr gerne. Und tatsächlich hat es sogar im entferntesten Sinne etwas mit Singleton zu tun.

Aber keine Sorgen, die Klassen müssen nicht alle als Singleton implementiert werden, nur um im Singleton Scope verwendet werden zu können.

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.

Man kann allerdings nicht pauschal sagen, was besser ist. Im Zweifel kann man die Objekte in den SingletonScope packen. Dieses Verhalten kennt man und hat sich bereits daran gewöhnt.

Singleton Pattern und Multithreading

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 (bzw. sofort) geladen.

Dann legen wir mal los 🙂

Lazy, aber 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 Doubleton.

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

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

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

Lazy und thread-safe mit verschachtelten Klassen

public sealed class Singleton
{
	private Singleton() { }
	
	public static Singleton GetInstance()
	{
		return SingletonLazyCreator.Instance;
	}
	
	private class SingletonLazyCreator
	{
		static SingletonLazyCreator() { }
		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.

Lazy und thread-safe (ab .NET 4.0)

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

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.

Singleton Pattern: Code Beispiele

Die Code-Beispiele sind jeweils bei github zu finden.

Singleton Pattern: Verwandte Muster

Viele Pattern, können als Singleton implementiert werden. Darunter fallen unter anderem:

Singleton Pattern: Das Fazit

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 Singleton hat diese Variablen gekapselt.

Ich persönlich meide es heutzutage gänzlich.

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.

Wie stehst du zum Singleton? Verwendest du es gerne oder habe ich dir gerade ein komplett neues Pattern vorgestellt?

Schreib es doch einfach unten in die Kommentare und lass uns miteinander diskutieren 🙂

Kategorien:
Design Pattern
Das könnte dir auch gefallen
Sei der erste und teile deine Meinung mit der Welt!
... und was meinst du dazu?
Deine E-Mail-Adresse wird nicht veröffentlicht.