Zum Inhalt springen

4. Entwurfsmuster II - Verhalten

Zu Zen-Modus wechseln

Verhaltensmuster ordnen die Zusammenarbeit von Objekten - wer wen benachrichtigt, wer welches Verhalten austauscht, wie eine Aktion zu einem Ding wird, das man aufheben und rückgängig machen kann. Alle drei Muster dieses Kapitels lösen ein wiederkehrendes Kopplungsproblem: Sie trennen, was passiert, von wer es auslöst oder wann es passiert.

Jedes Muster folgt hier demselben Aufbau: das Problem, die Lösung als Diagramm, eine lauffähige TypeScript-Implementierung und der Ort, an dem Ihnen das Muster ohnehin schon begegnet ist.

Problem: Ein Objekt ändert seinen Zustand, und mehrere andere müssen darauf reagieren - aber das erste soll nicht wissen müssen, wer alles zuhört. Ein Upload ist fertig; daraufhin sollen ein Vorschaubild erzeugt, ein Log-Eintrag geschrieben und die Oberfläche aktualisiert werden. Würde die Upload-Funktion diese drei direkt aufrufen, kennt sie sie alle - und muss geändert werden, sobald ein vierter Interessent dazukommt.

Lösung: Das beobachtete Objekt (Subject, Publisher) führt eine Liste von Interessenten (Observer, Subscriber) und benachrichtigt sie, ohne sie einzeln zu kennen. Interessenten melden sich an und wieder ab.

classDiagram
  class EventEmitter~T~ {
    -listeners: Listener[]
    +subscribe(listener) unsubscribe
    +emit(event) void
  }
  class Subscriber {
    <<function>>
    +(event) void
  }
  EventEmitter~T~ o--> Subscriber : benachrichtigt

Eine typsichere, allgemein verwendbare Implementierung:

type Listener<T> = (event: T) => void;
class EventEmitter<T> {
private listeners: Listener<T>[] = [];
subscribe(listener: Listener<T>): () => void {
this.listeners.push(listener);
return () => { // unsubscribe function
this.listeners = this.listeners.filter((l) => l !== listener);
};
}
emit(event: T): void {
for (const listener of [...this.listeners]) listener(event);
}
}

Zwei Details sind bewusst gesetzt. subscribe gibt eine Abmeldefunktion zurück - das ist bequemer und weniger fehlerträchtig, als sich den Listener zum späteren Entfernen zu merken. Und emit iteriert über eine Kopie der Liste ([...this.listeners]), damit ein Listener, der sich während der Benachrichtigung abmeldet, die laufende Schleife nicht durcheinanderbringt.

Im Gebrauch:

interface UploadDone { filename: string; sizeMb: number; }
const uploads = new EventEmitter<UploadDone>();
const stopLogging = uploads.subscribe((e) => console.log(`log: ${e.filename}`));
uploads.subscribe((e) => console.log(`thumbnail for: ${e.filename}`));
uploads.emit({ filename: "intro.mp4", sizeMb: 12 }); // both react
stopLogging();
uploads.emit({ filename: "outro.mp4", sizeMb: 8 }); // only the thumbnail reacts

Problem: Für dieselbe Aufgabe gibt es mehrere austauschbare Verfahren, und die Wahl soll zur Laufzeit möglich sein. Eine Medienliste lässt sich nach Name, Größe oder Datum sortieren; ein Preis wird je nach Kundengruppe anders berechnet. Das mit einem switch im Kern zu lösen bläht ihn auf und zwingt bei jedem neuen Verfahren zur Änderung.

Lösung: Jedes Verfahren wird ein eigenes Objekt hinter einem gemeinsamen Interface. Das nutzende Objekt hält eine Strategie und ruft sie auf, ohne ihr Innenleben zu kennen; die Strategie ist jederzeit austauschbar.

classDiagram
  class SortStrategy {
    <<interface>>
    +sort(files) MediaFile[]
  }
  class ByName
  class BySize
  class MediaGallery {
    -strategy: SortStrategy
    +setStrategy(s) void
    +ordered() string[]
  }
  SortStrategy <|.. ByName
  SortStrategy <|.. BySize
  MediaGallery o--> SortStrategy : verwendet
interface MediaFile { name: string; sizeMb: number; createdAt: string; }
interface SortStrategy {
readonly name: string;
sort(files: MediaFile[]): MediaFile[];
}
const byName: SortStrategy = {
name: "name",
sort: (files) => [...files].sort((a, b) => a.name.localeCompare(b.name)),
};
const bySize: SortStrategy = {
name: "size",
sort: (files) => [...files].sort((a, b) => b.sizeMb - a.sizeMb),
};
class MediaGallery {
constructor(private files: MediaFile[], private strategy: SortStrategy) {}
setStrategy(strategy: SortStrategy): void {
this.strategy = strategy; // swap behavior at runtime
}
ordered(): string[] {
return this.strategy.sort(this.files).map((file) => file.name);
}
}
const gallery = new MediaGallery(files, byName);
gallery.ordered(); // sorted by name
gallery.setStrategy(bySize);
gallery.ordered(); // now sorted by size - no change to MediaGallery

