18. Entwurfsmuster III – Struktur und Architektur
Nach diesem Kapitel kannst du:
- Adapter und Decorator mit ihrem Problem, Diagramm und Nutzen erklären.
- beide Muster in TypeScript implementieren und Decorator-Schichten zur Laufzeit zusammenstecken.
- MVC und die Idee der Schichtenarchitektur als Architekturmuster erklären und die Trennung von Darstellung und Fachlogik begründen.
- die behandelten Muster über eine Landkarte vom Problem aus zuordnen.
- übertriebenen Mustereinsatz (Patternitis) erkennen und dich begründet gegen ein Muster entscheiden.
Das Fußballteam aus den Kapiteln 11 bis 17 bekommt Verstärkung, und damit kommen neue Fragen: Wie wird etwas zusammengesetzt? Der neue Spieler aus dem Ausland spricht die Sprache der Mannschaft nicht, also übersetzt ein Dolmetscher die Anweisungen des Trainers. Die Kapitänsbinde macht aus einem Spieler keinen anderen, sie legt eine zusätzliche Aufgabe um ihn herum; die Rolle als Elfmeterschütze kommt bei Bedarf als weitere Schicht dazu. Und im Stadion sind Spielstand, Anzeigetafel und Schiedsrichter getrennte Zuständigkeiten. Wer schließlich für einen einfachen Einwurf die eingeübte “Eckball-Variante 2” ausruft, hat ein Muster ohne Problem eingesetzt.
flowchart LR accTitle: Dolmetscher, Kapitänsbinde und Stadion als Bilder für Adapter, Decorator und MVC D["Dolmetscher übersetzt für den neuen Spieler"] -.->|im Code| A["Adapter"] K["Kapitänsbinde als zusätzliche Schicht"] -.->|im Code| DE["Decorator"] S["Spielstand, Anzeigetafel, Schiedsrichter"] -.->|im Code| M["MVC"]
Strukturmuster beschäftigen sich damit, wie Objekte zusammengesetzt werden - wie man fremde Bausteine passend macht, wie man Verhalten schichtweise ergänzt, wie man ein ganzes System in Zuständigkeiten gliedert. Dieses Kapitel behandelt zwei Muster im Kleinen (Adapter, Decorator) und ein Muster im Großen (MVC), und es schließt mit der wichtigsten Fähigkeit im Umgang mit Mustern: sich dagegen zu entscheiden.
Begriffe vorab
Abschnitt betitelt „Begriffe vorab“- Strukturmuster (structural pattern): Muster, das regelt, wie Klassen und Objekte zu größeren Einheiten zusammengesetzt werden.
- Adapter: Objekt, das eine fremde Schnittstelle in die Schnittstelle übersetzt, die der eigene Code erwartet.
- Decorator (auch Wrapper): Objekt, das ein anderes mit derselben Schnittstelle umhüllt und Verhalten davor oder danach ergänzt.
- Architekturmuster: Muster für den Aufbau einer ganzen Anwendung statt einzelner Klassen.
- MVC (Model-View-Controller): Architekturmuster, das Daten und Regeln, Darstellung und Eingabeverarbeitung trennt.
- Schichtenarchitektur: Aufteilung in Präsentation, Fachlogik und Datenzugriff, wobei jede Schicht nur von der darunterliegenden abhängt.
- Patternitis: übertriebener Mustereinsatz, bei dem Muster ohne passendes Problem eingebaut werden.
Adapter
Abschnitt betitelt „Adapter“Problem: Du brauchst eine Funktion, und es gibt eine Bibliothek dafür - aber deren Schnittstelle passt nicht zu der, die dein Code erwartet. Deine Anwendung erwartet überall einen MediaStore mit save(name, bytes), die eingekaufte Cloud-Bibliothek bietet aber putObject({ key, sizeBytes }). Beide tun dasselbe, sprechen aber verschiedene Sprachen. Im Fußballbild ist das der neue Spieler, der die Anweisungen des Trainers nicht versteht.
Lösung: Ein Adapter sitzt dazwischen und übersetzt, wie der Dolmetscher. Er implementiert die Schnittstelle, die dein Code erwartet, und leitet die Aufrufe passend an die fremde Bibliothek weiter.
classDiagram
class MediaStore {
<<interface>>
+save(name, bytes) void
+list() string[]
}
class CloudBucketAdapter
class CloudBucketSdk {
+putObject(request) void
+listKeys() string[]
}
MediaStore <|.. CloudBucketAdapter
CloudBucketAdapter --> CloudBucketSdk : übersetzt Aufrufe
// what our application expects everywhereinterface MediaStore { save(name: string, bytes: number): void; list(): string[];}
// the foreign library, with an interface we cannot changeclass CloudBucketSdk { putObject(request: { key: string; sizeBytes: number }): void { /* ... */ } listKeys(): string[] { /* ... */ return []; }}
// the adapter makes the foreign interface fit oursclass CloudBucketAdapter implements MediaStore { constructor(private sdk: CloudBucketSdk) {}
save(name: string, bytes: number): void { this.sdk.putObject({ key: name, sizeBytes: bytes }); // translate }
list(): string[] { return this.sdk.listKeys(); }}Beispielprojekt: 18_entwurfsmuster_struktur_examples/ex01_adapter_media_store
Der Nutzen ist doppelt. Erstens bleibt dein Code von der fremden Bibliothek unberührt - er kennt nur MediaStore. Zweitens ist die Bibliothek austauschbar: Ein Wechsel des Anbieters bedeutet einen neuen Adapter, sonst nichts. Das ist dasselbe Prinzip wie ein Repository-Interface hinter verschiedenen Datenquellen - der Adapter ist die allgemeine Form davon.
Genauer erklärt: Wechselt der Anbieter von CloudBucketSdk zu einem Konkurrenzprodukt, ändert sich genau eine Datei - CloudBucketAdapter bzw. eine neue Adapter-Klasse daneben. Jede Stelle im Rest der Anwendung, die MediaStore verwendet, bleibt unangetastet, weil sie die konkrete SDK-Klasse nie gesehen hat. Ohne Adapter stünde putObject({ key, sizeBytes }) an jeder Speicherstelle, und der Anbieterwechsel würde zur Suche durch den ganzen Code.
Decorator
Abschnitt betitelt „Decorator“Problem: Du willst ein Objekt um Verhalten ergänzen, und zwar in frei kombinierbaren Schichten. Eine Benachrichtigung soll gespeichert werden; manchmal zusätzlich per E-Mail, manchmal zusätzlich per Chat, manchmal beides. Löst man das über Vererbung, braucht man für jede Kombination eine Unterklasse (EmailNotifier, SlackNotifier, EmailAndSlackNotifier, …) - die Zahl explodiert.
Lösung: Jede Ergänzung wird ein Objekt, das ein anderes desselben Interfaces umhüllt und sein Verhalten vor oder nach dem Weiterreichen erweitert. Man stapelt Hüllen, statt Unterklassen zu bilden, so wie die Kapitänsbinde um denselben Spieler gelegt wird, statt einen neuen Spieler “Kapitän” aufzustellen.
classDiagram
class Notifier {
<<interface>>
+send(message) string[]
}
class BaseNotifier
class EmailNotifier
class SlackNotifier
Notifier <|.. BaseNotifier
Notifier <|.. EmailNotifier
Notifier <|.. SlackNotifier
EmailNotifier o--> Notifier : umhüllt
SlackNotifier o--> Notifier : umhüllt
interface Notifier { send(message: string): string[];}
class BaseNotifier implements Notifier { send(message: string): string[] { return [`stored: ${message}`]; }}
class EmailNotifier implements Notifier { constructor(private wrapped: Notifier) {} send(message: string): string[] { return [...this.wrapped.send(message), `email: ${message}`]; // add one layer }}
class SlackNotifier implements Notifier { constructor(private wrapped: Notifier) {} send(message: string): string[] { return [...this.wrapped.send(message), `slack: ${message}`]; }}Die Schichten werden zur Laufzeit zusammengesteckt, in beliebiger Zahl und Reihenfolge:
let notifier: Notifier = new BaseNotifier();notifier = new EmailNotifier(notifier);notifier = new SlackNotifier(notifier);
notifier.send("render done");// ["stored: render done", "email: render done", "slack: render done"]Beispielprojekt: 18_entwurfsmuster_struktur_examples/ex02_decorator_notifier
Statt einer Unterklasse pro Kombination gibt es einen Baustein pro Ergänzung; die Kombinationen entstehen beim Zusammenstecken.
Genauer erklärt: Jede Schicht ruft zuerst die umhüllte Schicht auf und hängt ihren eigenen Eintrag danach an. Deshalb bestimmt die Reihenfolge des Zusammensteckens die Reihenfolge im Ergebnis: Wird zuerst SlackNotifier und dann EmailNotifier herumgelegt, steht slack: ... vor email: .... Mit zwei Ergänzungen sind das vier Varianten (keine, nur E-Mail, nur Chat, beide) aus zwei Klassen statt aus vier Unterklassen; jede weitere Ergänzung verdoppelt die Zahl der Kombinationen, aber nicht die Zahl der Klassen.
flowchart LR accTitle: Ein Aufruf läuft durch die Hüllen nach innen, jede Schicht ergänzt ihren Eintrag auf dem Rückweg C["Aufrufer: send"] --> S["SlackNotifier"] S --> E["EmailNotifier"] E --> B["BaseNotifier: stored"] B -.->|"+ email"| E E -.->|"+ slack"| S
Auflösung
(a) Adapter: Eine fremde Schnittstelle wird in die erwartete übersetzt, inklusive Umrechnung der Einheit. (b) Decorator: Die Schnittstelle bleibt gleich, Messen und Protokollieren sind zwei Schichten, die sich frei kombinieren lassen.
Architekturmuster: MVC und Schichten
Abschnitt betitelt „Architekturmuster: MVC und Schichten“Muster gibt es nicht nur für einzelne Klassen, sondern für den Aufbau ganzer Anwendungen. Das bekannteste ist MVC (Model-View-Controller). Es trennt drei Zuständigkeiten, so wie im Stadion der Spielstand, die Anzeigetafel und der Schiedsrichter, der ein Tor gibt, getrennte Aufgaben haben:
| Teil | Zuständigkeit | Kennt |
|---|---|---|
| Model | Daten und fachliche Regeln | nichts von der Darstellung |
| View | Darstellung für den Benutzer | das Model (liest es), nicht die Regeln |
| Controller | Eingaben entgegennehmen, Model und View koordinieren | beide |
flowchart LR accTitle: MVC-Zyklus von der Eingabe über Controller, Model und View zurück zum Benutzer U[Benutzer] -->|Eingabe| C[Controller] C -->|ändert| M[Model] M -->|Daten| V[View] V -->|Anzeige| U
Der Kern von MVC ist derselbe wie bei einer Schichtenarchitektur: Die Darstellung ist von der Fachlogik getrennt. Das Model weiß nichts davon, wie es angezeigt wird; deshalb lässt sich dieselbe Fachlogik hinter verschiedenen Oberflächen betreiben und unabhängig von der Anzeige testen.
Genauer erklärt: Ein Test für die Bestellregel (“Menge darf nicht negativ sein”) ruft nur das Model auf - kein Browser, kein Rendering, keine Klicksimulation. Genau das macht diese Tests zu den schnellen, günstigen Unit-Tests von der Unit-Ebene aus Kapitel 19, während ein Test, der auch durch die Oberfläche klicken muss, automatisch auf der teureren E2E-Ebene landet. MVC hat viele Verwandte (MVVM und andere), die sich in der Aufgabenverteilung zwischen den Teilen unterscheiden; die gemeinsame Idee bleibt die Trennung von Daten, Regeln und Darstellung.
Eine Schichtenarchitektur - Präsentation über Fachlogik über Datenzugriff, mit einbahniger Abhängigkeit nach unten - ist damit selbst ein Architekturmuster. Wer eine solche Trennung sauber umsetzt, hat ein Muster im Großen angewendet, auch ohne es so zu nennen.
flowchart TD accTitle: Schichtenarchitektur mit Abhängigkeit nur nach unten P["Präsentation"] --> F["Fachlogik"] F --> D["Datenzugriff"]
Die Muster-Landkarte
Abschnitt betitelt „Die Muster-Landkarte“Damit ist die Übersicht über die behandelten Muster vollständig. Diese Landkarte ist die Entscheidungshilfe fürs Fachgespräch: Vom Problem aus zum Muster, nicht umgekehrt.
| Kategorie | Muster | Löst das Problem … |
|---|---|---|
| Erzeugung | Singleton | genau eine Instanz, überall erreichbar |
| Erzeugung | Factory | Objekt passend zur Eingabe erzeugen, ohne die Klasse zu nennen |
| Verhalten | Observer | mehrere reagieren auf ein Ereignis, ohne dass der Auslöser sie kennt |
| Verhalten | Strategy | austauschbares Verfahren zur Laufzeit wählen |
| Verhalten | Command | eine Aktion als Objekt (rückgängig, Warteschlange, Protokoll) |
| Struktur | Adapter | fremde Schnittstelle passend machen |
| Struktur | Decorator | Verhalten in kombinierbaren Schichten ergänzen |
| Architektur | MVC / Schichten | Darstellung, Fachlogik und Daten trennen |
flowchart LR accTitle: Muster-Landkarte mit vier Kategorien und acht Mustern L["Muster-Landkarte"] --> E["Erzeugung"] L --> V["Verhalten"] L --> S["Struktur"] L --> A["Architektur"] E --> E1["Singleton"] E --> E2["Factory"] V --> V1["Observer"] V --> V2["Strategy"] V --> V3["Command"] S --> S1["Adapter"] S --> S2["Decorator"] A --> A1["MVC / Schichten"]
Auflösung
(a) Factory (Erzeugung). (b) MVC bzw. Schichten (Architektur): Die Darstellung ist von der Fachlogik getrennt. (c) Adapter (Struktur).
Patternitis: die Kunst, Nein zu sagen
Abschnitt betitelt „Patternitis: die Kunst, Nein zu sagen“Ein Muster ist die Antwort auf ein Problem. Wer Muster einsetzt, um Muster einzusetzen, erzeugt das Gegenteil von guter Struktur: unnötige Klassen, Indirektion ohne Nutzen, Code, den man durch fünf Dateien verfolgen muss, um zu verstehen, dass am Ende ein String zurückgegeben wird. Dieser Überschuss hat einen Namen: Patternitis. Im Spiel ist das die “Eckball-Variante 2”, ausgerufen für einen einfachen Einwurf.
// Patternitis: a factory that produces a strategy for a decision with one optionconst greeterFactory = new AbstractGreeterFactoryProvider();const greeter = greeterFactory.createFactory().getStrategy("default").build();greeter.execute("hello");
// the honest versionconsole.log("hello");Beispielprojekt: 18_entwurfsmuster_struktur_examples/ex04_patternitis_no_go
Die Gegenmittel sind schlicht:
- Problem zuerst. Erst das konkrete Problem benennen, dann prüfen, ob ein bekanntes Muster genau dieses Problem löst. Nie mit dem Muster beginnen.
- Der einfachste Weg zählt. Wenn eine Funktion genügt, ist eine Klassenhierarchie falsch. Ein Muster muss sich seinen Aufwand verdienen.
- Ein Muster ist Kommunikation. Sein Name spart Erklärung nur, wenn er tatsächlich passt. Ein falsch benanntes Muster verwirrt mehr, als es hilft.
flowchart LR
accTitle: Ein Muster wird nur eingesetzt, wenn es ein vorhandenes Problem löst und seinen Aufwand verdient
P["Problem benennen"] --> Q{"Löst ein Muster genau dieses Problem?"}
Q -->|nein| E["einfacher Code"]
Q -->|ja| K{"Verdient es seinen Aufwand?"}
K -->|nein| E
K -->|ja| M["Muster einsetzen und benennen"]
Auflösung
Das Problem “mehrere Sprachen” gibt es noch nicht, also gibt es auch nichts, was das Muster lösen könnte. Eine Funktion genügt. Kommt die zweite Sprache wirklich, ist der Umbau auf eine Strategy klein, und dann hat das Muster ein Problem, das es löst.
Zusatzmaterialien
Abschnitt betitelt „Zusatzmaterialien“- Gamma, Helm, Johnson, Vlissides, Design Patterns (GoF, 1994) - Adapter und Decorator im Original, Kapitel “Structural Patterns”; lohnt sich, um die ursprüngliche Absicht beider Muster einmal im Wortlaut zu lesen.
- refactoring.guru: Structural Patterns - Übersichtsseite mit allen Strukturmustern, auch den hier nicht behandelten (z. B. Facade, Proxy, Composite).
- Martin Fowler, Patterns of Enterprise Application Architecture - MVC, Schichtenarchitektur und verwandte Architekturmuster im Detail; die Quelle, wenn du für die Diplomarbeit die Architektur begründen musst.
- Wikipedia: Model View Controller - Geschichte und Varianten von MVC im Überblick, nützlich als Einstieg vor Fowlers Buch.
- Die “Weiterlesen”-Hinweise bei Adapter und Decorator oben führen direkt zu den passenden Detailseiten.
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“- Erklären: Adapter und Decorator mit ihrem Problem, Diagramm und Nutzen erklären.
- Anwenden: beide Muster in TypeScript implementieren und Decorator-Schichten zur Laufzeit zusammenstecken.
- Erklären: MVC und die Idee der Schichtenarchitektur als Architekturmuster erklären und die Trennung von Darstellung und Fachlogik begründen.
- Analysieren: die behandelten Muster über eine Landkarte vom Problem aus zuordnen.
- Beurteilen: übertriebenen Mustereinsatz (Patternitis) erkennen und dich begründet gegen ein Muster entscheiden.
Passende Übungen
Abschnitt betitelt „Passende Übungen“- Aufgabe 12 - Jahresprojekt Sprint 3 - Review, Schichten und Adapter: Kata: Patternitis-Review mit Gegenentwurf, dazu Review und Retrospektive von Sprint 3 (Wesentlich_überwiegend); Release und Schichtenprüfung des Projekts mit Behebung des schwersten Verstoßes (Wesentlich_vollständig). PlusPlus: fremde Bibliothek hinter einem Adapter kapseln; MandalorianPlus: Decorator und Muster-Landkarte des Projekts.
Autoren
Abschnitt betitelt „Autoren“GAM
CC-BY-SA