什么是状态模式?应用场景有哪些?
面试考察点
-
痛点识别能力:面试官想知道你是否遇到过 "状态多了,
if-else爆炸" 的场景,以及你是否能意识到这是一个设计问题而非业务问题。 -
模式理解深度:能否讲清楚 "把状态的行为封装成独立类,通过组合替代判断" 这个核心思路,而不是只会背一句 "对象行为随状态改变而改变"。
-
实战应用:能否举出订单状态机、工作流引擎等实际应用场景,证明你真的在项目中用过或者见过这个模式。
核心答案
一句话定义:状态模式允许一个对象在其内部状态改变时改变它的行为,对象看起来好像修改了它的类。
说人话就是:当一个对象有多种状态,不同状态下对同一操作的行为不同时,不要用 if-else 硬编码,而是把每个状态的行为封装成独立的类,对象持有当前状态的引用,操作委托给状态对象来执行。
生活中的例子:自动售货机。投币前你按 "出货" 没反应,投币后按 "出货" 吐商品,商品出完按 "出货" 显示 "已售罄"。同一个按钮,三种状态三种行为。你当然可以用 if (state == XX) 来写,但状态多了以后代码就炸了。
上面的图展示了状态模式的核心结构:
Context(上下文):持有当前状态的引用,对外提供操作方法。客户端只跟Context打交道,不知道当前具体是哪个状态。State(状态接口):定义所有状态下共有的操作方法。ConcreteState(具体状态):每个状态类实现自己的行为逻辑,并决定状态转换规则。
关键点在于:状态的切换逻辑分散在各个状态类内部,而不是集中在 Context 的一个大 switch-case 里。新增状态只需要新增一个类,完全不影响已有代码。
深度解析
一、不用状态模式:if-else 地狱
先看看不用状态模式的代码长什么样,你就知道痛点在哪了。
// 订单状态常量
public static final int UNPAID = 0; // 待付款
public static final int PAID = 1; // 已付款
public static final int SHIPPED = 2; // 已发货
public static final int COMPLETED = 3; // 已完成
public class Order {
private int state;
// 付款操作
public void pay() {
if (state == UNPAID) {
System.out.println("付款成功");
state = PAID;
} else if (state == PAID) {
System.out.println("已付款,请勿重复操作");
} else if (state == SHIPPED) {
System.out.println("已发货,无法操作");
} else {
System.out.println("订单已完成");
}
}
// 发货操作
public void ship() {
if (state == UNPAID) {
System.out.println("请先付款");
} else if (state == PAID) {
System.out.println("发货成功");
state = SHIPPED;
} else if (state == SHIPPED) {
System.out.println("已发货,请勿重复操作");
} else {
System.out.println("订单已完成");
}
}
// 确认收货
public void confirm() {
if (state == UNPAID) {
System.out.println("请先付款");
} else if (state == PAID) {
System.out.println("请等待发货");
} else if (state == SHIPPED) {
System.out.println("确认收货成功");
state = COMPLETED;
} else {
System.out.println("已完成,请勿重复操作");
}
}
}
看到了吗?每个操作方法里都有 4 层 if-else。这才 4 个状态、3 个操作。如果状态增加到 8 个,操作增加到 10 个呢?那就是 80 个 if-else 分支。而且新增一个状态,所有方法都得改,违反了开闭原则。
二、用状态模式重构
把每个状态的行为抽成独立类,问题就迎刃而解了。
// 状态接口
public interface OrderState {
void pay(OrderContext context);
void ship(OrderContext context);
void confirm(OrderContext context);
}
// 上下文(订单)
public class OrderContext {
private OrderState state;
public OrderContext() {
this.state = new UnpaidState(); // 初始状态:待付款
}
public void setState(OrderState state) {
this.state = state;
}
// 操作委托给当前状态对象
public void pay() {
state.pay(this);
}
public void ship() {
state.ship(this);
}
public void confirm() {
state.confirm(this);
}
}
四个具体状态类:
// 待付款状态
public class UnpaidState implements OrderState {
@Override
public void pay(OrderContext ctx) {
System.out.println("付款成功 ✓");
ctx.setState(new PaidState()); // 切换到已付款
}
@Override
public void ship(OrderContext ctx) {
System.out.println("请先付款");
}
@Override
public void confirm(OrderContext ctx) {
System.out.println("请先付款");
}
}
// 已付款状态
public class PaidState implements OrderState {
@Override
public void pay(OrderContext ctx) {
System.out.println("已付款,请勿重复操作");
}
@Override
public void ship(OrderContext ctx) {
System.out.println("发货成功 ✓");
ctx.setState(new ShippedState()); // 切换到已发货
}
@Override
public void confirm(OrderContext ctx) {
System.out.println("请等待发货");
}
}
// 已发货状态
public class ShippedState implements OrderState {
@Override
public void pay(OrderContext ctx) {
System.out.println("已付款,无需操作");
}
@Override
public void ship(OrderContext ctx) {
System.out.println("已发货,请勿重复操作");
}
@Override
public void confirm(OrderContext ctx) {
System.out.println("确认收货 ✓");
ctx.setState(new CompletedState()); // 切换到已完成
}
}
// 已完成状态
public class CompletedState implements OrderState {
@Override
public void pay(OrderContext ctx) {
System.out.println("订单已完成");
}
@Override
public void ship(OrderContext ctx) {
System.out.println("订单已完成");
}
@Override
public void confirm(OrderContext ctx) {
System.out.println("已完成,请勿重复操作");
}
}
使用:
OrderContext order = new OrderContext();
order.ship(); // 请先付款
order.pay(); // 付款成功 ✓
order.pay(); // 已付款,请勿重复操作
order.ship(); // 发货成功 ✓
order.confirm(); // 确认收货 ✓
order.confirm(); // 已完成,请勿重复操作
重构后的代码,每个状态类只关心自己的逻辑,新增状态只需要新增一个类,完全不用动其他状态类的代码。Context 的代码也变得极其简洁——每个操作就一行,委托给 state 就完事了。
三、状态模式 vs 策略模式
这两个模式在代码结构上几乎一模一样,面试时特别容易被追问。区别在哪?
| 对比项 | 状态模式 | 策略模式 |
|---|---|---|
| 核心目的 | 对象行为随内部状态变化 | 算法自由切换,客户端选策略 |
| 谁负责切换 | 状态对象自己切换(自动) | 客户端手动设置策略(被动) |
| 状态/策略是否互相知道 | 状态之间有转换关系,彼此知道 | 策略之间完全独立,互不知道 |
| 典型场景 | 订单状态机、工作流 | 支付方式选择、排序算法切换 |
一句话区分:状态模式的切换是自动的(状态对象内部决定),策略模式的切换是手动的(客户端决定)。
四、实际应用场景
1. 电商订单状态流转
最经典的场景。待付款 → 已付款 → 已发货 → 已完成,还有取消、退款等分支状态。用状态模式管理状态转换,逻辑清晰,扩展方便。
2. 工作流引擎
Activiti、Flowable 这些工作流引擎里,流程节点的流转本质上就是状态模式。审批通过从 "待审批" 到 "已审批",驳回到 "待修改",每个节点的操作行为都不同。
3. TCP 连接状态
TCP 协议有 LISTEN、SYN_SENT、ESTABLISHED、CLOSE_WAIT 等多种状态,不同状态下对同一个报文的处理方式完全不同。经典的 TCP 状态机就是状态模式的应用。
4. 游戏角色状态
游戏角色有站立、行走、奔跑、跳跃、受伤等状态,每个状态下按同一个键的行为完全不同。比如跳跃状态下按跳跃是二段跳,站立状态下按跳跃是起跳。
5. Spring StateMachine
Spring 官方提供了 spring-statemachine 框架,专门用来管理复杂的状态流转,底层就是状态模式 + 事件驱动的思想。
面试高频追问
-
状态模式和策略模式有什么区别?
- 结构相似,意图不同。状态模式的切换由状态对象内部自动完成,策略模式由客户端手动设置。状态之间有转换关系,策略之间完全独立。
-
什么情况下应该用状态模式?
- 当一个对象有多个状态,不同状态下对同一操作的行为不同,且状态转换规则复杂时。简单的 2~3 个状态判断用
if-else就够了,别过度设计。
- 当一个对象有多个状态,不同状态下对同一操作的行为不同,且状态转换规则复杂时。简单的 2~3 个状态判断用
-
状态模式会不会导致类爆炸?
- 会,每个状态一个类。但相比一个几千行的
switch-case,分散到多个小类里可维护性好得多。这是 "用类的数量换代码清晰度" 的典型权衡。
- 会,每个状态一个类。但相比一个几千行的
常见面试变体
- "如何设计一个订单状态机?"
- "状态模式和策略模式有什么区别?"
- "如果不用状态模式,你会怎么处理多状态逻辑?"
- "你在项目中用过状态模式吗?举一个例子"
记忆口诀
核心思想:状态封装成类,行为跟着状态走,切换由状态自己定。
vs 策略模式:状态自动切换,策略手动选择。
适用时机:状态多、转换复杂、if-else 成灾。
总结
状态模式的精髓就是 "把每个状态的行为封装成独立类,用组合替代 if-else"。面试时先说概念,然后用订单状态机这个例子对比 "重构前" 和 "重构后" 的代码,最后和策略模式做区分。能把 if-else 地狱的痛点讲清楚,再展示重构后的优雅,面试官就知道你不只是背了概念。
