Gestern habe ich bereits ein Post zum testbaren Singleton geschrieben, in welchem ich es mir zur Aufgabe gemacht hatte eine Möglichkeit zu finden Singletons so umzusetzen, dass sie möglichst einfach zu testen und ersetzen sind. Dies ist mir nur teilweise gelungen.
Das erste System das ich vorstellte, erlaubte zwar ein Testen des Singletons, nicht aber den Austausch durch ein Fake-Objekt. Die zweite Fassung ermöglichte zwar das Ersetzen und Testen, war in meinen Augen aber zu aufwändig. Dazu kommt, dass beide Systeme dem Entwickler die Freiheit gewähren die tatsächliche Singleton-Funktionalität zu nutzen oder sich einfach selbst eine Instanz der Klasse zu erstellen.
Die Herausforderung
Bevor ich mit meiner Erläuterung weiter mache, hier noch einmal die Aufgabenstellung:
- 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
Wie sich daraus schon ablesen lässt, war meine Überschrift das letzte Mal falsch. Denn es geht nicht nur darum einen testbaren Singleton zu erstellen, sondern einen Singleton der nach Möglichkeit so wenig böse Eigenschaften hat wie möglich.
Eine Sache des Prinzips
Bei meinem letzten Versuch habe ich hauptsächlich auf Separation of Concerns gebaut und war der festen Überzeugung, eine Klasse dürfe nicht wissen wie ihre Instanzierung von Statten geht. Dies hat den Vorteil, dass ihre Wiederverwendbarkeit steigt, da sie ohne Weiteres auch ohne die Eigenschaft einzigartig zu sein überleben kann.
Der Nachteil ist jedoch, dass ich das Problem verkompliziere. Wie häufig ändert denn ein Singleton seine zentrale Eigenschaft? Wie häufig wird aus ihm eine normale Klasse? Ich würde einmal sagen: sehr selten.
Im Sinne von KISS opfere ich nun also die Möglichkeit den Singleton nachträglich leicht wieder zu verwenden, vereinfache aber seinen gesamten Aufbau und seine Handhabe. Dazu nutze ich Dinge die ich im letzten Posting bereits angesprochen hatte.
An die Umsetzung
Ich habe bei der Umsetzung wieder bewusst auf Threadsicherheit verzichtet um das Beispiel nicht zu verkomplizieren.
Der folgende Singleton sieht dem üblichen sehr ähnlich. Er hat aber zwei entscheidende Unterschiede. Sowohl Konstruktor als auch das Feld, welches die Instanz hält, sind protected und nicht private!
Der protected Konstruktor verbietet demnach die Instanzierung des eigentlichen Singleton, nicht aber die Vererbung. Es ist somit möglich eine Kindklasse zu erstellen.
public class TestFriendlySingleton : ITestFriendlySingleton { /// <summary> /// protected instance allows mocking /// </summary> protected static ITestFriendlySingleton _instance; public static ITestFriendlySingleton Instance { get { return _instance ?? (_instance = new TestFriendlySingleton()); } } /// <summary> /// Protected Constructor allows to instantiate child classes /// </summary> protected TestFriendlySingleton() {} }
Dieses Kind wiederum hat die Möglichkeit die Instanz des Singletons zu überschreiben. Da ich in meinem Fall zudem die Instanz mit einem Interfacetypen deklariere ermögliche ich es sie mit jedem beliebigen ITestFriendlySingleton zu ersetzen.
public class TestFriendlySingletonStub : TestFriendlySingleton { public static void SetInstance(ITestFriendlySingleton fake) { _instance = fake; } }
Das Ersetzen wird ermöglicht, da ich direkt den Wert des Felds setzen kann. Ist das Feld ungleich null wird dann durch den ?? Operator in der Instance-Property des Singletons auch keine Instanz erzeugt.
Somit missbrauche ich im Tesfall die Property des Singletons um die Referenz des Fake-Objekts zurück zu geben. Nach außen hin hat sich somit nichts geändert, intern wurde jedoch die Funktionalität des Singletons durch eine andere ersetzt.
Ein (ausführliches) Beispiel
Wie könnte nun aber der Einsatz dieses Konstrukts konkret aussehen? Hierzu bemühe ich mal wieder den allseits beliebten Logger. Wobei dieser der Einfachheit halber nur eine Textnachricht ohne Drumherum loggt. Vorgeschrieben wird diese Funktion über ein Interface.
public interface ILogger { void Log(string message); }
Der tatsächliche Logger mag dann in etwa so aussehen:
public class Logger : ILogger { protected static ILogger _instance; /// <summary> /// Gett of the one and only instance /// </summary> public static ILogger Instance { get { return _instance ?? (_instance = new Logger()); } } /// <summary> /// Protected Constructor allows to instantiate child classes /// </summary> protected Logger() { } /// <summary> /// Logs the specified message. /// </summary> public void Log(string message) { // do something } }
Der Stub hat sich nicht wirklich verändert.
public class LoggerStub : Logger { public static void SetInstance(ILogger mock) { _instance = mock; } }
Nun aber zum eigentlichen Fakeobjekt. Dieses macht nichts mit der übergebenen Nachricht. Warum? Weil ein tatsächlicher Logger auf das Dateisystem zugreifen möchte und wir genau das nicht wollen. Wir isolieren also unseren Testgegenstand (Subject under Test - SUT) um die Fehleranfälligkeit zu verringern. Alternativ zu diesem Dummy, kann man natürlich auch jedes beliebige Isolation Framework nutzen.
public class LoggerDummy : ILogger { public void Log(string message) {} }
Das Setup für ein Test könnte dann wie folgt aussehen und ist wirklich sehr einfach geraten.
LoggerStub.SetInstance(new LoggerDummy());
Abnahmetest
Überprüfen wir mal ob die zuvor gestellten Anforderungen erfüllt sind:
Es darf von einer Klasse nur eine Instanz geben |
|
Die Instanz muss global verfügbar sein |
|
Die Instanz muss selbst testbar sein |
|
Man muss die Instanz ersetzen können um Testbestandteile besser zu isolieren |
|
Die Lösung muss möglichst einfach sein und darf nicht Gebrauch von Frameworks machen |
|
Test bestanden!
Fazit
Mit der hier beschrieben Lösung bin ich eigentlich recht glücklich. Dennoch würde ich gerne die Instanzverwaltung auslagern. Vielleicht in eine Basisklasse welche von allen Singletons abgeleitet wird. Leider funktioniert das mit dem protected Konsturktor dann nicht mehr und demnach können hier verschiedene Instanzen erstellt werden.
Eines zeigt mir aber der Vorgang noch: Clean Code Praktiken sind wichtig. Sie zeigen uns einen guten Weg auf mit Problemen umzugehen bzw. diese zu vermeiden. Sie können uns aber im Alltagsgeschäft nur eine Hilfe sein und keine Lösung. Wie auch, dafür sind die Aufgabenstellungen viel zu komplex und unterschiedlich. Es verhält sich also wie mit den Desingpatterns, man muss immer abwägen ob der Einsatz sinnvoll ist und welchem man Vorrang gibt.
Kommentare