Eine neue Sortierung (etwa nach Datum) ist ein neues Objekt, das dem Interface genügt - kein Eingriff in MediaGallery. Das ist derselbe Open-Closed-Gewinn wie bei der Factory, hier auf Verhalten statt auf Erzeugung angewendet.

Problem: Eine Aktion soll nicht sofort und direkt ausgeführt, sondern als Objekt behandelt werden - um sie aufzuheben, in eine Warteschlange zu legen, zu protokollieren oder rückgängig zu machen. Das klassische Beispiel ist Undo/Redo: Dafür muss jede Aktion wissen, wie sie sich selbst umkehrt.

Lösung: Jede Aktion wird ein Objekt mit execute() und undo(). Eine Historie führt die ausgeführten Kommandos und kann sie in umgekehrter Reihenfolge zurücknehmen.

classDiagram
  class Command {
    <<interface>>
    +execute() void
    +undo() void
  }
  class AppendCommand
  class CommandHistory {
    -done: Command[]
    +run(command) void
    +undo() void
  }
  Command <|.. AppendCommand
  CommandHistory o--> Command : verwaltet
interface Command {
execute(): void;
undo(): void;
}
class TextDocument {
private text = "";
append(part: string): void { this.text += part; }
removeLast(length: number): void { this.text = this.text.slice(0, -length); }
get content(): string { return this.text; }
}
class AppendCommand implements Command {
constructor(private doc: TextDocument, private part: string) {}
execute(): void { this.doc.append(this.part); }
undo(): void { this.doc.removeLast(this.part.length); } // knows how to reverse itself
}
class CommandHistory {
private done: Command[] = [];
run(command: Command): void {
command.execute();
this.done.push(command);
}
undo(): void {
const command = this.done.pop();
if (command) command.undo();
}
}
const doc = new TextDocument();
const history = new CommandHistory();
history.run(new AppendCommand(doc, "Hello"));
history.run(new AppendCommand(doc, ", world"));
doc.content; // "Hello, world"
history.undo();
doc.content; // "Hello"

Der Kern ist, dass jedes Kommando sein eigenes Rückgängigmachen kennt. Die Historie muss über die Aktionen nichts wissen; sie ruft nur undo() in umgekehrter Reihenfolge auf. Redo ergänzt man, indem zurückgenommene Kommandos auf einem zweiten Stapel landen.

Die drei Muster lassen sich an ihrer Kernfrage auseinanderhalten - genau diese Zuordnung braucht das Fachgespräch:

MusterKernfrageErkennungszeichen
ObserverWie reagieren mehrere auf ein Ereignis, ohne dass der Auslöser sie kennt?anmelden / benachrichtigen, “wenn X passiert, dann …”
StrategyWie tausche ich ein Verfahren gegen ein gleichwertiges aus?mehrere Wege zum selben Ziel, zur Laufzeit wählbar
CommandWie mache ich aus einer Aktion ein Objekt?rückgängig, Warteschlange, Protokoll, später ausführen

Mindestens ein Verhaltensmuster soll im Jahresprojekt bewusst eingesetzt und dokumentiert werden - nicht, weil ein Muster Pflicht ist, sondern weil die Begründung die Kompetenz zeigt. Die Dokumentation dazu ist kurz und beantwortet drei Fragen: Welches Problem lag vor? Warum dieses Muster? Was wäre die Alternative gewesen? Ein Satz je Frage genügt; entscheidend ist, dass die Wahl eine Entscheidung war und kein Zufall.

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: Observer, Strategy und Command mit ihrem jeweiligen Problem und einem Diagramm erklären.
  • Anwenden: die drei Muster in TypeScript implementieren, inklusive An-/Abmeldung beim Observer und undo beim Command.
  • Analysieren: die drei Muster an ihrer Kernfrage unterscheiden und in fremdem Code wiedererkennen.
  • Beurteilen: für ein gegebenes Verhaltensproblem das passende Muster auswählen und die Wahl begründen.
  • Anwenden: ein Verhaltensmuster im eigenen Projekt bewusst einsetzen und die Entscheidung dokumentieren.
  • Aufgabe 06 - Observer selbst gebaut
  • Aufgabe 07 - Strategie und Kommando