状态模式有哪些角色?调用流程是什么?
简化版
状态模式通常包含 Context、State 和 ConcreteState 三类角色。Context 持有当前状态并把操作委托给状态对象,State 定义统一行为接口,ConcreteState 实现具体状态下的行为,并在必要时修改 Context 的当前状态。
详细版
状态模式的角色包括:
- Context:上下文对象,也就是拥有状态的业务对象;
- State:状态接口,定义不同状态下可能执行的行为;
- ConcreteState:具体状态类,实现当前状态下的业务行为;
- Client:客户端,调用 Context 的方法,不直接操作具体状态类。
调用流程通常是:
Client 调用 Context.operation()
-> Context 委托给当前 State
-> ConcreteState 执行当前状态行为
-> ConcreteState 根据需要切换 Context 的状态
面试时要强调:状态模式的核心不是类图,而是“行为委托给当前状态对象”。客户端看到的是同一个 Context,但 Context 内部状态对象不同,最终表现出的行为也不同。
完整版教学
一、Context:拥有状态的对象
Context 是业务上真正被操作的对象。
比如:
- 订单;
- 工单;
- 播放器;
- TCP 连接;
- 审批单;
- 电梯。
Context 里通常会持有一个当前状态:
class OrderContext {
private OrderState state;
}
客户端不应该绕过 Context 直接调用某个状态类。否则状态模式的封装性会被破坏。
二、State:定义状态行为接口
State 接口规定所有状态都要支持哪些行为。
比如订单状态:
interface OrderState {
void pay(OrderContext context);
void cancel(OrderContext context);
void ship(OrderContext context);
void finish(OrderContext context);
}
注意,这些方法不代表每个状态都允许执行。它们代表“系统可能收到这些操作”。如果某个状态不支持某个操作,可以抛异常、返回失败结果或忽略,具体看业务规范。
三、ConcreteState:每个状态处理自己的行为
具体状态类把状态相关行为封装起来。
比如待支付状态:
class WaitPayState implements OrderState {
public void pay(OrderContext context) {
context.setState(new PaidState());
}
public void cancel(OrderContext context) {
context.setState(new CanceledState());
}
}
已支付状态:
class PaidState implements OrderState {
public void ship(OrderContext context) {
context.setState(new ShippedState());
}
public void pay(OrderContext context) {
throw new RuntimeException("订单已支付,不能重复支付");
}
}
每个状态类都围绕自己的状态展开,避免一个大类判断所有状态。
四、状态迁移通常由状态类或 Context 触发
状态切换可以有两种写法。
第一种:具体状态类直接修改 Context 状态。
context.setState(new PaidState());
第二种:具体状态类返回下一个状态,由 Context 统一设置。
OrderState next = state.pay();
this.state = next;
第一种简单直接,第二种更容易集中管理迁移和日志。实际项目可以根据复杂度选择。
五、Client:只调用上下文对象
客户端应该这样调用:
order.pay();
order.ship();
而不是:
new WaitPayState().pay(order);
因为客户端不应该知道订单当前是什么状态,也不应该负责选择具体状态类。状态选择和行为委托应该封装在 Context 内部。
六、状态模式的流程重点是委托
状态模式的调用不是:
Context 自己 if else 判断状态
而是:
Context 把操作交给当前 State
当前 State 不同,表现就不同。这种结构把变化点从条件分支变成多态分派。
七、Context 委托与迁移闭环
订单从 Pending 执行第 1 次 pay 后切到 Paid;第 2 次 pay 到达时由 PaidState 拒绝或按幂等返回,因此 2 次调用最多只能产生 1 次扣款。
Client -> Context.pay -> PendingState.pay -> persist Paid -> Context.state=Paid
状态题的正确性落在“当前状态 + 事件 + 守卫条件 → 目标状态”这一条边上,而不是类数量上。每条边都要说明持久化原子性、重复事件结果和副作用触发时机,非法边则必须得到稳定且可观测的拒绝。
八、迁移一致性、幂等与并发验证
| 检查维度 | 应确认的内容 |
|---|---|
| 机制正确性 | 一次迁移要同时回答前置状态、事件、目标状态、副作用和失败结果。 |
| 适用边界 | Context 对外提供稳定入口,State 处理当前行为;持久化成功与内存对象更新必须保持一致。 |
| 测试证据 | 覆盖正常迁移、重复事件、非法迁移、持久化失败和两个请求竞争同一版本 |
| 工程代价 | 重点评估状态数、迁移边数、一次迁移的数据库竞争,以及新增状态对完整迁移矩阵的影响 |
易错点:一次迁移要同时回答前置状态、事件、目标状态、副作用和失败结果。
九、常见误区与追问
- 误区:客户端应该先判断状态再调用 Context。 这会把状态分支重新泄漏到客户端,应直接调用行为入口并由状态模型校验。
- 误区:把状态码换成状态类就自动消除了所有条件判断。 状态对象只能封装状态相关行为;持久化、并发控制、权限和跨聚合规则仍需显式设计。
- 追问:State 是否必须持有 Context? 不必须;可由方法参数传入迁移能力,也可返回目标状态,选择取决于耦合和测试需求。
- 追问:非法状态迁移应该静默忽略吗? 不应该;应返回明确业务错误并记录当前状态、事件和聚合标识,重复事件则按幂等规则单独处理。
- 追问:状态模式最容易漏掉哪类测试? 非法迁移、重复事件和并发竞争;它们比单线程成功路径更能证明状态模型可靠。
十、加强记忆
状态模式角色可以记成“Context 持有 State,State 定义行为,ConcreteState 处理状态细节”。客户端只找 Context,Context 委托当前状态对象执行,状态对象可以决定行为结果和下一个状态。答题时把“委托”和“状态迁移”说清楚就抓住了核心。