1. 状态模式基础:定义、核心角色与适用场景
状态模式,说白了就是让对象在内部状态改变时,能自动切换行为。你想想看,一个对象在不同状态下做同一件事,表现却完全不一样——这种场景在业务代码里太常见了。
我记得刚入行那会儿,接手过一个订单系统。订单状态有新建、已支付、已发货、已完成、已取消……每个状态下的操作逻辑都不一样。当时的前辈用了一堆 if-else 硬写,代码膨胀得厉害。后来我重构时用了状态模式,整个系统清晰多了。
1.1 状态模式的定义
状态模式(State Pattern)属于行为型设计模式。它的核心思想是:将对象的状态封装成独立的类,让对象在状态变化时委托给对应的状态类处理。
用官方的话说:允许对象在内部状态改变时改变它的行为,对象看起来好像修改了它的类。
一句话总结:状态模式 = 状态驱动行为切换。每个状态只管自己的事,互不干扰。
1.2 核心角色
状态模式有三个核心角色。我习惯把它们比作「遥控器、按钮、具体功能」——你一看就懂。
| 角色 | 名称 | 职责 | 类比 |
|---|---|---|---|
| Context | 上下文 | 持有当前状态引用,对外暴露行为接口 | 遥控器本体 |
| State | 抽象状态 | 定义状态行为的接口 | 按钮的抽象功能 |
| ConcreteState | 具体状态 | 实现特定状态下的行为逻辑 | 按一下开、再按一下关 |
Context(上下文)
Context 是客户端直接交互的对象。它维护一个 State 引用,所有请求都委托给当前 State 处理。Context 本身不关心状态切换的具体逻辑——它只负责「换人」。
State(抽象状态)
State 是一个接口或抽象类,定义了一组行为方法。这些方法对应 Context 在不同状态下需要执行的操作。比如订单系统里,State 会定义 pay()、ship()、cancel() 等方法。
ConcreteState(具体状态)
每个 ConcreteState 实现 State 接口,只关注自己状态下的行为。比如「已支付」状态下的 cancel() 可以退款,但「已发货」状态下的 cancel() 可能就不允许了。
我的习惯:写状态模式时,我会先画一张状态转换图,把每个状态允许的操作和下一个状态标清楚。这样 ConcreteState 里的逻辑就不会乱。
1.3 核心逻辑图解
下面这张图展示了状态模式的核心结构。我特意把 Context、State、ConcreteState 的关系画清楚了——箭头表示委托调用,虚线表示状态切换。
1.4 适用场景分析
状态模式不是万能的。我见过不少团队滥用它,把简单的逻辑搞复杂了。到底什么时候该用?我总结了四个典型场景。
场景一:对象行为随状态变化
这是最直接的场景。一个对象在不同状态下,对同一请求做出不同响应。比如 TCP 连接的状态机:CLOSED 状态下调用 open() 会建立连接,ESTABLISHED 状态下调用 open() 可能直接报错。
场景二:大量条件分支
代码里充斥着 if (state == A) {...} else if (state == B) {...} 这种结构。每次新增状态都要改一大片代码,容易改出 bug。
我曾经踩过的坑:有个项目用 switch-case 处理 8 种状态,每个 case 里还有嵌套条件。后来加第 9 种状态时,改漏了一个分支,线上出了 P0 事故。重构为状态模式后,每种状态独立成类,新增状态只需加一个类,再也不用担心改漏了。
场景三:状态转换规则复杂
状态之间不是随意切换的,有严格的转换图。比如工作流引擎:草稿→审批中→已通过/已驳回。状态模式把转换逻辑封装在每个 ConcreteState 里,清晰可控。
场景四:多个对象共享状态逻辑
当多个 Context 对象需要复用同一套状态行为时,状态模式天然支持。比如游戏里的 NPC,多个 NPC 可以共享「巡逻」「追击」「攻击」等状态实例。
1.5 什么时候别用状态模式
嗯,这里要注意。状态模式也有不适合的场景:
- 状态很少且稳定:比如只有 2-3 个状态,且几乎不变。用 if-else 反而更直接。
- 状态行为差异极小:如果不同状态下的行为只有一两行代码不同,没必要拆成独立类。
- 性能敏感且状态切换极频繁:每个请求都创建/销毁状态对象会有开销。当然,可以用享元模式优化。
我的建议:先写 if-else,等状态超过 4 个或者条件分支开始嵌套时,再考虑重构为状态模式。过早优化是万恶之源。
1.6 一个简单的代码示例
光说不练假把式。我写一个最简单的状态模式示例——电灯开关。只有两个状态:开和关。
// 抽象状态
interface State {
void handle(Context context);
}
// 具体状态:开
class OnState implements State {
@Override
public void handle(Context context) {
System.out.println("灯已亮,切换到关");
context.setState(new OffState());
}
}
// 具体状态:关
class OffState implements State {
@Override
public void handle(Context context) {
System.out.println("灯已灭,切换到开");
context.setState(new OnState());
}
}
// 上下文
class Context {
private State state;
public Context() {
this.state = new OffState(); // 初始状态
}
public void setState(State state) {
this.state = state;
}
public void request() {
state.handle(this);
}
}
// 客户端
public class Client {
public static void main(String[] args) {
Context context = new Context();
context.request(); // 灯已灭,切换到开
context.request(); // 灯已亮,切换到关
context.request(); // 灯已灭,切换到开
}
}
你看,每个状态只关心自己的行为。新增一个「闪烁」状态,只需要加一个 BlinkState 类,不用改任何现有代码——这就是状态模式最大的好处。
1.7 本章小结
状态模式的核心就三件事:封装状态、委托行为、解耦切换。它把复杂的状态逻辑拆散到独立类里,让代码可读性和可维护性都上了一个台阶。
我个人习惯在项目初期先画状态转换图,把每个状态的行为和转换条件列清楚。这样写出来的状态模式代码,后期几乎不用改——除非产品经理又加新状态了。嗯,你懂的。