讲解

红绿灯不用思考「我现在该亮什么灯」——红灯亮完换绿灯,绿灯亮完换黄灯,每种灯只管好自己的时长和「下一个是谁」。状态模式把这种「对象在不同状态下行为不同」的问题对象化:允许一个对象在其内部状态改变时改变它的行为,对象看起来似乎修改了它的类。

没有用状态模式时,状态机长什么样大家都熟:一个 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 字段重建回对应的状态对象(按名字映射),而不是永远从初始状态开始。

小结

状态模式把状态转移表竖切成状态类,上下文只转发,新增状态由编译器护航;与策略模式同构不同图——策略是客户端选算法,状态是内部驱动流转。下一章正好讲策略模式,对比着看。