什么是状态模式?应用场景有哪些?
结论先行:状态模式(State)把"对象在某个状态下的行为"抽成独立的状态类,对象内部持有当前状态对象,行为随之委托给它;状态之间的迁移由状态自己或上下文驱动。它治的是同一个病:一个 switch(state) 散落在七八个方法里,每加一个状态就要改所有分支。
1. 结构:行为跟着状态走
┌──────────────────────────┐
│ OrderContext │
│ - state: OrderState │
├──────────────────────────┤
│ + pay() → state.pay() │
│ + ship() → state.ship() │
│ + setState(s) │
└────────────┬─────────────┘
│ 委托
┌─────────────────────┼─────────────────────┐
▼ ▼ ▼
┌────────────────┐ ┌────────────────┐ ┌────────────────┐
│ UnpaidState │ │ PaidState │ │ ShippedState │
│ pay()→Paid │ │ ship()→Shipped │ │ ship()→拒绝 │
│ ship()→拒绝 │ │ cancel()→已付 │ │ finish()→完成 │
└────────────────┘ └────────────────┘ └────────────────┘
调用方始终只看到 OrderContext.pay();状态不同,同一个方法的表现不同。
2. 状态机视角
┌──────────┐ pay ┌──────────┐ ship ┌───────────┐ confirm ┌──────────┐
│ 待支付 │───────►│ 已支付 │───────►│ 已发货 │──────────►│ 已完成 │
└────┬─────┘ └────┬─────┘ └───────────┘ └──────────┘
│ cancel │ refund
▼ ▼
┌──────────┐ ┌──────────┐
│ 已取消 │ │ 退款中 │
└──────────┘ └──────────┘
把这张图画出来,就能一眼看出:哪些迁移合法、哪些状态是终态、有没有"回退边"。状态模式就是把这幅图写成代码。
3. 代码:订单状态
interface OrderState {
void pay(OrderContext ctx);
void ship(OrderContext ctx);
String name();
}
class UnpaidState implements OrderState {
public void pay(OrderContext ctx) {
System.out.println("支付成功");
ctx.setState(new PaidState()); // 状态自迁移
}
public void ship(OrderContext ctx) {
throw new IllegalStateException("未支付不能发货"); // 非法迁移显式失败
}
public String name() { return "待支付"; }
}
class PaidState implements OrderState {
public void pay(OrderContext ctx) {
System.out.println("已支付,忽略重复支付");
}
public void ship(OrderContext ctx) {
System.out.println("已发货");
ctx.setState(new ShippedState());
}
public String name() { return "已支付"; }
}
class ShippedState implements OrderState {
public void pay(OrderContext ctx) { throw new IllegalStateException("已发货,不能支付"); }
public void ship(OrderContext ctx) { System.out.println("已发货,忽略重复操作"); }
public String name() { return "已发货"; }
}
class OrderContext {
private OrderState state = new UnpaidState(); // 初始状态
void setState(OrderState state) { this.state = state; }
String stateName() { return state.name(); }
void pay() { state.pay(this); }
void ship() { state.ship(this); }
}
// 使用
OrderContext order = new OrderContext();
order.ship(); // 抛 IllegalStateException:未支付不能发货
order.pay(); // 支付成功 → 迁移到 PaidState
order.ship(); // 已发货 → 迁移到 ShippedState
注意 OrderState 的方法都接收 ctx,这样状态类才能把上下文推到下一个状态——这是状态模式区别于普通策略的关键细节。
4. 状态模式与策略模式
| 对比项 | 状态模式 | 策略模式 |
|---|---|---|
| 谁决定用哪个实现 | 内部状态自己迁移 | 外部调用方显式指定 |
| 实现之间是否互相知道 | 知道(要能跳到下一个状态) | 通常互不知道 |
| 关注点 | 对象生命周期上的阶段 | 同一件事的不同算法 |
| 调用方视角 | 只调一次接口,行为随状态变化 | 先选策略,再调用 |
| 典型例子 | 订单、审批、TCP 连接 | 计价规则、压缩算法、路由策略 |
一句话区分:策略是"你选我",状态是"我自己走"。面试常被用来验证你是否真的理解两者的意图差异。
5. 应用场景
| 场景 | 状态举例 |
|---|---|
| 电商订单 | 待支付、已支付、已发货、已完成、已取消、退款中 |
| 审批流 | 草稿、待审批、审批中、通过、驳回 |
| 工作流引擎 | Activiti/Flowable 的节点与连线本质上就是状态机 |
| 网络连接 | TCP 的 LISTEN、SYN_SENT、ESTABLISHED、CLOSE_WAIT |
| 游戏 | 角色的待机、移动、攻击、死亡 |
| 账号风控 | 正常、限流、冻结、申诉中 |
6. 常见追问
Q:状态多了会类爆炸吗? 会的。5 个状态 × 4 个动作就是 20 个方法待实现。缓解办法:把"某状态下不支持的动作"收敛到一个抽象基类的默认抛异常实现,子类只覆写合法的迁移;状态超过十几个时,改用表驱动(状态迁移表 + 配置),或引入成熟状态机框架。
Q:状态对象要不要每次 new? 如果状态类无字段(多数情况),可以做成单例常量复用,避免频繁创建;如果要携带进入该状态的时间戳等数据,就必须 new。
Q:状态怎么持久化、怎么恢复? 数据库存状态枚举值而不是对象引用,加载时按枚举构造对应状态类。千万别把状态对象序列化进 DB——重构一次类名就全废了。
Q:并发下会不会两个请求同时迁移?
会。状态迁移必须加乐观锁:update orders set state='PAID' where id=? and state='UNPAID',影响行数为 0 说明状态已被别人改过,直接返回冲突。
小结
- 状态模式 = 把
switch(state)拆成状态类,行为委托给当前状态,迁移由状态驱动。 - 与策略的分水岭:谁来决定用哪个实现——内部状态 vs 外部调用方。
- 落地要点:非法迁移显式抛错、无状态类可复用、库里存枚举值、迁移加乐观锁;状态再多就转向迁移表。