Singletons und ihre Nebeneffekte

Singletons sind leider ein heutzutage viel zu viel genutzter Weg, um einfach überall an die Instanz einer Klasse heranzukommen. Bei der Suche in diversen Suchmaschinen für C#-Implementierungen dieser Konstrukte findet man oft ähnliche Umsetzungen wie diese hier:

public sealed class SomeSingleton
{
    private static SomeSingleton _instance; 

    private SomeSingleton()
    { }

    public static SomeSingleton GetInstance()
    {
        if (_instance == null) 
            _instance = new SomeSingleton();

        return _instance;
    } 
}

Sieht zwar erstmal nicht sonderlich böse aus, aber es gibt da so einige Punkte, in dem diese alles andere als freundlich zum Entwickler sind. Was ist nun, wenn in Zukunft mehr als nur eine Instanz dieser Klasse nötig ist? Wie soll das sauber getestet werden, wenn die Reihenfolge von Tests nicht vorherbestimmt werden kann? Ist es immer Transparent wo diese Klasse verwendet werden kann? 

Denn genau die Punkte, die einen Singleton stark machen, sind auch gleichzeitig seine Nachteile. Es gibt zwar definitiv nur eine Instanz und diese ist in der Regel global verfügbar, aber genau das erschwert es enorm bei der Fehlersuche. Denn was ist, wenn zwei Threads (nebenbei gemerkt ist das obige Beispiel nichtmal Thread-Safe) gleichzeitig einen Wert verändern, auf den eine andere Stelle zugreift? Es wird zu einer unter Umständen langen Fehlersuche kommen. Auch bei den bereits erwähnten Tests muss jedesmal dafür gesorgt werden, dass der Singleton sauber zurückgesetzt werden kann. Wobei dies wiederum zu Problemen in der Laufzeit führen könnte, wenn das Zurücksetzen im falschen Moment geschieht.

Natürlich gibt es auch Stellen an denen ein Singleton Sinn machen kann und auch genutzt werden sollte. Ich nutze Singletons z.B. sehr gerne in kleinen Projekten für die Konfigurationsdateien, wenn mir gerade kein ServiceManager zur Verfügung steht, denn exakt jener wäre eine viel sauberere Lösung. In der Regel sollte man Singletons so gut es geht vermeiden, um sich Ärger mit ihnen zu ersparen und stattdessen lieber auf Services setzen und entsprechende Factory-Klassen wie bei IoC-Frameworks zum Instanzieren nutzen. Und wenn es dann trotzdem noch notwendig ist ein Singleton zu nutzen, dann bieten sich folgende zwei Varianten doch deutlich mehr an als die obige. Die erste Variante ist eine Thread-Safe Version und die zweite ist ein spezieller Singleton, der durch Keys mehrfach instanziert werden kann. Zusätzlich nutzen beide Interface für die Abstraktion, um so auch in UnitTests besser ansprechbar zu sein.

public interface ISomeSingleton
{ }

public class SomeSingleton : ISomeSingleton 
{
    private static volatile SomeSingleton _instance; 
    private static object _lock = new object();

    protected internal SomeSingleton() 
    { }

    public static ISomeSingleton GetInstance()
    {
        if (_instance == null)
        {
            lock (_lock)
            {  
                if (_instance == null)
                    _instance = new SomeSingleton();
            }
        }

        return _instance;
    } 
}

Hier sollte beachtet werden, dass der Konstruktor „protected internal“ ist. Dies heißt, dass nur abgeleitete Klassen der eigenen Library darauf zugreifen können. Um dies in den Tests verwenden zu können, muss in der „AssemblyInfo.cs“ folgendes am Ende eingefügt werden:

[assembly: InternalsVisibleTo("Name.Des.UnitTes.Projekts.Ohne.Extension")]

So und nun zur zweiten Version des Singletons, der Multi-Instanz Singleton über Keys.

public interface ISomeSingleton
{ }

public class SomeSingleton : ISomeSingleton
{
    private static volatile Dictionary<string, ISomeSingleton> _instances = new Dictionary<string,ISomeSingleton>();
    private static readonly Dictionary<string, object> _locks = new Dictionary<string, object>();

    public string Key { get; private set; }

    protected internal SomeSingleton(string key)
    {
        Key = key;
    }

    public static ISomeSingleton GetInstance(string key)
    {
        if (!_instances.ContainsKey(key))
        {
            _locks.Add(key, new object());
            lock (_locks[key])
            {
                if (!_instances.ContainsKey(key) || _instances[key] == null)
                    _instances.Add(key, new SomeSingleton(key));
            }
        }

        return _instances[key];
    }

    public static bool RemoveInstance(string key)
    {
        if (!_instances.ContainsKey(key) || _locks.ContainsKey(key))
            return false;

        lock (_locks[key])
        {
            _instances[key] = null;
            _instances.Remove(key);
        }

        _locks.Remove(key);
        return true;
    }
}

Ich hoffe das hat wieder einigen weitergeholfen.