01 · Problem
Wofür braucht man das?
Wenn Statuswechsel über viele if-Zweige verteilt sind, entstehen unmögliche Zustände, versteckte Seiteneffekte und schwer reproduzierbare Fehler.
02 · Denkmodell
Das mentale Modell
Eine endliche Zustandsmaschine liest das Paar aus aktuellem Zustand und Ereignis, prüft optional einen Guard und wechselt genau einmal zum Zielzustand.
StateMachine, Konfigurationen und Transitionstypen sind exportiert. Asynchrone Guards werden jedoch nicht korrekt abgewartet, Entry-Aktionen greifen teils auf den alten Zustand zu und Aktionsrückgaben werden als Folgeereignis interpretiert. Eine Ereigniswarteschlange und sauberes Abmelden fehlen.
03 · Kontrollfluss
Was passiert in welcher Reihenfolge?
- Aktuellen Zustand und Ereignis gegen die Transitionstabelle auflösen.
- Optionalen Guard vollständig abwarten und bei false ohne Änderung zurückkehren.
- Exit-Aktion ausführen, Zielzustand atomisch setzen und Entry-Aktion starten.
- Beobachter nach erfolgreichem Wechsel mit Vorher/Nachher-Daten benachrichtigen.
- Gleichzeitige Ereignisse über eine Queue serialisieren.
04 · Bauteile
Die entscheidenden Verträge
StateMachine
Verwaltet Zustand, Transitionen und State-Change-Observer.
addTransition
Registriert Ereignis, Ziel, Guard und optionale Aktion.
transition
Versucht einen Zustandswechsel für ein Ereignis.
subscribe
Beobachtet erfolgreiche Zustandswechsel.
05 · Build it yourself
Selbst implementieren
Für den Kern reichen eine typisierte Tabelle und eine einzige asynchrone send-Methode.
- Zustände und Ereignisse als String-Unions definieren.
- Transitionen je Zustand und Ereignis in einer Tabelle ablegen.
- Guard abwarten und erst danach den Zustand in einem Schritt aktualisieren.
type Transition<S, P> = {
target: S;
guard?: (payload: P) => boolean | Promise<boolean>;
};
class Machine<S extends string, E extends string, P> {
constructor(
private state: S,
private table: Record<S, Partial<Record<E, Transition<S, P>>>>,
) {}
async send(event: E, payload: P) {
const next = this.table[this.state][event];
if (!next || (next.guard && !(await next.guard(payload)))) return this.state;
this.state = next.target;
return this.state;
}
}06 · Verifizieren
Was du testen solltest
- Unbekannte Ereignisse lassen den Zustand unverändert.
- Synchrone und asynchrone Guards blockieren oder erlauben dieselbe Transition korrekt.
- Parallel gesendete Ereignisse werden in definierter Reihenfolge verarbeitet.
07 · Grenzen
Kompromisse und Stolperfallen
- Explizite Tabellen machen Verhalten sichtbar, werden bei vielen orthogonalen Zuständen aber groß.
- Asynchrone Aktionen sind praktisch, brauchen jedoch eine klare Queue- und Fehlerstrategie.
- Historie und Persistenz gehören als Adapter um den kleinen Kern, nicht in jede Transition.
Entry- und Exit-Aktionen sollten keine weiteren Ereignisse rekursiv auslösen; lege Folgeereignisse in eine Queue.
08 · Weiterdenken