Zum Inhalt springen
Design Patterns Fortgeschritten 25 min

Observer Pattern

Observer ist das Herzstueck moderner UI-Frameworks und Event-Systeme. Lerne das Muster und erkenne es in React, RxJS, Pub/Sub und mehr.

Aktualisiert:
Inhaltsverzeichnis

Observer Pattern

Observer ist wohl das wichtigste Behavioral Pattern - es liegt hinter fast allem, was mit Events, State Management und UI-Reaktivitaet zu tun hat.

Das Problem

Mehrere Objekte sollen benachrichtigt werden, wenn ein Zustand sich aendert. Du willst sie nicht fest verdrahten - neue Empfaenger sollen einfach hinzukommen.

Die Grundstruktur

      Subject (Observable)
      - state
      - observers[]
      + attach(obs)
      + detach(obs)
      + notify()

      Observer (Listener)
      + update(state)

Das Subject haelt eine Liste von Observers. Wenn sich etwas aendert, ruft es bei jedem Observer update() auf.

Einfache Implementation

class EventBus:
    def __init__(self):
        self._listeners = []

    def subscribe(self, fn):
        self._listeners.append(fn)
        return lambda: self._listeners.remove(fn)  # Unsubscribe-Funktion

    def emit(self, data):
        for fn in self._listeners:
            fn(data)

bus = EventBus()

unsubscribe = bus.subscribe(lambda d: print(f"Listener 1: {d}"))
bus.subscribe(lambda d: print(f"Listener 2: {d}"))

bus.emit("Nutzer eingeloggt")
# Listener 1: Nutzer eingeloggt
# Listener 2: Nutzer eingeloggt

unsubscribe()    # Listener 1 entfernen

bus.emit("Neue Nachricht")
# Listener 2: Neue Nachricht

Der Trick mit der unsubscribe-Closure ist sehr Python/JS-idiomatisch.

TypeScript-Version mit generischen Types

class EventBus<T> {
  private listeners = new Set<(data: T) => void>();

  subscribe(fn: (data: T) => void): () => void {
    this.listeners.add(fn);
    return () => this.listeners.delete(fn);
  }

  emit(data: T): void {
    this.listeners.forEach(fn => fn(data));
  }
}

interface UserEvent {
  name: string;
  action: string;
}

const bus = new EventBus<UserEvent>();

bus.subscribe(e => console.log(`${e.name} hat ${e.action}`));
bus.emit({ name: "Anna", action: "sich eingeloggt" });

Mit mehreren Event-Typen

Oft hast du unterschiedliche Events (Login, Logout, Error, etc.):

type Events = {
  userLogin: { name: string };
  userLogout: { name: string };
  error: { message: string };
};

class TypedEventBus<T> {
  private listeners = new Map<keyof T, Set<(data: any) => void>>();

  on<K extends keyof T>(event: K, fn: (data: T[K]) => void): () => void {
    if (!this.listeners.has(event)) {
      this.listeners.set(event, new Set());
    }
    this.listeners.get(event)!.add(fn);
    return () => this.listeners.get(event)!.delete(fn);
  }

  emit<K extends keyof T>(event: K, data: T[K]): void {
    this.listeners.get(event)?.forEach(fn => fn(data));
  }
}

const bus = new TypedEventBus<Events>();

bus.on("userLogin", (e) => console.log(`${e.name} logged in`));
bus.emit("userLogin", { name: "Anna" });

Ueber TypeScriptโ€™s keyof T und T[K] bekommst du volle Typ-Sicherheit.

Wo begegnet dir Observer im echten Code?

DOM-Events

button.addEventListener("click", () => {
  console.log("Geklickt!");
});

Das ist Observer pur - der Button ist das Subject, der Callback der Observer.

React State

const [count, setCount] = useState(0);

useState ist intern Observer-basiert - wenn setCount gerufen wird, werden alle abhaengigen Komponenten neu gerendert.

Redux / Zustand

State-Management-Tools nutzen das Muster massiv:

import { create } from "zustand";

const useStore = create((set) => ({
  count: 0,
  increment: () => set((state) => ({ count: state.count + 1 })),
}));

// In einer Komponente:
const count = useStore((state) => state.count);

Wenn sich count im Store aendert, werden alle Komponenten, die darauf โ€œsubscribenโ€, neu gerendert.

Node EventEmitter

import EventEmitter from "node:events";

const emitter = new EventEmitter();

emitter.on("data", (msg) => console.log("Empfangen:", msg));
emitter.emit("data", "Hallo");

Node selbst ist Event-basiert - praktisch alles (Streams, HTTP, Readline) nutzt diesen Mechanismus.

RxJS - Observer auf Steroiden

RxJS bringt Observable Streams:

import { fromEvent, map, filter, debounceTime } from "rxjs";

const input = document.querySelector("input");

