Zum Inhalt springen

15. Entwurfsmuster I – Singleton

Zu Zen-Modus wechseln

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"]
  • 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, der new von 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.

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 VariableKlasse mit privatem Konstruktor
Einmaligkeit durchModulsystem (wird nur einmal ausgewertet)private constructor und getInstance()
Aufwandgering, kein eigenes Muster-Gerüstmehr Code, dafür sprachübergreifend erkennbar
Typisch inJavaScript und TypeScriptPrü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 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 Codeja, als Parameter
Frische Instanz im Testnur über Reset-Methodeeinfach übergeben
Zustandglobal, überlebt Testfällegehört dem, der ihn übergibt
Aufwandgeringetwas 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.

Diese Abschnitte gehören nicht zum Vortrag. Lies sie vor der Übung nach.

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

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).

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

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.
  • 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.

GAM

CC-BY-SA

https://qiz.at/rules