5. Entwurfsmuster III - Struktur und Architektur
Entwurfsmuster III: Struktur und Architektur
Abschnitt betitelt „Entwurfsmuster III: Struktur und Architektur“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.
Adapter
Abschnitt betitelt „Adapter“Problem: Sie brauchen eine Funktion, und es gibt eine Bibliothek dafür - aber deren Schnittstelle passt nicht zu der, die Ihr Code erwartet. Ihre 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.
Lösung: Ein Adapter sitzt dazwischen und übersetzt. Er implementiert die Schnittstelle, die Ihr 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 : uebersetzt 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(); }}Der Nutzen ist doppelt. Erstens bleibt Ihr 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.
Decorator
Abschnitt betitelt „Decorator“Problem: Sie wollen 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.
classDiagram
class Notifier {
<<interface>>
+send(message) string[]
}
class BaseNotifier
class EmailNotifier
class SlackNotifier
Notifier <|.. BaseNotifier
Notifier <|.. EmailNotifier
Notifier <|.. SlackNotifier
EmailNotifier o--> Notifier : umhuellt
SlackNotifier o--> Notifier : umhuellt
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"]Statt einer Unterklasse pro Kombination gibt es einen Baustein pro Ergänzung; die Kombinationen entstehen beim Zusammenstecken.
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:
| 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 U[Benutzer] -->|Eingabe| C[Controller] C -->|aendert| 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. 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.
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 |
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.
// 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");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.
Lernergebnisse: Was Sie nach diesem Kapitel können sollten
Abschnitt betitelt „Lernergebnisse: Was Sie nach diesem Kapitel können sollten“Nach Abschluss dieses Kapitels sollten Schülerinnen und Schüler in der Lage sein:
- 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 sich begründet gegen ein Muster entscheiden.
Passende Übungen
Abschnitt betitelt „Passende Übungen“- Aufgabe 08 - Muster-Landkarte