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.
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,subscribemit 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.