16. Entwurfsmuster I – Factory und Registry
Nach diesem Kapitel kannst du:
- eine Factory in TypeScript implementieren, mit gemeinsamem Interface, einer Klasse je Variante und einer Registry.
- begründen, warum eine Factory das Open-Closed-Prinzip stützt und den Aufrufer von konkreten Klassen entkoppelt.
- eine
switch-Fabrik und eine Registry beim Erweitern um einen neuen Typ vergleichen. - Singleton und Factory in bestehendem Code und in Frameworks wiedererkennen.
Im Fußballteam aus Kapitel 11 ist der Schiedsrichter ein Singleton (Kapitel 15), die Auswechselbank eine Factory. Der Trainer ruft nicht jeden Ersatzspieler beim Namen, er sagt “ich brauche einen Stürmer”, und die Bank liefert den passenden Spieler. Wer konkret aufs Feld läuft, entscheidet eine Stelle, nicht der Trainer bei jedem Wechsel neu.
flowchart LR accTitle: Der Trainer verlangt eine Position, die Auswechselbank liefert den passenden Spieler T["Trainer"] -->|"ich brauche einen Stürmer"| B["Auswechselbank"] B --> S["passender Stürmer"] B -.->|im Code| F["Factory: liefert die passende Klasse"]
Genau diese Frage lässt das Open-Closed-Prinzip aus Kapitel 12 offen: Wenn jede Variante ihre eigene Klasse hat, wer wählt dann aus, welche entsteht? Dieses Kapitel beantwortet sie mit der Factory und ihrer besten Form, der Registry, und schließt Entwurfsmuster I mit dem Blick auf Muster in fremdem Code ab.
Begriffe vorab
Abschnitt betitelt „Begriffe vorab“- Factory (Fabrik): Erzeugungsmuster, bei dem eine eigene Stelle anhand einer Eingabe entscheidet, welche konkrete Klasse entsteht.
- Registry: Tabelle (hier eine
Map), in der jede Variante unter einem Schlüssel eingetragen ist; die Factory schlägt dort nach, statt zu verzweigen. - Erzeugungsfunktion: kleine Funktion wie
() => new ImageThumbnailer(), die bei jedem Aufruf ein neues Objekt liefert. - Aufrufer: der Code, der ein Objekt braucht und benutzt, es aber nicht selbst auswählt.
Factory: der Aufrufer muss die Klasse nicht kennen
Abschnitt betitelt „Factory: der Aufrufer muss die Klasse nicht kennen“Problem: An einer Stelle muss ein Objekt erzeugt werden, aber welche konkrete Klasse es sein soll, hängt von einer Eingabe ab - dem Medientyp, dem Ausgabeformat, der Konfiguration. Schreibt man das direkt hin, entsteht überall dieselbe Fallunterscheidung. Im Spiel ist das die Auswechselbank: Der Trainer sagt “ich brauche einen Stürmer”, und die Bank liefert den passenden Spieler, ohne dass der Trainer jeden einzeln aufrufen muss.
Der Ausgangspunkt ist eine switch-Anweisung, die sich durch den Code zieht:
// the problem: this switch appears wherever a thumbnail is neededfunction createThumbnail(kind: string, source: string): string { switch (kind) { case "image": return `${source}: scaled still image`; case "video": return `${source}: frame at second 1, then scaled`; case "audio": return `${source}: waveform image`; default: throw new Error(`unknown kind ${kind}`); }}Kommt ein vierter Medientyp dazu, muss jede solche Stelle gefunden und geändert werden. Das verstößt gegen das Open-Closed-Prinzip (das O aus SOLID, Kapitel 12): offen für Erweiterung, geschlossen für Änderung.
Die Lösung: Ein gemeinsames Interface Thumbnailer, eine Implementierung je Variante (ImageThumbnailer, VideoThumbnailer, AudioThumbnailer) und eine Fabrik, die anhand der Eingabe die passende liefert. Den Code der Varianten findest du unter Weitere Infos.
classDiagram
class Thumbnailer {
<<interface>>
+create(source) string
}
class ImageThumbnailer
class VideoThumbnailer
class AudioThumbnailer
class Factory {
+createThumbnailer(kind) Thumbnailer
}
Thumbnailer <|.. ImageThumbnailer
Thumbnailer <|.. VideoThumbnailer
Thumbnailer <|.. AudioThumbnailer
Factory ..> Thumbnailer : liefert
Wo begegnet dir das? Überall dort, wo ein Aufruf ein Objekt “passend zur Eingabe” zurückgibt, ohne dass der Aufrufer die konkrete Klasse nennt: Funktionen, die je nach Dateiendung den passenden Parser liefern, oder je nach angefragtem Format den passenden Serialisierer.
Registry: ein neuer Typ ist eine neue Zeile
Abschnitt betitelt „Registry: ein neuer Typ ist eine neue Zeile“Die Fabrik selbst lässt sich als switch schreiben - oder, meist besser, als Registry: eine Tabelle, in die sich Varianten eintragen. Dann ist das Hinzufügen eines Typs ein Eintrag in der Tabelle, ohne Änderung am Fabrik-Code:
class UnknownMediaKindError extends Error { constructor(kind: string) { super(`no thumbnailer registered for media kind "${kind}"`); this.name = "UnknownMediaKindError"; }}
const thumbnailers = new Map<MediaKind, () => Thumbnailer>([ ["image", () => new ImageThumbnailer()], ["video", () => new VideoThumbnailer()], ["audio", () => new AudioThumbnailer()],]);
function createThumbnailer(kind: string): Thumbnailer { const factory = thumbnailers.get(kind as MediaKind); if (!factory) throw new UnknownMediaKindError(kind); return factory();}Der entscheidende Gewinn zeigt sich am Aufrufer: Er kennt keine einzige konkrete Klasse mehr und ändert sich nie wieder, egal wie viele Medientypen dazukommen:
function buildThumbnails(sources: { kind: string; path: string }[]): string[] { return sources.map((source) => createThumbnailer(source.kind).create(source.path));}Beispielprojekt: 16_entwurfsmuster_factory_registry_examples/ex01_factory_thumbnailer
flowchart LR accTitle: Der Aufrufer fragt die Factory, die Registry liefert die passende Thumbnailer-Klasse C["Aufrufer: buildThumbnails"] -->|kind| F["createThumbnailer"] F -->|Eintrag suchen| R["Registry: Map"] R -->|passende Klasse| T["Thumbnailer"] F -.->|kein Eintrag| X["UnknownMediaKindError"]
Warum die Registry-Variante der switch-Fabrik vorzuziehen ist, zeigt sich erst beim Erweitern:
switch-Fabrik | Registry | |
|---|---|---|
| Neuer Medientyp | Fabrik-Code öffnen, neuen case einfügen | neue Klasse, neuer Wert in MediaKind, eine Zeile in der Map |
| Änderung an bestehendem Code | ja, an der Fabrik-Funktion | nein, createThumbnailer bleibt unberührt |
| Aufrufer | unverändert | unverändert |
Das ist derselbe Unterschied wie zwischen dem schlechten und dem besseren OCP-Beispiel aus Kapitel 12, nur diesmal an der Stelle angewendet, an der Objekte entstehen statt an der Stelle, an der sie sich verhalten. Damit ist auch die offene Frage aus Kapitel 12 beantwortet, wer beim neuen Kundentyp die passende Rabatt-Strategie auswählt: eine Factory.
Wo begegnet dir das? In Plugin-Systemen, in denen sich Dateiformate, Exportarten oder Zahlungsanbieter selbst registrieren, und in jedem Framework, das anhand einer Konfiguration die passende Implementierung auswählt.
Muster in freier Wildbahn
Abschnitt betitelt „Muster in freier Wildbahn“Beide Muster stecken in Software, die du täglich benutzt, meist ohne dass es dransteht:
- Singleton-artig: die zentrale Konfiguration eines Frameworks, ein Datenbank-Verbindungspool, ein Logger - jeweils eine Instanz, überall verfügbar.
- Factory-artig: überall dort, wo ein Aufruf ein Objekt “passend zur Eingabe” zurückgibt, ohne dass der Aufrufer die konkrete Klasse nennt - etwa Funktionen, die je nach Dateiendung den passenden Parser liefern, oder je nach angefragtem Format den passenden Serialisierer.
Die Übung, Muster in fremdem Code aufzuspüren, ist mehr als ein Spiel: Sie ist die Vorstufe dazu, sich in einer unbekannten Codebasis zurechtzufinden. Wer die Muster erkennt, versteht die Struktur schneller als jemand, der Zeile für Zeile liest.
flowchart LR
accTitle: Erst das Problem benennen, dann prüfen, ob ein Muster passt
P["Problem benennen"] --> Q{"Löst ein bekanntes Muster genau dieses Problem?"}
Q -->|ja| A["Muster anwenden und anpassen"]
Q -->|nein| E["einfachen Code schreiben"]
Wo begegnet dir das? Wenn du ein fremdes Open-Source-Projekt oder den Code eines Teammitglieds liest: Findest du ein getInstance(), ein Verzeichnis factories oder eine Map, die Namen auf Klassen abbildet, hast du die Struktur schon zur Hälfte verstanden. In Aufgabe 10 suchst du solche Stellen in deinem Projekt und in einer Bibliothek, die ihr verwendet.
Auflösung
(a) ist Singleton-artig: Eine Instanz steht überall zur Verfügung (hier als Modul-Variable, wie in loadConfig aus Kapitel 15). (b) ist Factory-artig: Der Aufrufer nennt die konkrete Klasse nicht, die Funktion entscheidet anhand der Eingabe.
Weitere Infos
Abschnitt betitelt „Weitere Infos“Diese Abschnitte gehören nicht zum Vortrag. Lies sie vor der Übung nach.
Interface und Varianten im Code
Abschnitt betitelt „Interface und Varianten im Code“type MediaKind = "image" | "video" | "audio";
interface Thumbnailer { readonly kind: MediaKind; create(source: string): string;}
class ImageThumbnailer implements Thumbnailer { readonly kind = "image" as const; create(source: string): string { return `${source}: scaled still image`; }}
class VideoThumbnailer implements Thumbnailer { readonly kind = "video" as const; create(source: string): string { return `${source}: frame at second 1, then scaled`; }}// ... AudioThumbnailer analogBeispielprojekt: 16_entwurfsmuster_factory_registry_examples/ex01_factory_thumbnailer
Genauer erklärt: eine neue Zeile
Abschnitt betitelt „Genauer erklärt: eine neue Zeile“Genauer erklärt: “Eine neue Zeile” heißt präzise: Du schreibst die neue Klasse (z. B. PdfThumbnailer), ergänzt den Wert "pdf" im Typ MediaKind und trägst sie in die Map ein. Der Compiler hilft dir dabei, weil Map<MediaKind, ...> nur bekannte Werte zulässt. Was du nicht anfasst, sind createThumbnailer und alle Aufrufer. Eine Eingabe ohne Eintrag führt bewusst zu einem klaren Fehler (UnknownMediaKindError) statt zu einem stillen Fehlverhalten.
Auflösung
Bei einem über den Code verteilten switch änderst du jede Stelle, an der er steht. Bei der Registry schreibst du einen PdfThumbnailer, ergänzt "pdf" in MediaKind und trägst eine Zeile in die Map ein. createThumbnailer und die Aufrufer bleiben unverändert, also null Aufrufer.
Eine Factory in vier Schritten einführen
Abschnitt betitelt „Eine Factory in vier Schritten einführen“Du kannst eine Factory in vier Schritten einführen:
- Gemeinsames Interface festlegen. Was müssen alle Varianten können (
create(source))? - Pro Variante eine Klasse schreiben, die das Interface implementiert.
- Registry anlegen, die jedem Schlüssel (hier
MediaKind) eine Erzeugungsfunktion zuordnet. - Aufrufer umstellen, sodass er nur noch die Factory-Funktion und das Interface kennt.
Zusatzmaterialien
Abschnitt betitelt „Zusatzmaterialien“- Gamma, Helm, Johnson, Vlissides, Design Patterns (GoF, 1994) - Singleton und Factory Method im Original, Kapitel “Creational Patterns”; lohnt sich, um die Quelle des Vokabulars einmal gesehen zu haben.
- refactoring.guru: Creational Patterns - Übersichtsseite; enthält auch Abstract Factory und Builder, die in diesem Kapitel bewusst nicht behandelt werden, weil Singleton und Factory für den Einstieg genügen.
- refactoring.guru: Factory Method - zeigt die klassische GoF-Struktur mit Unterklassen, gut als Vergleich zur Registry-Variante dieses Kapitels.
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: eine Factory in TypeScript implementieren, mit gemeinsamem Interface, einer Klasse je Variante und einer Registry.
- Analysieren: an einem Muster zeigen, welches SOLID-Prinzip es umsetzt (Factory zu Open-Closed, Dependency Injection zu Dependency Inversion), und begründen, warum eine Factory den Aufrufer von konkreten Klassen entkoppelt.
- Beurteilen: eine
switch-Fabrik und eine Registry beim Erweitern um einen neuen Typ vergleichen und die Registry begründet vorziehen. - Analysieren: Singleton und Factory in bestehendem Code und in Frameworks wiedererkennen.
Passende Übungen
Abschnitt betitelt „Passende Übungen“- Aufgabe 10 - Jahresprojekt Sprint 3 - Singleton und Factory (gemeinsam mit Kapitel 15 in einer Woche): siehe Kapitel 15. Für dieses Kapitel: Factory mit Registry im Projekt oder begründeter Verzicht nach dem Grundsatz „erst das Problem, dann das Muster“ (Wesentlich_vollständig), Fehlerklasse und Diff-Nachweis für eine neue Variante (PlusPlus), Muster in freier Wildbahn (MandalorianPlus).
Autoren
Abschnitt betitelt „Autoren“GAM
CC-BY-SA