Ich hatte mich vor einiger Zeit in einem Blogpost kritisch über Singletons und deren Eigenschaften geäußert. Ganz fair war ich in diesem Post nicht, wie mir auch Thomas Mentzel zeigte. Denn wer meckert, sollte auch Gegenvorschläge bringen und genau das möchte ich jetzt einmal versuchen.
(Un)sinn ?
Zunächst möchte ich festhalten, dass das Singleton Pattern an sich seine Berechtigung hat und diese, auch wenn der Titel meines letzten Posts zu dem Thema etwas anderes vermuten lässt, von mir nicht in Zweifel gezogen wird.
Immerhin braucht man gelegentlich wirklich eine zentrale Anlaufstelle. Man denke an den typischen Logger, verschiedene Factories oder Wrapper zu unmanaged Code. Hier ist man tatsächlich darauf angewiesen, dass sie global verfügbar sind. Testen möchte man sie aber nach Möglichkeit auch. Was also tun?
Die Herausforderung
Im Zusammenhang mit IoC-Frameworks ist dies kein Problem, denn diese übernehmen das Instanziieren und somit kann man ihnen auch überlassen wie sie Singletons umsetzen. Einzig die Konfiguration muss auf irgend eine Weise sagen, dass es nicht mehr als eine Instanz der Klasse geben darf. Wie soll man aber vorgehen wenn man kein IoC nutzen kann?
Dies war auch die Frage die ich mir eingangs gestellt und aus der ich folgende Anforderungen für mich abgeleitet habe:
- Es darf von der Klasse nur eine Instanz geben
- Diese Instanz muss global verfügbar sein
- Diese Instanz muss selbst testbar sein
- Man muss diese Instanz ersetzen können um Testbestandteile besser zu isolieren
- Die Lösung muss möglichst einfach sein und darf nicht Gebrauch von gängigen Frameworks machen
Testbare Singletons
Grundsätzlich finde ich es eigenartig, dass sich ein Singleton selbst seiner Einzigartigkeit bewusst sein soll. Im Sinne von Separation of Concerns ist es doch ein Fehler zu denken, dass der Singleton selbst über seine Erstellung verfügt. Dies wird vorallem dann schmerzlich wenn er eben nicht mehr Single ist.
Mit diesem Gedankengang kommt man recht schnell zu einer Lösung wie sie zum Beispiel bei Dotnet-Snippets.de zu finden ist. Sie hat den Vorteil, dass die Singleton-Klasse völlig unabhängig von ihrer folgenden statischen Verwendung ist. Darüber hinaus kann jede Klasse nun als Singleton deklariert werden.
Ein Nachteil bleibt aber: Es ist unmöglich dieses Konstrukt im Zusammenhang mit anderen zu mocken.
public static class Singleton<T> where T : class, new() { public static T instance; public static T Instance { get { return instance ?? (instance = new T()); } } }
Aufbrechen der Abhängigkeiten
Das Vorgehen des obigen Beispiels ist löblich. Immerhin wird die Singleton-Klasse dadurch endlich testbar. Leider werden aber die Abhängigkeiten nicht aufgebrochen. Denn Ruft man nun Singleton auf, erstellt dieser intern auch nur wieder eine Instanz des angeforderten Typs und diese kann man nicht ersetzen. Meine wichtigste Anforderung ist demnach nicht erfüllt.
Man merkt schon, will man anständig testen kommt man an IoC einfach nicht vorbei. Denn nur damit ist es möglich die Erzeugung einer Instanz zu enkoppeln.
Ich habe mir lange den Kopf zerbrochen wie man es noch machen könnte und bin letztendlich an einem zweistufigen Service-Locator hängen geblieben. Das eigentliche Verfahren wurde von Martin Fowler in einem seiner bekanntesten Postings bereits ausgiebig beschrieben, weshalb ich mir das an der Stelle schenke.
Grundsätzlich sei aber erklärt, dass über einen Service-Locator Instanzen angefragt werden die dann von ihm verwaltet werden. Was hindert uns also daran, dass alle Instanzen Singletons sind?
public class SingletonFactory { protected static Dictionary<type, object> _instances = new Dictionary<type, object>(); public static T Get<T>() where T : class, new() { lock (_instances) { var type = typeof (T); if (!_instances.ContainsKey(type)) { _instances.Add(type, new T()); } return _instances[type] as T; } } }
Leider erlaubt uns das Beispiel nicht Interfaces zu registrieren, da wir mit ihnen ja keine Instanz erstellen können. Durch Reflection und anderem, könnte man dem zwar abhelfen, dann wird das Beispiel aber zu komplex…
// will not compile var logger = SingletonFactory.Get();
Einen Stub bitte…
Aber warte, wie soll man damit jetzt bitte schön mocken?
Dadurch, dass das Dictionary der Factory nur protected gekennzeichnet ist, hat jede Kindklasse Zugriff auf die verwalteten Instanzen und weil die Factory selbst nicht als static gekennzeichnet ist, kann es überhaupt erst Kinder geben. Demnach muss man sich für den Test nur eine Klasse von SingletonFactory ableiten, welche dann entsprechende Methoden bietet mit denen das Dictionary verändert werden kann.
public class SingletonFactoryStub : SingletonFactory { public static void Add(T instanceToAdd) { lock (_instances) { var type = typeof(T); if (_instances.ContainsKey(type)) { throw new ArgumentException(string.Format("Instance of type {0} already present!", type)); } _instances.Add(type, instanceToAdd); } } public static void Remove() { lock (_instances) { var type = typeof(T); _instances.Remove(type); } } public static void Clear() { lock (_instances) { _instances.Clear(); } } }
Warum habe ich die Methoden in eine abgeleitete Klasse verschoben? Tatsächlich kann man sie auch in SingletonFactory belassen und damit die Zuordnung zu einzelnen Typen bereits beim Start der Applikation vornehmen. Dank Remove und Clear wäre es dann aber auch zu jeder Zeit möglich die Konfiguration zu ändern und dies kann eigentlich nicht gewünscht sein.
Die Ernüchterung
Das wärs, könnte ich jetzt sagen. Stimmt aber nicht. Mocken kann man die bisherigen Singletons nämlich immer noch nicht. Denn dazu müssen diese noch etwas angepasst werden bzw. müssen folgenden Anforderungen genügen:
- dürfen nicht sealed, static o.ä. sein
- alle Methoden müssen virtual gekennzeichnet sein
Einen weiteren gravierenden Nachteil habe ich verschwiegen: Es wird nicht offensichtlich, dass es sich bei einer Klasse um einen Singleton handelt. Demnach weiß ein Programmierer nicht zwanglsläufig, dass man diesen über den Service-Locator anfragen muss. Dies könnte man nur über einen privaten Konstruktor lösen, der wiederum das Ableiten verhindert, wodurch die Klasse nicht gemockt werden kann.
Man dreht sich also im Kreis. Entweder mocken/testen oder echter Singleton. Das ist auch der Grund warum ich die Überschrift mit „Version 1.0“ erweitert habe. Denn in meinen Augen ist hier das Ende der Fahnenstange noch nicht erreicht. Irgend wie muss man es doch hinbekommen, dass man die beiden scheinbaren Gegensätzlichkeiten vereint. Vorallem muss es doch auch einfacher gehen.
Hier geht es zum 2. Teil: Der testfreundliche Singleton