← 返回题目列表

状态模式有哪些角色?调用流程是什么?

高频 简单 第 2 / 25 题 更新于 2026/07/28
状态模式ContextStateConcreteState状态流转

简化版

状态模式通常包含 Context、State 和 ConcreteState 三类角色。Context 持有当前状态并把操作委托给状态对象,State 定义统一行为接口,ConcreteState 实现具体状态下的行为,并在必要时修改 Context 的当前状态。

详细版

状态模式的角色包括:

  1. Context:上下文对象,也就是拥有状态的业务对象;
  2. State:状态接口,定义不同状态下可能执行的行为;
  3. ConcreteState:具体状态类,实现当前状态下的业务行为;
  4. 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 委托当前状态对象执行,状态对象可以决定行为结果和下一个状态。答题时把“委托”和“状态迁移”说清楚就抓住了核心。