Zum Inhalt springen

18. Entwurfsmuster III – Struktur und Architektur

Zu Zen-Modus wechseln

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.

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

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 everywhere
interface MediaStore {
save(name: string, bytes: number): void;
list(): string[];
}
// the foreign library, with an interface we cannot change
class CloudBucketSdk {
putObject(request: { key: string; sizeBytes: number }): void { /* ... */ }
listKeys(): string[] { /* ... */ return []; }
}
// the adapter makes the foreign interface fit ours
class 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.

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.

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:

TeilZuständigkeitKennt
ModelDaten und fachliche Regelnnichts von der Darstellung
ViewDarstellung für den Benutzerdas Model (liest es), nicht die Regeln
ControllerEingaben entgegennehmen, Model und View koordinierenbeide
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"]

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.

KategorieMusterLöst das Problem …
ErzeugungSingletongenau eine Instanz, überall erreichbar
ErzeugungFactoryObjekt passend zur Eingabe erzeugen, ohne die Klasse zu nennen
VerhaltenObservermehrere reagieren auf ein Ereignis, ohne dass der Auslöser sie kennt
VerhaltenStrategyaustauschbares Verfahren zur Laufzeit wählen
VerhaltenCommandeine Aktion als Objekt (rückgängig, Warteschlange, Protokoll)
StrukturAdapterfremde Schnittstelle passend machen
StrukturDecoratorVerhalten in kombinierbaren Schichten ergänzen
ArchitekturMVC / SchichtenDarstellung, 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).

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 option
const greeterFactory = new AbstractGreeterFactoryProvider();
const greeter = greeterFactory.createFactory().getStrategy("default").build();
greeter.execute("hello");
// the honest version
console.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.

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

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

GAM

CC-BY-SA

https://qiz.at/rules