状态模式中状态迁移应该怎么设计?
简化版
状态迁移设计要明确当前状态、触发事件、目标状态、迁移动作和非法迁移处理。简单场景可以由状态类直接切换状态,复杂场景更建议集中维护状态迁移表或状态机,避免迁移规则散落、难以审查和测试。
详细版
状态迁移不能只写 setState(),还要回答几个问题:
- 从哪些状态可以迁移到哪些状态;
- 什么事件触发迁移;
- 迁移前需要什么校验;
- 迁移过程中要执行什么动作;
- 迁移失败如何回滚;
- 非法迁移如何提示;
- 状态变更是否需要持久化和日志。
在状态模式中,状态迁移可以由具体状态类负责,也可以由 Context 或状态机组件统一负责。状态数量少时放在状态类中比较自然;状态多、迁移复杂时,集中式迁移表更清晰。
面试中可以结合订单例子说:状态迁移必须受业务规则约束,不能让调用方随意设置状态。
完整版教学
一、状态迁移首先要画清状态图
状态模式不是随便从一个状态跳到另一个状态。
比如订单状态可以是:
待支付 -> 已支付 -> 已发货 -> 已完成
待支付 -> 已取消
已支付 -> 已退款
但通常不允许:
待支付 -> 已完成
已取消 -> 已发货
已完成 -> 待支付
状态图是状态迁移设计的基础。没有状态图,代码里的迁移很容易变成隐式规则。
二、迁移要由事件触发
更清晰的建模方式是:
当前状态 + 事件 -> 目标状态
比如:
| 当前状态 | 事件 | 目标状态 |
|---|---|---|
| 待支付 | 支付成功 | 已支付 |
| 待支付 | 用户取消 | 已取消 |
| 已支付 | 发货 | 已发货 |
| 已发货 | 确认收货 | 已完成 |
这样比单纯写 setState(PAID) 更容易理解,因为它说明了状态为什么变化。
三、状态类直接迁移适合简单场景
简单状态模式中,可以在状态类里直接修改状态:
class WaitPayState implements OrderState {
public void pay(OrderContext context) {
context.setState(new PaidState());
}
}
优点是直观,缺点是迁移规则分散在各个状态类中。
当状态数量少时,这种方式很好读。
四、集中式迁移表适合复杂场景
如果状态和事件很多,可以把迁移集中管理:
(WAIT_PAY, PAY_SUCCESS) -> PAID
(WAIT_PAY, CANCEL) -> CANCELED
(PAID, SHIP) -> SHIPPED
集中式管理的好处是:
- 能看清完整状态图;
- 容易校验非法迁移;
- 容易写测试;
- 容易记录状态变更日志;
- 更适合配置化和可视化。
这时状态模式可以退到“状态行为封装”的位置,迁移合法性由状态机或迁移表管理。
五、迁移动作要和状态更新保持一致
状态迁移往往伴随副作用。
比如支付成功:
- 更新订单状态;
- 记录支付单;
- 扣减库存;
- 发送消息;
- 写状态变更日志。
这些动作如果部分成功、部分失败,会导致状态不一致。
因此真实项目要考虑事务边界、消息可靠性、幂等和补偿。状态模式本身不自动解决这些问题。
六、并发状态迁移要特别小心
同一个订单可能同时收到两个请求:
支付成功回调
用户取消请求
如果没有并发控制,订单可能从待支付同时变成已支付和已取消。
常见方案包括:
- 数据库乐观锁;
- 条件更新:
where status = WAIT_PAY; - 分布式锁;
- 幂等表;
- 状态变更流水。
面试中提到这些工程细节,会比只讲类图更有说服力。
七、非法迁移要统一处理
非法迁移不能悄悄忽略。
比如已取消订单收到发货事件,应该明确返回错误或记录告警。
推荐统一处理:
当前状态不允许该事件
而不是每个状态类随便抛不同异常。
八、用状态、事件、目标三元组定义迁移
5 个状态、4 类事件理论组合有 20 个,实际只允许 8 条;显式迁移表能让其余 12 条自动判为非法。
(currentState, event) -> guard/action -> nextState
状态题的正确性落在“当前状态 + 事件 + 守卫条件 → 目标状态”这一条边上,而不是类数量上。每条边都要说明持久化原子性、重复事件结果和副作用触发时机,非法边则必须得到稳定且可观测的拒绝。
九、迁移一致性、幂等与并发验证
| 检查维度 | 应确认的内容 |
|---|---|
| 机制正确性 | 状态图先于代码:没有图或表,就很难证明迁移完整且无冲突。 |
| 适用边界 | 迁移需要守卫条件、原子更新和幂等;副作用应在状态成功更新后按可靠机制触发。 |
| 测试证据 | 覆盖正常迁移、重复事件、非法迁移、持久化失败和两个请求竞争同一版本 |
| 工程代价 | 重点评估状态数、迁移边数、一次迁移的数据库竞争,以及新增状态对完整迁移矩阵的影响 |
易错点:状态图先于代码:没有图或表,就很难证明迁移完整且无冲突。
十、常见误区与追问
- 误区:状态类内部直接 set 任意目标状态最灵活。 任意设置会绕过合法边校验,应通过受控事件或集中迁移函数。
- 误区:把状态码换成状态类就自动消除了所有条件判断。 状态对象只能封装状态相关行为;持久化、并发控制、权限和跨聚合规则仍需显式设计。
- 追问:守卫条件应该放在哪里? 简单状态局部条件可放 State,跨聚合复杂规则宜由领域服务判断并把结果显式传入。
- 追问:非法状态迁移应该静默忽略吗? 不应该;应返回明确业务错误并记录当前状态、事件和聚合标识,重复事件则按幂等规则单独处理。
- 追问:状态模式最容易漏掉哪类测试? 非法迁移、重复事件和并发竞争;它们比单线程成功路径更能证明状态模型可靠。
十一、加强记忆
状态迁移设计要记成“状态图先行,事件触发,合法迁移,动作一致,非法兜底”。简单场景可以让状态类直接切换状态,复杂业务要用迁移表或状态机集中管理,并补上持久化、并发控制和状态日志。