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.
Serialisiert Übergänge, wartet Guards ab und betritt den korrekten Zielzustand. Aktionen können externe Effekte haben. Rekursiv ausgelöste Events innerhalb einer aktiven Aktion nicht abwarten; Folgeereignisse nach ihrem Ende einplanen.
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 / addTransition / transitionKonfiguriert und serialisiert Guard-geschützte Zustandsübergänge.subscribe / getCurrentState / isInEndStateBeobachtet mit explizitem Abmelden und liest den Zustand.05 · Selbst bauen
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 solltest du testen?
- 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.
Selbst ausprobieren
Formuliere die erste Prüfaussage als fehlschlagenden Test, implementiere die kleinste Korrektur und ergänze einen Grenzfall.
Unbekannte Ereignisse lassen den Zustand unverändert.
Ansatz vergleichen
Dein Test sollte beim gezielten Verletzen der Invariante fehlschlagen und nach der Korrektur wieder bestehen. Halte den Aufbau deterministisch und prüfe ein beobachtbares Ergebnis.
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.
Aktionen können externe Effekte haben. Rekursiv ausgelöste Events innerhalb einer aktiven Aktion nicht abwarten; Folgeereignisse nach ihrem Ende einplanen.
08 · Weiterdenken
Quellcode und verwandte Ideen
yarn tsx packages/state-machine/examples/basic.ts