功能说明
该示例用泛型约束状态名与事件名,构建一个小型有限状态机。每条转移声明起点、事件、终点与可选守卫,守卫可以读取上下文并决定是否允许转移。can 用于预判,send 执行转移并把新状态压入历史;非法事件或守卫拒绝都会抛出带上下文的错误。适合订单状态、上传流程、连接生命周期等需要严格状态约束的逻辑。
关键代码
构造器校验转移数组、起点、事件与终点非空,并确认守卫是函数;转移被复制到内部数组,避免外部修改。can 与 send 都按当前状态和事件查找第一条匹配转移,保证两者结论一致。守卫接收上下文与事件,可在运行时依赖余额、权限等条件。转移成功后先更新状态,再追加历史并调用回调,回调抛错不会回滚状态。
使用注意
守卫应保持纯函数语义,若在 can 中产生副作用,随后 send 会再次调用它。历史只记录成功到达的状态,不包含初始状态,也没有回滚或撤销接口。同一状态与事件存在多条转移时只有第一条生效,守卫拒绝后不会继续尝试后续转移。类型只在编译期约束,运行时仍可能传入非法事件字符串。onTransition 抛错会阻止 send 返回,但状态已经改变。
验证方式
定义三态流程,测试 send 依次到达终点并检查 previousStates;用守卫让某条转移在上下文不满足时拒绝,断言 can 返回 false 且 send 抛错;发送未声明事件断言错误信息包含当前状态与事件;测试自转移会被记录;构造缺少字段或守卫非函数的转移断言 TypeError;确认修改外部转移数组不影响实例。
代码示例
export interface Transition<State extends string, Event extends string, Context> {
from: State;
event: Event;
to: State;
guard?: (context: Context, event: Event) => boolean;
}
export interface StateMachineOptions<State extends string, Event extends string, Context> {
context: Context;
onTransition?: (next: State, previous: State, event: Event) => void;
}
export class StateMachine<State extends string, Event extends string, Context = undefined> {
private current: State;
private readonly transitions: ReadonlyArray<Transition<State, Event, Context>>;
private readonly context: Context;
private readonly onTransition?: (next: State, previous: State, event: Event) => void;
private readonly history: State[] = [];
constructor(
initial: State,
transitions: ReadonlyArray<Transition<State, Event, Context>>,
options: StateMachineOptions<State, Event, Context>,
) {
if (!Array.isArray(transitions)) throw new TypeError("transitions must be an array");
for (const transition of transitions) {
if (!transition.from || !transition.event || !transition.to) {
throw new TypeError("transition requires from, event and to");
}
if (transition.guard !== undefined && typeof transition.guard !== "function") {
throw new TypeError("guard must be a function");
}
}
this.current = initial;
this.transitions = [...transitions];
this.context = options.context;
this.onTransition = options.onTransition;
}
get state(): State {
return this.current;
}
get previousStates(): readonly State[] {
return [...this.history];
}
can(event: Event): boolean {
return this.transitions.some((transition) =>
transition.from === this.current &&
transition.event === event &&
(!transition.guard || transition.guard(this.context, event)));
}
send(event: Event): State {
const transition = this.transitions.find((candidate) =>
candidate.from === this.current && candidate.event === event);
if (!transition) {
throw new Error("no transition from " + this.current + " on " + event);
}
if (transition.guard && !transition.guard(this.context, event)) {
throw new Error("guard rejected " + this.current + " -> " + transition.to + " on " + event);
}
const previous = this.current;
this.current = transition.to;
this.history.push(this.current);
this.onTransition?.(this.current, previous, event);
return this.current;
}
}