const events$ = fromEvent(input, "input").pipe(
  map((e: any) => e.target.value),
  filter(text => text.length >= 3),
  debounceTime(300)
);

events$.subscribe(text => console.log("Suche:", text));

Das ist Observer mit Transformations-Pipeline - machtvoll fuer komplexe Event-Streams.

Pub/Sub - der grosse Bruder

Wenn Sender und Empfaenger nicht direkt verbunden sein sollen, nutzt man Pub/Sub:

[Publisher] โ†’ [Broker] โ†’ [Subscriber1, Subscriber2, ...]

Der Broker (z.B. Redis Pub/Sub, Kafka, RabbitMQ) entkoppelt beide Seiten. Publisher und Subscriber kennen einander nicht.

Typisch fuer:

  • Microservices-Kommunikation
  • Realtime-Notifications (WebSockets)
  • Event-Driven Architectures

Varianten

Push vs. Pull

  • Push: Subject schickt neue Daten direkt (notify(data))
  • Pull: Subject ruft nur โ€œetwas hat sich geaendertโ€ - Observers holen sich die Daten

In der Praxis ist Push einfacher und haeufiger.

Synchron vs. Asynchron

Synchron (Default): emit laeuft die Listener sofort durch. Problem: Ein langsamer Listener blockiert alles.

Asynchron: Listener laufen in Task Queue / setTimeout:

emit(data) {
  for (const fn of this.listeners) {
    setTimeout(() => fn(data), 0);
  }
}

Oder via queueMicrotask, Promises. Wichtig bei vielen/langsamen Listenern.

Hot vs. Cold Observables

In RxJS-Terminologie:

  • Cold: jede Subscription startet einen eigenen Stream (z.B. HTTP-Request pro Subscriber)
  • Hot: ein gemeinsamer Stream (z.B. Mouse-Events - alle sehen die gleichen)

Memory Leaks vermeiden

Ein klassisches Observer-Problem: Subscriptions werden nicht abgemeldet.

class Component {
  constructor() {
    emitter.on("update", this.handleUpdate.bind(this));
  }
  // Wenn Component zerstoert wird - Listener bleibt!
}

Immer abmelden:

class Component {
  constructor() {
    const handler = this.handleUpdate.bind(this);
    emitter.on("update", handler);
    this.unsubscribe = () => emitter.off("update", handler);
  }

  destroy() {
    this.unsubscribe();
  }
}

In React: useEffect-Cleanup:

useEffect(() => {
  const sub = bus.subscribe(handler);
  return () => sub();  // Cleanup on unmount
}, []);

Ein praktisches Beispiel

Ein kleiner Warenkorb mit Observer-basierten Updates:

type CartEvent =
  | { type: "added"; produkt: string; preis: number }
  | { type: "removed"; produkt: string }
  | { type: "checkout"; gesamt: number };

class Warenkorb {
  private items: { produkt: string; preis: number }[] = [];
  private listeners = new Set<(e: CartEvent) => void>();

  subscribe(fn: (e: CartEvent) => void) {
    this.listeners.add(fn);
    return () => this.listeners.delete(fn);
  }

  private emit(e: CartEvent) {
    this.listeners.forEach(fn => fn(e));
  }

  add(produkt: string, preis: number) {
    this.items.push({ produkt, preis });
    this.emit({ type: "added", produkt, preis });
  }

  remove(produkt: string) {
    this.items = this.items.filter(i => i.produkt !== produkt);
    this.emit({ type: "removed", produkt });
  }

  checkout() {
    const gesamt = this.items.reduce((s, i) => s + i.preis, 0);
    this.emit({ type: "checkout", gesamt });
    this.items = [];
  }
}

const korb = new Warenkorb();

// UI-Listener: aktualisiert die Summe in der Navbar
korb.subscribe(e => {
  if (e.type === "added" || e.type === "removed") renderCartBadge();
});

// Analytics-Listener: Events ans Tracking
korb.subscribe(e => analytics.track(e.type, e));

// E-Mail-Listener: Versenden beim Checkout
korb.subscribe(e => {
  if (e.type === "checkout") sendConfirmationEmail(e.gesamt);
});

korb.add("Buch", 19.99);
korb.checkout();

Drei Subscriber reagieren auf die gleichen Events - ohne voneinander zu wissen.

Zusammenfassung

  • Observer = Subject benachrichtigt registrierte Listener bei Aenderung
  • Grundformen: on/emit, subscribe mit Unsubscribe-Funktion
  • Hinter DOM-Events, React-State, Redux, RxJS, EventEmitter
  • Pub/Sub ist Observer mit Broker dazwischen
  • Immer abonnieren abmelden, sonst Memory Leaks
  • Synchron vs. Asynchron je nach Use-Case

Im naechsten Kapitel: Strategy und Command - zwei elegante Behavioral Patterns.

Zurรผck zum Design Patterns Kurs