什么是状态模式?它解决什么问题?
简化版
状态模式是把对象在不同状态下的行为封装到不同状态类中,让对象在状态变化时表现出不同的行为。它主要解决“一个对象有多种状态,并且每种状态下行为不同,代码里充满 if else 或 switch”的问题。
详细版
状态模式属于行为型设计模式,核心思想是:把状态相关的判断逻辑从上下文对象中拆出去,每个状态类只负责自己状态下的行为和可能的状态迁移。
典型结构是:
Context 持有 State
State 定义统一行为接口
ConcreteState 实现具体状态行为
比如订单有待支付、已支付、已发货、已完成、已取消等状态。不同状态下调用“支付”“取消”“发货”会有不同结果。如果全部写在 Order 类里,代码很容易变成大段条件分支。状态模式可以让每个订单状态对应一个状态类,把行为分散到状态对象中。
状态模式的价值是减少复杂条件判断,让状态行为更清晰,也让新增状态更容易。但它会增加类数量,并且状态迁移关系复杂时,需要额外管理状态流转规则。
完整版教学
一、先理解“状态影响行为”是什么
很多对象不是一直用同一种方式工作,它的行为会受到当前状态影响。
比如订单:
- 待支付状态:可以支付、可以取消,不能发货;
- 已支付状态:可以发货,可以退款,不能再次支付;
- 已发货状态:可以确认收货,不能取消;
- 已完成状态:可以评价,不能发货;
- 已取消状态:大多数操作都不允许。
同一个方法名,在不同状态下语义不同。比如 cancel():
待支付订单 cancel -> 取消成功
已支付订单 cancel -> 可能需要退款后取消
已发货订单 cancel -> 不允许取消
已完成订单 cancel -> 不允许取消
如果这些逻辑都写在订单类里,订单类会越来越胖。
二、没有状态模式时容易出现条件分支膨胀
常见写法是:
public void cancel(Order order) {
if (order.status == WAIT_PAY) {
order.status = CANCELED;
} else if (order.status == PAID) {
refund(order);
order.status = CANCELED;
} else if (order.status == SHIPPED) {
throw new RuntimeException("已发货不能取消");
} else if (order.status == FINISHED) {
throw new RuntimeException("已完成不能取消");
}
}
一个操作已经这样复杂,如果再加上 pay()、ship()、finish()、refund(),条件分支会成倍增长。
状态模式就是把这些分支拆成状态类。
三、状态模式把状态行为封装成对象
状态模式会定义一个状态接口:
interface OrderState {
void pay(OrderContext context);
void cancel(OrderContext context);
void ship(OrderContext context);
}
然后每个状态类实现自己能做的事情:
class WaitPayState implements OrderState {
public void pay(OrderContext context) {
context.setState(new PaidState());
}
public void cancel(OrderContext context) {
context.setState(new CanceledState());
}
public void ship(OrderContext context) {
throw new RuntimeException("待支付不能发货");
}
}
这样,状态相关行为从主对象里分离出来了。
四、Context 负责持有当前状态
Context 是拥有状态的对象,比如订单上下文:
class OrderContext {
private OrderState state;
public void pay() {
state.pay(this);
}
public void setState(OrderState state) {
this.state = state;
}
}
客户端调用的是 order.pay(),真正执行哪个逻辑由当前 state 决定。
这就是状态模式的关键:对象行为随着内部状态对象变化而变化。
五、状态模式不是简单把 if else 换地方
如果只是把 if else 从 Order 类搬到某个 StateHandler 里,状态模式价值不大。
真正的状态模式应该让每个状态类只关心自己状态下允许的行为和迁移。这样代码从“按操作集中判断所有状态”,变成“按状态封装自己的行为”。
换句话说,状态模式的组织方式是:
待支付状态知道待支付能做什么
已支付状态知道已支付能做什么
已发货状态知道已发货能做什么
六、状态对象如何替代行为分支
订单有待支付、已支付、已取消 3 个状态和 pay/cancel 两个动作,过程式写法最多要判断 3×2 个组合;状态对象把合法行为放回对应状态。
Context.pay -> currentState.pay(context) -> transition or reject
状态题的正确性落在“当前状态 + 事件 + 守卫条件 → 目标状态”这一条边上,而不是类数量上。每条边都要说明持久化原子性、重复事件结果和副作用触发时机,非法边则必须得到稳定且可观测的拒绝。
七、迁移一致性、幂等与并发验证
| 检查维度 | 应确认的内容 |
|---|---|
| 机制正确性 | 状态模式解决的是“状态决定行为”,并不等于数据库状态字段可以省略。 |
| 适用边界 | 状态少、行为简单且稳定时枚举加转换表更直接;状态类不是越多越专业。 |
| 测试证据 | 覆盖正常迁移、重复事件、非法迁移、持久化失败和两个请求竞争同一版本 |
| 工程代价 | 重点评估状态数、迁移边数、一次迁移的数据库竞争,以及新增状态对完整迁移矩阵的影响 |
易错点:状态模式解决的是“状态决定行为”,并不等于数据库状态字段可以省略。
八、常见误区与追问
- 误区:状态模式就是把每个状态值包装成对象。 关键是封装该状态下的行为和迁移规则,只建空状态类没有消除分支。
- 误区:把状态码换成状态类就自动消除了所有条件判断。 状态对象只能封装状态相关行为;持久化、并发控制、权限和跨聚合规则仍需显式设计。
- 追问:状态对象由谁切换? 简单场景可由状态实现触发,复杂场景可由 Context 或集中迁移器统一控制。
- 追问:非法状态迁移应该静默忽略吗? 不应该;应返回明确业务错误并记录当前状态、事件和聚合标识,重复事件则按幂等规则单独处理。
- 追问:状态模式最容易漏掉哪类测试? 非法迁移、重复事件和并发竞争;它们比单线程成功路径更能证明状态模型可靠。
九、加强记忆
状态模式要记成“状态变了,行为也变;把每种状态下的行为封装成状态类”。它解决多状态对象里的大量条件分支,适合订单、审批、工单、播放器、连接会话等状态驱动场景;但状态很多、迁移复杂时,要注意状态关系治理。