← 返回题目列表

状态模式中状态迁移应该怎么设计?

高频 中等 第 11 / 25 题 更新于 2026/07/28
状态模式状态迁移状态流转设计模式

简化版

状态迁移设计要明确当前状态、触发事件、目标状态、迁移动作和非法迁移处理。简单场景可以由状态类直接切换状态,复杂场景更建议集中维护状态迁移表或状态机,避免迁移规则散落、难以审查和测试。

详细版

状态迁移不能只写 setState(),还要回答几个问题:

  1. 从哪些状态可以迁移到哪些状态;
  2. 什么事件触发迁移;
  3. 迁移前需要什么校验;
  4. 迁移过程中要执行什么动作;
  5. 迁移失败如何回滚;
  6. 非法迁移如何提示;
  7. 状态变更是否需要持久化和日志。

在状态模式中,状态迁移可以由具体状态类负责,也可以由 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,跨聚合复杂规则宜由领域服务判断并把结果显式传入。
  • 追问:非法状态迁移应该静默忽略吗? 不应该;应返回明确业务错误并记录当前状态、事件和聚合标识,重复事件则按幂等规则单独处理。
  • 追问:状态模式最容易漏掉哪类测试? 非法迁移、重复事件和并发竞争;它们比单线程成功路径更能证明状态模型可靠。

十一、加强记忆

状态迁移设计要记成“状态图先行,事件触发,合法迁移,动作一致,非法兜底”。简单场景可以让状态类直接切换状态,复杂业务要用迁移表或状态机集中管理,并补上持久化、并发控制和状态日志。