Warum Singletons böse sind

Update – Da diese Seite sehr stark frequentiert ist, möchte ich hier noch auf einen anderen Artikel hinweisen, der zeigt wie man einen Großteil der hier beschriebenen Probleme umgeht: http://www.just-about.net/der-freundliche-singleton


Googelt man Singletons und .NET wird man sehr schnell auf die folgende oder eine ähnliche Implementierung stoßen. Was in diesem Zusammenhang nicht erwähnt wird ist, dass auf diese Weise erstellte Singletons schnell zum Teufelswerk mutieren. Offensichtlich wird es, wenn man den obigen Suchbegriffen noch ein „evil“ hinzufügt.

public sealed class Singleton
{
   private static readonly Singleton instance = new Singleton();
   private Singleton(){}

   public static Singleton Instance
   {
      get
      {
         return instance;
      }
   }
}

Gegenüber einer rein statischen Klasse bieten sich Singletons an, wenn eine Klasse einen internen Status besitzt und von ihr nur eine einzige Instanz erstellt werden können soll. Dies wäre beispielsweise bei einem Logger der Fall. Dieser hat, gegenüber der Klasse Math als reine Functionssammlung, einen Zustand da er bspw. eine Filereference hält um die geloggten Daten in eine Datei zu schreiben. Hat die Datei eine gewisse Maximalgröße erreicht wird der Logger sie schließen und eine andere anlegen, somit ändert er seinen internen Status. Darüber hinaus kann eine Referenz auf ihn als Parameter auch Methoden übergeben werden die nicht sie nicht selbstständig bei der Instance-Property anfrage, was eine nachträgliche Änderung von Single- zu Multiinstanz erleichtern könnte, wenn es denn konsequent durchgezogen würde.

Da wir nicht in einer perfekten Welt leben wird Letzteres jedoch nicht gemacht und somit sind wir auch schon bei den negativen Seiten. Die da wären:

  1. Es gibt nur eine Instanz
  2. Diese ist auf einfachem Weg global verfügbar

Genau das was den Singleton ausmacht ist also sein größtes Problem. Denn wer sagt uns, dass wir auf Dauer wirklich nur eine Instanz des Loggers brauchen? Was wenn wir unterschiedliche Bereiche der Applikation in Zukunft unterschiedlich loggen wollen? Es beginnt der Umbau und bei dem merkt man, dass durch die Eigenschaft global zu sein Abhängigkeiten entstehen die so gar nicht geplant waren. Es ist zu einfach auf die Instanz zuzugreifen, weshalb sie eben nicht als Parameter in einen Konstruktor oder eine Methode hinein gegeben wird. Jeder Interessent kann einfach und ohne Weiteres die Instance-Property aufrufen und erhöht damit (unbemerkt) die Abhängigkeiten innerhalb des Projekts.

Besonders deutlich wird dies wenn es um das Unittesting geht. Ein kleines, zugegebenermaßen sehr konstruiertes, Beispiel soll dies verdeutlichen:

public int CountErrorMessages()
{
   var statusManager = StatusManager.Instance;
   return statusManager.StatusMessages.Count
      (message => message.Type == MessageType.Error);
}

public void AddErrorMessage(string message)
{
   var statusManager = StatusManager.Instance;
   var statusMessage = new StatusMessage
        {Message = message, MessageType = MessageType.Error}

   statusManager.AddMessage(statusMessage);
}

Methode 1 zählt alle vorhandenen Fehlermeldungen und Nummer 2 fügt eine hinzu. Nun müssen beide Methoden getestet werden. Also befüllt man zunächst den Singleton mit Testdaten. Da wir jedoch nicht sagen können in welcher Reihenfolge unsere Tests ausgeführt werden gibt es ein Problem. Denn welcher Wert wird von CountErrorMessages erwartet der vor oder nach dem Hinzufügen?

Also müssen wir unsere Testdaten, so wie man das immer tun sollte, vor jedem Test zurück setzen, aber können wir das so einfach? Nein, bzw. nur sehr aufwändig denn wir haben keine Chance den Singleton zu mocken. Wir können ihn dank der starren Bindung nicht einfach ersetzen. Selbst Vererbung, insofern sie den möglich ist, bringt uns nichts.  Dies tut vor allem dann weh, wenn die Komponente nicht in unserem Einflussbereich liegt weil sie von einem anderen Entwickler(team) stammt.

public sealed class StatusManager
{
   private static readonly StatusManager instance = new StatusManager();

   private StatusManager() { }

   public static StatusManager Instance
   {
      get
      {
         return instance;
      }
   }

   private List<StatusMessage> statusMessages = new List<StatusMessage>();

   public IEnumerable<StatusMessage> StatusMessages
   {
      get
      {
         return this.statusMessages;
      }
   }

   public event EventHandler<StatusEventArgs> StatusArrivedEvent;

   public void AddMessage(StatusMessage message)
   {
     statusMessages.Add(message);

     . . .

     if(StatusArrivedEvent != null)
       StatusArrivedEvent(this, new StatusEventArgs(message));
   }
}

Weiterhin wissen wir auch in anderen Tests nicht ob der Singleton nicht evtl. doch verwendet wird, wir testen ihn also unbewusst mit und dies führt dazu, dass ein Fehler in seiner Implementierung auch die Tests ganz anderer Komponenten beeinflussen könnte. Was wenn ich im obigen Beispiel die Prüfung auf null beim Eventhandler vergessen hätte? Dann schlüge jeder Test fehl in dem eine Statusnachricht hinterlegt wird ohne dass es einen Abonnenten für das Ereignis gibt. Genau dies ist bei Unittests aber meist der Fall, denn warum sollte ich mich beim Testen einer Datenanbindung darum kümmern ob mein Logging richtig aufgesetzt ist? Dies ist Teil des Integrationstests und hat hier nichts zu suchen.

Letztendlich soll zusammengefasst werden, dass sich die Aussage „Singletons sind böse“ auf die obige Implementierung bezieht. Natürlich gibt es Fälle in denen sie notwendig sind da beispielsweise eine Hardwareschnittstelle abstrahiert werden soll die nur einmalig verfügbar ist. ABER dann sollte sich die Komponente nicht selbst um ihre Erstellung kümmern, sondern eine entsprechende Factory wie sie in IoC-Frameworks verwendet werden.

Ein Kommentar

  1. Hi Hendrik,

    guter Artikeln, dem kann ich nur zustimmen.

    Wir haben beispielsweise mehrere Legacyanwendungen zusammen unter einen neuen „Hut“ mittels CAB gesteckt. Die alten Anwendungen haben Gebrauch von statischen Klassen und Singletons an zentralen Stellen gemacht, die für sie zur damaligen Zeit „global“ waren (bsp. Logger, zentrale Controller usw.).

    Doch nun sollen mehrere Instanzen der alten Anwendung parallel in einer neuen Shell laufen, so dass genau diese Elemente eben nicht mehr einen globalen Wirkungskreis haben. So mussten wir also diese statischen/singleton Altlasten mühsam raus-refactoren.

    Merke: Vermeitlich globale Wirkungsbereiche können später zu lokaler Bedeutung reduziert werden.

    Jörg

Kommentar hinterlassen