Generische Singletons mit C#

In der objektorientierten Softwareentwicklung ist es meist verpönt, globale Variablen oder Objekte zu verwenden. Und doch gibt es viele Anwendungen, an denen es einfacher ist ein globales Objekt zu haben, statt Parameterurwälder zu bilden und alles in einzelne Methoden durchzureichen. Ein Anwendungsbeispiel könnte ein globaler Logmechanismus sein.

Eine Lösung bietet das Erzeugungsmuster “Singleton”. Ein Singleton ist ein Entwurfsmuster, welches im Buch Entwurfsmuster . Elemente wiederverwendbarer objektorientierter Software von Gamma, Helm, Johnson und Vlissides (der sogenannten Gang of Four) beschrieben wurde.

Richtig angewendet stellt ein Singleton sicher, dass es nur ein Objekt einer Klasse gibt, und ermöglicht den Zugriff auf dieses Objekt. Folgende Punkte sollen unter anderem gegenüber globalen Objekten verbessert werden:

  • Erzeugung der Instanz wenn es nötig ist (kein statisches globales Objekt)
  • Zugriffskontrollen sollen möglich werden
  • Eine spätere Änderung auf mehrere Objekte ist möglich
  • Unterklassen sind möglich.

Ein Singleton funktioniert grob wie folgt:
Der eigentliche Klassenkonstruktor ist privat, d.h. es kann von außen kein Objekt der Klasse erzeugt werden.

private static Logger instance;

Wenn auf die Klasse zugegriffen wird, so kann dies nur über eine Methode geschehen, die die Instanz der Klasse zurückgibt. Eine Möglichkeit in C# dafür ist die Folgende:

public static Logger Instance
{
	get
	{
		if (instance != null)
			return instance;

		lock (lockObj)
		{
			if (instance == null)				
				instance = new T();
			return instance;
		}
	}
}

Wenn bereits eine Instanz der Klasse erzeugt wurde, so wird diese zurückgegeben. Ist noch keine erzeugt worden, so wird eine neue Instanz erzeugt. Über die Sperre ( lock(lockObj) ) wird sichergestellt, dass zur gleichen Zeit nur ein Thread ein Objekt erzeugen kann. Warten mehrere Threads auf die Erzeugung, so wird nochmals geprüft, ob es eine Instanz bereits gibt (der erste Thread der das Log bekommt erzeugt die Instanz, die weiteren bekommen die bereits erzeugte Instanz).

Jetzt wäre es natürlich möglich, dieses Konstrukt für jede Singleton-Klasse zu nutzen und in jeder Singleton-Klasse den obigen Code zu wiederholen.

Eleganter erscheint es, dies in eine generische Klasse auszulagern.
GenericSingleton

Was macht die generische Klasse? Es wird wie bereits beschrieben ein Singleton-Objekt erzeugt. Statt einem spezifischen Objekt wie im obigen Beispiel wird ein generischer Typ (T) verwendet. Im BEispielcode ist folgendes interessant:

private static readonly object lockObj = new object();

Dieser Abschnitt wirkt zunächst, als gäbe es nur ein globales Lockobjekt für alle Singletons. In der Realität ist das Objekt welches hier als Sperre verwendet wird aber nur statisch pro generischen Instanz. Die Sperre sperrt damit nicht für jede zeitnahe Erzeugung eines beliebigen Singletons, sondern nur für eine spezifische Ausprägung.

Kompletter Code:

using System;

namespace SharedObjects
{
    /// <summary>
    /// Class for creating a singleton for a generic class
    /// </summary>
    /// <typeparam name="T"></typeparam>
	public static class Singleton<T> where T : class, new()
	{
        /// <summary>
        /// The instance
        /// </summary>
		private static T instance;
        /// <summary>
        /// The lock object - INFO this is static only per generic instance 
        /// </summary>
		private static readonly object lockObj = new object();

        /// <summary>
        /// Gets the instance. 
        /// </summary>
        /// <value>
        /// The instance.
        /// </value>
		public static T Instance
		{
			get
			{
				if (instance != null)
					return instance;

				lock (lockObj)
				{
					if (instance == null)
					{
						try
						{
							instance = new T();
						}
						catch (Exception ex) // ex is for debugging only
						{
							instance = null;
						}
					}
					return instance;
				}
			}
		}
	}
}

Wie verwendet man so etwas nun?

Sehr simpel. Nehmen wir an wir haben eine Klasse namens Logger, welche als Singleton verwendet werden soll. Entweder rufen wir die Singleton-Klasse spezifisch bei jedem Aufruf auf:

Singleton<Logger>.Instance.DoSomething()

Oder wir erzeugen eine Variable und verwenden diese

var logger = Singleton<Logger>.Instance;
...
logger.DoSomething();

In beiden Fällen arbeiten wir mit der jeweils einzigen Instanz der Klasse.

Wie bei fast allem ist die Auffassung von Singletons mittlerweile stark gespalten. Eine Meinung ist, dass Singletons schlecht seien, da sie bei exzessiver Verwendung einfach eine andere Art globale Variablen seine. Abhängigkeiten und Kopplungen können erhöht sein. Teilweise wird Testen erschwert.

Und doch: Ich mag Singletons. Wenn ich mir vorstelle einen Logger oder eine Konfiguration nur deshalb durch acht Objekte/Methoden durchzureichen, weil ich sie in einer neunten brauche – dann empfinde ich dass als redundanten Müll im Code. Ich will lieber übersichtliche Methodennamen mit überschaubaren Parametern haben. Diese Diskussion hatte ich schon mal mit jemanden geführt, der vorschlug für Loggen stattdessen ein Logframework zu verwenden. Lustigerweise hat das Framework was er damals vorschlug … natürlich intern ein Singleton verwendet. Und die Konfiguration wirkte deutlich größer/komplexer wie mein einfacher Logger. Log-Frameworks können sinnvoll sein – oder aber auch einfach zu überladen für eine einfache Lösung. Im Zweifel kann man später immer noch auf ein großes Framework umstellen.

Eine andere Möglichkeit zur Vermeidung kann aspektorientierte Programmierung oder Code Injection sein ( siehe z.B. ninject oder POSTSHARP. Bei diesen wird beispielsweise Code bei der Generierung von Klassen aufgrund von Attributen automatisch erzeugt. Aber ganz ehrlich – das macht Code im Vergleich zu Singletons nicht einfacher zu testen. Ist aber an sich natürlich bei der Verwendung “cooler”. (Ja ich mag auch POSTSHARP California region phone , verzichte aber nicht ganz auf Singletons)

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.