stateless 比手写 switch 更可靠,因其显式建模状态流转,非法跳转运行时立即抛异常而非静默失败;配置绑定允许触发、目标状态与动作,强制通过 fire() 控制迁移,并支持序列化与异步行为。

Stateless 为什么比手写 switch 状态判断更可靠
因为状态流转逻辑被显式建模,非法跳转在运行时直接抛异常,而不是静默失败或数据错乱。比如用户在 Shipped 状态下误点“取消订单”,stateMachine.Fire(Trigger.Cancel) 会立刻报 InvalidOperationException: Cannot fire trigger 'Cancel' in state 'Shipped',而不是让业务代码继续往下走、污染数据库。
手写 switch 或 if 链容易漏掉校验、状态更新和副作用分离不清晰;Stateless 把「允许哪些触发」「触发后进入什么状态」「触发时执行什么动作」三者绑定在一条配置里,避免逻辑散落。
- 所有状态迁移必须通过
Fire()显式发起,无法绕过控制流 - 支持为同一
Trigger在不同状态下绑定不同OnEntry/OnExit行为,比如OnEntryAsync发通知、扣库存、写日志 - 状态机实例可序列化为 JSON(需启用
AllowSerialization),适合与持久化层配合做状态快照
如何正确配置订单状态机的 Guard 条件和异步动作
Guard 不是装饰用的——它决定某次 Fire() 是否真正生效。比如“仅当支付成功且库存充足才允许发货”,必须写成 stateMachine.Configure(OrderState.Paid).PermitIf(Trigger.Ship, OrderState.Shipped, () => IsInventoryAvailable()),而不是在 OnEntry 里 throw。
异步动作要用 OnEntryAsync 和 PermitIfAsync,但注意:Stateless 默认不 await,必须手动调用 FireAsync() 才会等待。否则 OnEntryAsync 只是“发火不管”,可能在状态已变更后才执行完扣库存。
-
PermitIf()的谓词函数在Fire()时同步执行;PermitIfAsync()则需搭配FireAsync() - 不要在
OnEntry里写耗时 IO(如 DB 查询);该放OnEntryAsync的就放进去,并确保调用方用了await stateMachine.FireAsync(trigger) - Guard 函数应尽量轻量,避免网络/DB 调用;复杂校验建议前置到 API 层,Guard 只做内存态判断(如金额非负、时间未超期)
Stateless 序列化后状态丢失?检查 StateMachine 的构造方式
直接 new StateMachine<orderstate ordertrigger>(GetState, SetState)</orderstate> 是最安全的初始化方式。如果用无参构造再手动 Configure(),反序列化后所有配置会丢失——因为配置信息不在对象字段里,而是闭包捕获的委托。
序列化时默认只存当前状态和触发器,不存配置逻辑。所以恢复时必须保证:同一个程序集、同一版本的配置代码已执行过,且 GetState/SetState 委托能正确读写你的订单实体字段(比如 order.CurrentState)。
- 别用
new StateMachine(() => state, s => state = s)这种临时变量捕获,反序列化后委托指向失效 - 推荐把状态机封装进
Order类内部,用属性访问器封装GetState/SetState,避免状态字段暴露 - 若需跨服务共享状态机定义,把
Configure()逻辑抽成静态方法,在反序列化后显式重跑一次(但注意幂等性)
测试状态流转逻辑时最容易忽略的边界
不是测“能走到目标状态”,而是测“不能走到非法状态”以及“副作用只执行一次”。比如对一个已发货的订单重复调用 Fire(Trigger.Ship),应抛异常,而不是静默忽略——后者会让前端以为操作成功,实际没发生任何事。
- 每个
Permit都要配对应反向测试:用错误状态 + 正确 Trigger,验证是否抛InvalidOperationException - 带
OnEntry的状态,要 mock 依赖并断言其调用次数为 1;重复Fire同一 Trigger 不应导致二次执行 - 异步动作测试必须用
await stateMachine.FireAsync(trigger),并用Task.Delay或async void模拟竞态,验证状态不会在 action 完成前就变成新值
Stateless 的强约束力只在配置完整、使用姿势正确时才起作用;一旦漏掉 Permit、错用同步/异步 API、或序列化路径不对,它就会退化成一个看起来很酷但实际兜不住业务的空壳。










