15. Entwurfsmuster I – Singleton
Nach diesem Kapitel kannst du:
- einen Singleton in TypeScript implementieren, als Modul mit gekapselter Variable und als Klasse mit privatem Konstruktor.
- die beiden Formen des Singletons vergleichen und für einen Einsatz die passende wählen.
- die Kritik am Singleton (globaler Zustand, Testabhängigkeit) beurteilen und abwägen, wann er trotzdem bequem ist.
- Dependency Injection als Alternative zum Singleton benennen und eine Funktion so umbauen, dass sie ohne globalen Zustand testbar wird.
In Kapitel 11 hast du Entwurfsmuster als eingeübte Standardsituationen eines Fußballteams kennengelernt, in den Kapiteln 12 bis 14 die fünf SOLID-Prinzipien dahinter. Jetzt kommen die beiden Erzeugungsmuster, und das Fußballteam liefert wieder die Bilder: Der Schiedsrichter ist ein Singleton, die Auswechselbank eine Factory. Dieses Kapitel gehört dem Schiedsrichter, die Auswechselbank folgt in Kapitel 16.
Der Schiedsrichter ist ein gutes Bild, weil er beide Seiten des Musters zeigt. Es gibt genau einen, und alle richten sich nach ihm, das ist der Nutzen. Aber jede Entscheidung auf dem Platz hängt an dieser einen Person, ohne dass es auf der Aufstellung steht, und genau das ist die Kritik, um die es im zweiten Teil geht.
flowchart LR accTitle: Alle auf dem Platz richten sich nach dem einen Schiedsrichter, so wie aller Code dieselbe eine Instanz nutzt H["Heimteam"] --> R["Schiedsrichter: genau einer"] G["Gastteam"] --> R T["Trainer"] --> R R -.->|im Code| S["Singleton: eine Instanz, ein Zugriffspunkt"]
Begriffe vorab
Abschnitt betitelt „Begriffe vorab“- Erzeugungsmuster (creational pattern): Muster, das regelt, wie und wie viele Objekte entstehen und wer darüber entscheidet.
- Singleton: Erzeugungsmuster, das sicherstellt, dass es von einer Klasse genau eine Instanz gibt, und einen Zugriffspunkt darauf bereitstellt.
- Privater Konstruktor: Konstruktor mit
private, dernewvon außerhalb der Klasse verbietet. - Statische Zugriffsmethode: Methode der Klasse selbst (nicht einer Instanz), hier
getInstance(), die die eine Instanz erzeugt und liefert. - Globaler Zustand (global state): Daten, die von überall im Programm gelesen und geändert werden können.
- Dependency Injection: Eine Abhängigkeit wird von außen hereingereicht, statt dass der Code sie sich selbst besorgt.
Singleton: genau eine Instanz
Abschnitt betitelt „Singleton: genau eine Instanz“Problem: Von einer Sache soll es im ganzen Programm genau eine geben - eine Konfiguration, eine Verbindung zu einem externen Dienst, eine zentrale Warteschlange. Mehrere Instanzen wären falsch oder verschwenderisch. Im Spiel ist das der Schiedsrichter: Es gibt genau einen, und alle richten sich nach ihm.
Lösung: Eine Klasse kontrolliert selbst, dass es nur eine Instanz gibt, und stellt einen Zugriffspunkt darauf bereit.
In JavaScript und TypeScript ist die einfachste Form ein Modul mit einer gekapselten Variablen - Module werden nur einmal ausgewertet, das erledigt die Einmaligkeit von selbst. Den Code der Modul-Form findest du unter Weitere Infos.
Die klassische, sprachübergreifende Form mit einer Klasse sieht so aus (und ist die, die in Prüfungen gezeichnet wird):
class RenderQueue { private static instance: RenderQueue | null = null; private jobs: string[] = [];
private constructor() {} // no "new" from outside
static getInstance(): RenderQueue { if (RenderQueue.instance === null) { RenderQueue.instance = new RenderQueue(); } return RenderQueue.instance; }
add(job: string): void { this.jobs.push(job); } size(): number { return this.jobs.length; }}Beispielprojekt: 15_entwurfsmuster_singleton_examples/ex02_singleton_render_queue
Zwei Bausteine tragen das Muster: der private Konstruktor (verhindert new RenderQueue() von außen) und die statische Zugriffsmethode, die die eine Instanz erzeugt und wiedergibt.
classDiagram
class RenderQueue {
-static instance: RenderQueue
-jobs: string[]
-constructor()
+static getInstance() RenderQueue
+add(job) void
+size() number
}
RenderQueue --> RenderQueue : hält eine Instanz
Die Selbstreferenz im Klassendiagramm ist das Erkennungsmerkmal des Singletons. Die Notation (Sichtbarkeit - und +, Assoziation) kennst du aus Kapitel 09.
Welche der beiden Formen du wählst, hängt vom Einsatz ab:
| Modul mit gekapselter Variable | Klasse mit privatem Konstruktor | |
|---|---|---|
| Einmaligkeit durch | Modulsystem (wird nur einmal ausgewertet) | private constructor und getInstance() |
| Aufwand | gering, kein eigenes Muster-Gerüst | mehr Code, dafür sprachübergreifend erkennbar |
| Typisch in | JavaScript und TypeScript | Prüfungen, Java, C# und andere Sprachen |
Wo begegnet dir das? Bei der zentralen Konfiguration eines Frameworks, einem Datenbank-Verbindungspool oder einem Logger: jeweils eine Instanz, überall verfügbar. In Spielen ist ein zentraler Audio- oder Spielstandsmanager ein typischer Kandidat.
Singleton in der Kritik: globaler Zustand
Abschnitt betitelt „Singleton in der Kritik: globaler Zustand“Singleton ist das umstrittenste der klassischen Muster, und die Kritik gehört zum Wissen dazu:
- Ein Singleton ist globaler Zustand mit schönem Namen. Jeder Code, der
getInstance()aufruft, hängt heimlich an dieser einen Instanz, ohne dass es in seiner Signatur steht. - Das rächt sich im Test: Der Zustand des Singletons überlebt von einem Testfall in den nächsten. Test A legt zwei Jobs in die Queue, Test B erwartet eine leere Queue und schlägt fehl - abhängig von der Reihenfolge, in der die Tests laufen. Solche Fehler sind zäh zu finden.
sequenceDiagram accTitle: Test A hinterlässt Jobs in der Queue, Test B erwartet eine leere Queue participant A as Test A participant Q as RenderQueue participant B as Test B A->>Q: add(job 1), add(job 2) B->>Q: size() Q-->>B: 2 (erwartet: 0)
Ein Ausweg für Tests ist eine Reset-Methode resetForTests(), aber schon ihre Notwendigkeit ist ein Warnzeichen.
Die bessere Antwort ist oft, das Singleton gar nicht zu holen, sondern die Abhängigkeit hereinzureichen (Dependency Injection - die praktische Form des D aus SOLID, siehe Kapitel 14): Die Warteschlange wird als Parameter übergeben, statt sich über getInstance() selbst besorgt zu werden. Dann ist im Test eine frische Instanz übergebbar, und der globale Zustand verschwindet. Wann ein Singleton bequem ist und was er im Test kostet, ist genau die Abwägung, die du treffen können sollst.
Singleton per getInstance() | Dependency Injection | |
|---|---|---|
| Abhängigkeit sichtbar in der Signatur? | nein, versteckt im Code | ja, als Parameter |
| Frische Instanz im Test | nur über Reset-Methode | einfach übergeben |
| Zustand | global, überlebt Testfälle | gehört dem, der ihn übergibt |
| Aufwand | gering | etwas mehr Verdrahtung |
Im Fußballbild: Ein Spiel, in dem sich jeder Spieler “den” Schiedsrichter selbst sucht, lässt sich nicht mit einem Ersatz-Schiedsrichter proben. Wird der Schiedsrichter dagegen vor dem Anpfiff eingeteilt, kann das Training einen eigenen bekommen.
flowchart LR accTitle: Mit Dependency Injection übergibt die Produktion die gemeinsame Queue, der Test eine frische P["Produktion"] -->|gemeinsame Instanz| F["processJobs(queue)"] T["Test"] -->|frische Instanz| F
Wo begegnet dir das? Wenn Tests nur “manchmal” fehlschlagen oder in einer anderen Reihenfolge anders ausgehen, steckt häufig geteilter globaler Zustand dahinter. Das gilt für selbst gebaute Singletons genauso wie für zentrale Objekte in Frameworks. Systematisch testen lernst du in den Kapiteln 19 und 20.
Auflösung
Die Funktion nimmt die Warteschlange als Parameter entgegen (processJobs(queue)), typischerweise als kleines Interface statt als konkrete Klasse. Die Produktion übergibt die eine gemeinsame Instanz, der Test eine frische. Die Abhängigkeit steht jetzt in der Signatur, und der Zustand eines Tests kann den nächsten nicht mehr beeinflussen.
Weitere Infos
Abschnitt betitelt „Weitere Infos“Diese Abschnitte gehören nicht zum Vortrag. Lies sie vor der Übung nach.
Die Modul-Form im Code
Abschnitt betitelt „Die Modul-Form im Code“interface AppConfig { mediaRoot: string; maxUploadMb: number;}
let config: AppConfig | null = null;
export function loadConfig(): AppConfig { if (config === null) { console.log("reading configuration ..."); // runs exactly once config = { mediaRoot: "/srv/media", maxUploadMb: 512 }; } return config;}Beispielprojekt: 15_entwurfsmuster_singleton_examples/ex01_singleton_config_module
Nachgedacht: new oder getInstance()
Abschnitt betitelt „Nachgedacht: new oder getInstance()“Auflösung
new RenderQueue() ist ein Compilerfehler, weil der Konstruktor privat ist. getInstance() ist der einzige Weg zur Instanz, und jeder Aufruf liefert dieselbe: a === RenderQueue.getInstance() ist true. Das Beispielprojekt ex02_singleton_render_queue zeigt beides (die new-Zeile steht dort auskommentiert).
Die Reset-Methode im Code
Abschnitt betitelt „Die Reset-Methode im Code“Ein Ausweg für Tests ist eine Reset-Methode, aber schon ihre Notwendigkeit ist ein Warnzeichen:
static resetForTests(): void { RenderQueue.instance = null;}Beispielprojekt: 15_entwurfsmuster_singleton_examples/ex02_singleton_render_queue
Zusatzmaterialien
Abschnitt betitelt „Zusatzmaterialien“- Gamma, Helm, Johnson, Vlissides, Design Patterns (GoF, 1994) - Singleton im Original, Kapitel “Creational Patterns”; lohnt sich, um die Quelle des Vokabulars einmal gesehen zu haben.
- refactoring.guru: Singleton - Problem, Struktur und Vor- und Nachteile des Musters auf einer Seite, gut zum Wiederholen vor der Schularbeit.
- MDN: JavaScript-Module - erklärt, warum ein Modul nur einmal ausgewertet wird, und damit, warum die Modul-Form überhaupt ein Singleton ist.
- Martin Fowler: Inversion of Control Containers and the Dependency Injection pattern - der klassische Artikel zur Dependency Injection als Gegenentwurf zum selbst geholten Singleton.
Zum Anhören
Abschnitt betitelt „Zum Anhören“Du kannst dir diesen Text mit der Vorlesefunktion deines Browsers oder Betriebssystems kostenlos anhören, etwa mit “Laut vorlesen” in Microsoft Edge. Das eignet sich gut zum Wiederholen vor der Schularbeit.
Lernergebnisse: Was du nach diesem Kapitel können solltest
Abschnitt betitelt „Lernergebnisse: Was du nach diesem Kapitel können solltest“- Anwenden: einen Singleton in TypeScript implementieren, als Modul mit gekapselter Variable und als Klasse mit privatem Konstruktor und
getInstance(). - Analysieren: die beiden Formen des Singletons vergleichen und für einen Einsatz die passende wählen.
- Beurteilen: die Kritik am Singleton (globaler Zustand, Testabhängigkeit) erklären und abwägen, wann er trotzdem bequem ist.
- Anwenden: Dependency Injection als Alternative benennen (Bezug zu Dependency Inversion aus Kapitel 14) und eine Funktion so umbauen, dass sie ohne globalen Zustand testbar wird.
Passende Übungen
Abschnitt betitelt „Passende Übungen“- Aufgabe 10 - Jahresprojekt Sprint 3 - Singleton und Factory (gemeinsam mit Kapitel 16 in einer Woche): Kata: die Testfalle eines Singletons auslösen und mit Dependency Injection beheben, dazu das Planning von Sprint 3 (Wesentlich_überwiegend); globalen Zustand im Projekt prüfen, Factory mit Registry einführen oder den Verzicht begründen, Muster-Steckbrief (Wesentlich_vollständig). PlusPlus: Umbau mit Dependency Injection und Erweiterungsnachweis; MandalorianPlus: Muster in einer Bibliothek des Projekts.
Autoren
Abschnitt betitelt „Autoren“GAM
CC-BY-SA