Kein anderes Pattern wird so oft als Antipattern bezeichnet. Der Grund ist nicht die Einzigartigkeit der Instanz — die ist manchmal richtig. Es ist die zweite Hälfte der klassischen Umsetzung: der globale, statische Zugriffspunkt.
Konfiguration.Instanz bedeutet, dass jede Klasse im Programm von überall auf dasselbe Objekt zugreifen kann. Daraus folgen vier Dinge.
1. Abhängigkeiten werden unsichtbar
Sieh dir diese Signatur an und sag, was die Klasse braucht:
public sealed class RechnungsService
{
public RechnungsService() { }
public Rechnung Erstelle(Bestellung bestellung)
{
var satz = Konfiguration.Instanz.Mehrwertsteuersatz;
var nummer = NummernGenerator.Instanz.Naechste();
Protokoll.Instanz.Schreibe("Rechnung erstellt");
// ...
}
}Der Konstruktor ist leer. Er behauptet: «Ich brauche nichts.» Tatsächlich braucht die Klasse drei globale Objekte, und das erfährst du erst, wenn du den Rumpf liest — oder wenn ein Test mit einer NullReferenceException abbricht, weil eines davon nicht eingerichtet war.
Mit Constructor Injection stünde dasselbe Wissen in der Signatur, und der Compiler würde es durchsetzen.
2. Tests werden unzuverlässig
Ein Singleton lebt so lange wie der Prozess — und ein Testlauf ist ein Prozess. Damit teilen sich alle Tests denselben Zustand:
[Fact]
public void Rechnung_mit_reduziertem_Satz()
{
Konfiguration.Instanz.Mehrwertsteuersatz = 0.026m; // für diesen Test
// ...
} // und wer setzt das zurück?Das typische Schadensbild
Die Tests laufen einzeln grün und zusammen rot. Oder sie laufen grün, bis der Runner die Reihenfolge ändert. Oder — am schlimmsten — sie laufen parallel, und ein Test verändert den Satz, während ein anderer gerade rechnet.
3. Der Zustand ist geteilt, ohne dass es jemand geplant hat
Ein Singleton in einer Webanwendung wird von allen Anfragen gleichzeitig benutzt. Was harmlos aussieht, ist es dann nicht mehr:
public sealed class Zwischenspeicher
{
public static readonly Zwischenspeicher Instanz = new Zwischenspeicher();
private readonly Dictionary<string, Kunde> _kunden = new Dictionary<string, Kunde>();
public void Lege(string id, Kunde kunde) => _kunden[id] = kunde;
}Dictionary<K,V> ist nicht threadsicher. Zwei gleichzeitige Schreibzugriffe können die interne Struktur beschädigen — in älteren Laufzeiten konnte ein späterer Lese-Zugriff dann in einer Endlosschleife hängen bleiben. Der Fehler zeigt sich als hängende Anfrage, nicht als Exception, und niemand sucht ihn in dieser Klasse.
Merksatz: Jedes Singleton mit veränderlichem Zustand ist ein Nebenläufigkeitsproblem, bis das Gegenteil bewiesen ist. ConcurrentDictionary, Interlocked oder ein lock sind Pflicht, nicht Kür.
4. Die Lebensdauer lässt sich nicht mehr ändern
Das fällt zuletzt auf und wiegt am schwersten. Konfiguration.Instanz steht an achtzig Stellen. Jetzt kommt die Anforderung, pro Mandant eine eigene Konfiguration zu führen.
Es gibt keinen kleinen Umbau dafür. Der statische Zugriffspunkt ist die Entscheidung «es gibt genau eines» — an achtzig Stellen einzementiert.
Dieselbe Einzigartigkeit, zwei Bauformen
Statischer Zugriffspunkt
public Rechnung Erstelle(Bestellung b)
{
var satz = Konfiguration
.Instanz.Mehrwertsteuersatz;
// ...
}
// Abhängigkeit unsichtbar
// Im Test nicht ersetzbar
// Lebensdauer festgelegtHereingereicht
private readonly IKonfiguration _konfig;
public RechnungsService(IKonfiguration k)
=> _konfig = k;
public Rechnung Erstelle(Bestellung b)
{
var satz = _konfig.Mehrwertsteuersatz;
}
// Abhängigkeit in der Signatur
// Im Test eine Attrappe
// Lebensdauer bestimmt Program.csRechts kann es immer noch genau eine Instanz geben. Der Unterschied ist, wer das entscheidet — und ob die Entscheidung an einer Stelle steht oder an achtzig.
Kurz nachgedacht
Zählt nicht — nur zum Prüfen, ob es angekommen ist.
1.Was genau ist am klassischen Singleton das Problem?