讲解
红绿灯不用思考「我现在该亮什么灯」——红灯亮完换绿灯,绿灯亮完换黄灯,每种灯只管好自己的时长和「下一个是谁」。状态模式把这种「对象在不同状态下行为不同」的问题对象化:允许一个对象在其内部状态改变时改变它的行为,对象看起来似乎修改了它的类。
没有用状态模式时,状态机长什么样大家都熟:一个 status 字段 + 每个方法里一长串 if (status === 'paid') { ... } else if (status === 'shipped') { ... }。三个状态九个分支还能忍,七个状态、每个状态四个操作,就是一张 28 格的隐式表格摊在 if-else 里——加新状态要翻出所有方法逐个检查,漏一处就是一个线上事故。
状态模式把这张表格「竖着切」:每个状态一个类(待支付、已支付、已发货……),状态类实现同一套操作接口(pay、ship、cancel),自己声明「我这个状态下每个操作该干什么、转移到谁」。上下文对象(订单)只持有当前状态对象并转发调用。新增一个「退款中」状态 = 新增一个类,编译器会逼着你实现全部操作——if-else 表格里漏格子的问题被类型系统消灭了。
和策略模式结构几乎相同(都是上下文 + 多态实现),区分仍在意图:策略由客户端选择、一次选定很少换(选个折扣算法);状态由对象内部驱动、随运行不断流转,而且状态类知道彼此(待支付知道支付成功该去已支付)。一个是「我选算法」,一个是「我的阶段推着算法换」。
示例
订单状态机:非法操作直接抛错,合法操作自动流转,没有任何一个 if-else 分支:
import assert from 'node:assert/strict';
// 状态接口:每种状态回答「这个操作在我这儿意味着什么」
interface OrderState {
readonly name: string;
pay(order: Order): void;
ship(order: Order): void;
}
class PendingState implements OrderState {
readonly name = '待支付';
pay(order: Order): void {
order.transitionTo(new PaidState());
}
ship(): void {
throw new Error('未支付,不能发货');
}
}
class PaidState implements OrderState {
readonly name = '已支付';
pay(): void {
throw new Error('不能重复支付');
}
ship(order: Order): void {
order.transitionTo(new ShippedState());
}
}
class ShippedState implements OrderState {
readonly name = '已发货';
pay(): void {
throw new Error('已发货订单不能再支付');
}
ship(): void {
throw new Error('订单已发货,请勿重复发货');
}
}
// 上下文:只转发,不做状态判断
class Order {
private state: OrderState = new PendingState();
transitionTo(state: OrderState): void {
this.state = state;
}
status(): string {
return this.state.name;
}
pay(): void {
this.state.pay(this);
}
ship(): void {
this.state.ship(this);
}
}
const order = new Order();
assert.equal(order.status(), '待支付');
assert.throws(() => order.ship(), /未支付/); // 非法操作被当前状态拦下
order.pay();
assert.equal(order.status(), '已支付');
assert.throws(() => order.pay(), /不能重复支付/);
order.ship();
assert.equal(order.status(), '已发货');
console.log('订单流转:待支付 -> 已支付 -> ' + order.status());
Order 类里没有任何一处 if (status === ...)——它只转发。「未支付不能发货」「已支付不能重复支付」这些规则各自长在最相关的状态类里,读规则时不用在几百行 if-else 里翻。新增「退款中」状态时,新写一个类 + 在 PaidState 里加一条转移路径,编译器会检查新类实现了全部操作。
常见坑
- 状态类里塞共享数据:状态流转中产生的数据(支付流水号、发货时间)属于订单上下文,塞进状态类会随状态对象销毁而丢失。
- 状态对象间直接互相 new 导致循环依赖:复杂状态机里状态互相引用会绕晕;可以用状态注册表(按名字取实例)解耦。
- 转移规则散在状态外:又在 Order 里写 if 判断「什么状态允许什么操作」,模式就退化了——规则必须长在状态类里。
- 两个状态也用状态模式:status 只有 true/false 且永不扩展时,一个布尔字段加两行 if 更诚实;模式为「状态多、操作多、会扩展」准备。
- 忘记持久化状态:订单重启后从数据库恢复,要记得把 status 字段重建回对应的状态对象(按名字映射),而不是永远从初始状态开始。
小结
状态模式把状态转移表竖切成状态类,上下文只转发,新增状态由编译器护航;与策略模式同构不同图——策略是客户端选算法,状态是内部驱动流转。下一章正好讲策略模式,对比着看。