状态模式
1. 核心概念与价值
状态模式的定义是:允许一个对象在其内部状态改变时改变它的行为。对象看起来似乎修改了它的类。
它的核心思想是:将每一种状态封装成一个独立的类/对象,并将请求委托给当前状态对象。
| 维度 | 描述 |
|---|---|
| 核心目的 | 消除庞大的条件分支语句,将状态逻辑分布到各个状态类中。 |
| 主要优点 | 1. 结构清晰,避免了状态判断的逻辑堆积; 2. 封装性好,将“状态切换”和“状态行为”结合在一起; 3. 扩展性强,新增状态只需增加一个新类,符合开闭原则。 |
| 主要缺点 | 1. 增加系统类/对象的数量; 2. 逻辑分散,如果状态极多且跳转复杂,可能导致“迷路”。 |
2. 模式演进:从“面条代码”到“状态机”
让我们以一个简单的 “交通灯” 系统为例。
1. 传统写法(难以维护)
这种写法在增加新状态(如“黄灯闪烁”)时,需要改动整个函数逻辑。
js
class TrafficLight {
constructor() {
this.state = 'red';
}
change() {
if (this.state === 'red') {
console.log('红灯停 -> 变绿灯');
this.state = 'green';
} else if (this.state === 'green') {
console.log('绿灯行 -> 变黄灯');
this.state = 'yellow';
} else if (this.state === 'yellow') {
console.log('黄灯等 -> 变红灯');
this.state = 'red';
}
}
}2. 状态模式写法(解耦版)
我们将行为委托给具体的状态对象。
js
// 1. 定义状态对象集合
const States = {
red: {
handle(context) {
console.log('🔴 红灯停 -> 下一站:绿灯');
context.setState(States.green); // 状态自动流转
}
},
green: {
handle(context) {
console.log('🟢 绿灯行 -> 下一站:黄灯');
context.setState(States.yellow);
}
},
yellow: {
handle(context) {
console.log('🟡 黄灯等 -> 下一站:红灯');
context.setState(States.red);
}
}
};
// 2. 环境类 (Context)
class TrafficLight {
constructor() {
this.currentState = States.red; // 初始状态
}
setState(newState) {
this.currentState = newState;
}
request() {
this.currentState.handle(this);
}
}
// 使用
const light = new TrafficLight();
light.request(); // 🔴 红灯停...
light.request(); // 🟢 绿灯行...3. 实战场景:工作流与复杂 UI
状态模式在以下场景中几乎是“标准答案”:
- 文件审批流:
草稿 -> 待审批 -> 已发布 -> 已下线。 - 网络游戏状态:
加载中 -> 菜单页 -> 战斗中 -> 结算中。 - 复杂组件 UI:例如一个上传组件,有
等待上传、上传中、暂停、成功、失败状态,不同状态下按钮的文字和功能完全不同。
4. 常见问题 (FAQ)
4.1 状态模式和策略模式(Strategy Pattern)有什么区别?
这是最经典的问题。它们结构非常相似,但意图完全不同:
- 策略模式:不同的算法之间是平等、独立的。由外部调用者决定用哪一个。它们通常不互相知道对方的存在。
- 状态模式:状态之间是有联系、有流转的。状态的切换通常是由状态内部逻辑自动触发的。目的是表现对象在不同生命周期的不同行为。
4.2 状态模式会增加很多对象,值得吗?
- 答:如果你的状态只有 2 个(如 开/关),用布尔值即可。但如果状态超过 3 个,且每个状态下的操作逻辑很重,那么增加对象的成本远低于维护“面条代码”的成本。
4.3 如何处理状态转换非常复杂的情况?
- 答:如果状态转换变成了一个巨大的迷宫,手动写状态模式可能会出错。此时推荐使用 有限状态机 (FSM) 库。
- 在 JavaScript 社区,XState 是目前的行业标准,它基于状态图理论,能处理极复杂的逻辑。
4.4 状态对象需要存储在 Context 内部还是外部?
- 答:
- 如果状态是无状态的(即所有实例共用逻辑),可以像我上文示例中那样放在外部常量中。
- 如果每个状态需要存储不同的私有数据,则应该在
Context实例化时为每个状态创建独立实例。