跳转到内容

第 21 章 · 状态(State)

一句话:对象有几种状态,每种状态写成一个类,对象把操作交给当前状态去做,由状态决定下一步变成谁。 解决的痛:每个方法里都有一串 if status == …,加一个状态要把所有方法翻一遍;漏判一处,那个方法就按“其余情况”处理,不报错,直到出事。

你在电商公司做订单系统。一个订单从下单到收货要经过几种状态(state):

  • 待支付:刚下单,买家还没付钱,可以付款,也可以取消;
  • 已支付:可以发货;买家反悔了,也可以退款;
  • 已发货:等买家确认收货;
  • 已完成、已取消、已退款:订单结束,什么操作都不能再做。

订单上有五个操作:pay(付款)、ship(发货)、confirm(确认收货)、cancel(取消)、refund(退款)。同一个操作,在不同状态下结果不同:已支付的订单能发货,待支付的不能;待支付的能取消,已发货的不能。不允许的操作要报错,不能悄悄执行。

客服查问题时最常问“这个订单经历了哪些状态”,所以订单上还记一条状态轨迹 trail,每变一次状态追加一项。本章不连数据库,订单就是内存里的对象,金额以元为单位。

状态用一个字符串字段 status 存,每个操作先看当前状态,再决定做什么:

from collections.abc import Callable
class OrderError(Exception):
pass
class Order:
def __init__(self, order_id: str, amount: int) -> None:
self.order_id = order_id
self.amount = amount # 订单金额(元)
self.status = "待支付"
self.trail = [self.status] # 状态轨迹,方便查问题
def _go(self, status: str) -> None:
self.status = status
self.trail.append(status)
def pay(self) -> None:
if self.status == "待支付":
self._go("已支付")
else:
raise OrderError(f"{self.status}的订单不能支付")
def ship(self) -> None:
if self.status == "待支付":
raise OrderError("还没付款,不能发货")
if self.status in ("已发货", "已完成"):
raise OrderError("已经发过货了")
if self.status in ("已取消", "已退款"):
raise OrderError("订单已关闭,不能发货")
self._go("已发货")
def confirm(self) -> None: # 买家确认收货
if self.status == "已发货":
self._go("已完成")
else:
raise OrderError(f"{self.status}的订单不能确认收货")
def cancel(self) -> None:
if self.status == "待支付":
self._go("已取消")
else:
raise OrderError(f"{self.status}的订单不能取消")
def refund(self) -> None: # 整单退款
if self.status == "已支付":
self._go("已退款")
else:
raise OrderError(f"{self.status}的订单不能退款")
def show(self) -> str:
return f"{self.order_id}:{' → '.join(self.trail)}"
def attempt(order: Order, label: str, action: Callable[[], None]) -> None:
try:
action()
except OrderError as e:
print(f"{order.order_id} {label}被拒:{e}")
if __name__ == "__main__":
a = Order("A001", 100)
a.pay()
a.ship()
a.confirm()
b = Order("B002", 80)
b.pay()
b.refund()
c = Order("C003", 60)
attempt(c, "发货", c.ship)
for o in (a, b, c):
print(o.show())
# 输出:
# C003 发货被拒:还没付款,不能发货
# A001:待支付 → 已支付 → 已发货 → 已完成
# B002:待支付 → 已支付 → 已退款
# C003:待支付

TS 版结构一样:

class OrderError extends Error {}
class Order {
status = "待支付";
trail: string[] = [this.status]; // 状态轨迹,方便查问题
constructor(readonly orderId: string, readonly amount: number) {} // amount:订单金额(元)
private go(status: string): void {
this.status = status;
this.trail.push(status);
}
pay(): void {
if (this.status === "待支付") {
this.go("已支付");
} else {
throw new OrderError(`${this.status}的订单不能支付`);
}
}
ship(): void {
if (this.status === "待支付") throw new OrderError("还没付款,不能发货");
if (["已发货", "已完成"].includes(this.status)) throw new OrderError("已经发过货了");
if (["已取消", "已退款"].includes(this.status)) throw new OrderError("订单已关闭,不能发货");
this.go("已发货");
}
confirm(): void { // 买家确认收货
if (this.status === "已发货") {
this.go("已完成");
} else {
throw new OrderError(`${this.status}的订单不能确认收货`);
}
}
cancel(): void {
if (this.status === "待支付") {
this.go("已取消");
} else {
throw new OrderError(`${this.status}的订单不能取消`);
}
}
refund(): void { // 整单退款
if (this.status === "已支付") {
this.go("已退款");
} else {
throw new OrderError(`${this.status}的订单不能退款`);
}
}
show(): string {
return `${this.orderId}:${this.trail.join(" → ")}`;
}
}
function attempt(order: Order, label: string, action: () => void): void {
try {
action();
} catch (e) {
if (!(e instanceof OrderError)) throw e;
console.log(`${order.orderId} ${label}被拒:${e.message}`);
}
}
const a = new Order("A001", 100);
a.pay();
a.ship();
a.confirm();
const b = new Order("B002", 80);
b.pay();
b.refund();
const c = new Order("C003", 60);
attempt(c, "发货", () => c.ship());
for (const o of [a, b, c]) console.log(o.show());
// 输出:
// C003 发货被拒:还没付款,不能发货
// A001:待支付 → 已支付 → 已发货 → 已完成
// B002:待支付 → 已支付 → 已退款
// C003:待支付

规则都写在各自的方法里:想知道“什么时候能发货”,打开 ship() 一看就知道。只有四五个状态、规则也不常变的时候,这样写清楚直接,大多数人第一版都这么写。

运营要做两件事:一是预售,预售商品下单时先付定金,到发售日再付尾款,付完尾款才能发货;二是部分退款,一个订单买了几件,缺货一件就先退那一件的钱,剩下的照常发货,退完全款才算已退款。

这带来三个新状态:待付定金、已付定金、部分退款。Order 构造时多一个 presale 参数,预售订单从“待付定金”开始;再加一个 refunded 字段记已退金额。五个方法里要改的有四个:

  1. pay():加两个分支,待付定金付款后变成已付定金,已付定金再付款(付尾款)变成已支付。
  2. ship():原来的写法是“列出不能发货的状态,其余一律发货”,新状态要逐个判断该不该拦。
  3. cancel():待付定金、已付定金也能取消,加进条件里。
  4. refund():加参数 amount,已支付和部分退款都能退,退完全款变成已退款,没退完变成部分退款。

每加一个状态,五个方法都得过一遍:“这个状态下能不能做这件事、做完变成什么”。规则散在五个方法里,没有一个地方能看全某个状态能做什么。

小王改 ship() 时,照着“待支付不能发货”那条,加了一条“待付定金不能发货”,没想到已付定金的订单也还没付尾款。改完的四处是这样(confirm、show、attempt 和 21.2 相同):

def __init__(self, order_id: str, amount: int, presale: bool = False) -> None:
self.order_id = order_id
self.amount = amount # 订单金额(元)
self.refunded = 0 # 已退金额
self.status = "待付定金" if presale else "待支付" # 预售订单先付定金
self.trail = [self.status] # 状态轨迹,方便查问题
def pay(self) -> None:
if self.status == "待支付":
self._go("已支付")
elif self.status == "待付定金": # ① 付定金
self._go("已付定金")
elif self.status == "已付定金": # ① 付尾款
self._go("已支付")
else:
raise OrderError(f"{self.status}的订单不能支付")
def ship(self) -> None:
if self.status == "待支付":
raise OrderError("还没付款,不能发货")
if self.status == "待付定金": # ② 照着上一条加的
raise OrderError("还没付定金,不能发货")
if self.status in ("已发货", "已完成"):
raise OrderError("已经发过货了")
if self.status in ("已取消", "已退款"):
raise OrderError("订单已关闭,不能发货")
self._go("已发货")
def cancel(self) -> None:
if self.status in ("待支付", "待付定金", "已付定金"): # ③ 加两种状态
self._go("已取消")
else:
raise OrderError(f"{self.status}的订单不能取消")
def refund(self, amount: int) -> None: # ④ 加参数:退多少
if self.status in ("已支付", "部分退款"): # ④ 部分退款后还能再退
self.refunded += amount
self._go("已退款" if self.refunded >= self.amount else "部分退款")
else:
raise OrderError(f"{self.status}的订单不能退款")

小王测了“部分退款后发货”和“没付定金就发货”,结果都对。把上面四处换进 before.py,入口换成下面这段,最后一个订单是上线后出事的那一单:

if __name__ == "__main__":
d = Order("D004", 100)
d.pay()
d.refund(30) # 缺货一件,先退 30
d.ship() # 剩下的照常发
q = Order("P005", 300, presale=True)
attempt(q, "发货", q.ship) # 没付定金:拦住了
p = Order("P006", 300, presale=True)
p.pay() # 付了定金
p.ship() # 尾款还没付
for o in (d, q, p):
print(o.show())

运行结果如下(TS 版改的是同样的地方,输出相同):

P005 发货被拒:还没付定金,不能发货
D004:待支付 → 已支付 → 部分退款 → 已发货
P005:待付定金
P006:待付定金 → 已付定金 → 已发货

P006 只付了定金,ship() 的四条拦截都没命中,走到最后一行,货发出去了,尾款还没收。程序没有报错,轨迹里“已付定金”后面直接接着“已发货”,要等财务对账才发现。D004 能在部分退款后发货也不是谁想清楚了,是同一行“其余一律发货”碰巧放行了它。

问题的形状是:每加一个状态,每个方法都要重新判断一遍;漏判一处不报错,那个状态就按“其余情况”走。

直觉写法:五个方法各一串 if,ship 漏判了已付定金

图中 ①–④ 就是上面四处,浅红是被改动的方法,红色加粗的框是那次事故:已付定金的订单落进了 ship() 的“其余一律发货”。

换个思路:不在每个方法里问“现在是什么状态”,而是让每个状态自己回答“在我这里,这个操作能不能做、做完变成什么”。

基类 OrderState 给五个操作都写好默认实现:报错“某某状态的订单不能某某”。每个状态一个类,只覆写本状态允许的操作,做完事调 order.go(下一个状态)。Order 不再判断状态,只持有当前状态对象,把每个操作原样交给它:

from __future__ import annotations
from abc import ABC
from collections.abc import Callable
class OrderError(Exception):
pass
class OrderState(ABC): # 每个操作默认都"不允许",状态类只覆写自己允许的
name = ""
def pay(self, order: Order) -> None:
raise OrderError(f"{self.name}的订单不能支付")
def ship(self, order: Order) -> None:
raise OrderError(f"{self.name}的订单不能发货")
def confirm(self, order: Order) -> None:
raise OrderError(f"{self.name}的订单不能确认收货")
def cancel(self, order: Order) -> None:
raise OrderError(f"{self.name}的订单不能取消")
def refund(self, order: Order, amount: int) -> None:
raise OrderError(f"{self.name}的订单不能退款")
class PendingPayment(OrderState):
name = "待支付"
def pay(self, order: Order) -> None:
order.go(Paid())
def cancel(self, order: Order) -> None:
order.go(Cancelled())
class AwaitingDeposit(OrderState): # 预售:先付定金
name = "待付定金"
def pay(self, order: Order) -> None:
order.go(DepositPaid())
def cancel(self, order: Order) -> None:
order.go(Cancelled())
class DepositPaid(OrderState): # 预售:已付定金,等尾款;没有覆写 ship
name = "已付定金"
def pay(self, order: Order) -> None: # 付尾款
order.go(Paid())
def cancel(self, order: Order) -> None:
order.go(Cancelled())
def refund_part(order: Order, amount: int) -> None: # 已支付、部分退款共用
order.refunded += amount
order.go(Refunded() if order.refunded >= order.amount else PartiallyRefunded())
class Paid(OrderState):
name = "已支付"
def ship(self, order: Order) -> None:
order.go(Shipped())
def refund(self, order: Order, amount: int) -> None:
refund_part(order, amount)
class PartiallyRefunded(OrderState): # 退了一部分,剩下的照常发货
name = "部分退款"
def ship(self, order: Order) -> None:
order.go(Shipped())
def refund(self, order: Order, amount: int) -> None:
refund_part(order, amount)
class Shipped(OrderState):
name = "已发货"
def confirm(self, order: Order) -> None:
order.go(Completed())
class Completed(OrderState):
name = "已完成"
class Cancelled(OrderState):
name = "已取消"
class Refunded(OrderState):
name = "已退款"
class Order:
def __init__(self, order_id: str, amount: int, presale: bool = False) -> None:
self.order_id = order_id
self.amount = amount # 订单金额(元)
self.refunded = 0 # 已退金额
self.state: OrderState = AwaitingDeposit() if presale else PendingPayment()
self.trail = [self.state.name]
def go(self, state: OrderState) -> None: # 只由状态类调用
self.state = state
self.trail.append(state.name)
def pay(self) -> None:
self.state.pay(self)
def ship(self) -> None:
self.state.ship(self)
def confirm(self) -> None:
self.state.confirm(self)
def cancel(self) -> None:
self.state.cancel(self)
def refund(self, amount: int) -> None:
self.state.refund(self, amount)
def show(self) -> str:
return f"{self.order_id}:{' → '.join(self.trail)}"
def attempt(order: Order, label: str, action: Callable[[], None]) -> None:
try:
action()
except OrderError as e:
print(f"{order.order_id} {label}被拒:{e}")
if __name__ == "__main__":
p = Order("P006", 300, presale=True)
p.pay() # 付了定金
attempt(p, "发货", p.ship) # 尾款还没付:拒绝
p.pay() # 付尾款
p.ship()
d = Order("D004", 100)
d.pay()
d.refund(30)
d.ship()
d.confirm()
attempt(d, "退款", lambda: d.refund(70))
e = Order("E007", 100)
e.pay()
e.refund(30)
e.refund(70)
for o in (p, d, e):
print(o.show())
# 输出:
# P006 发货被拒:已付定金的订单不能发货
# D004 退款被拒:已完成的订单不能退款
# P006:待付定金 → 已付定金 → 已支付 → 已发货
# D004:待支付 → 已支付 → 部分退款 → 已发货 → 已完成
# E007:待支付 → 已支付 → 部分退款 → 已退款

TS 版结构相同:

class OrderError extends Error {}
abstract class OrderState { // 每个操作默认都"不允许",状态类只覆写自己允许的
abstract readonly name: string;
pay(_order: Order): void {
throw new OrderError(`${this.name}的订单不能支付`);
}
ship(_order: Order): void {
throw new OrderError(`${this.name}的订单不能发货`);
}
confirm(_order: Order): void {
throw new OrderError(`${this.name}的订单不能确认收货`);
}
cancel(_order: Order): void {
throw new OrderError(`${this.name}的订单不能取消`);
}
refund(_order: Order, _amount: number): void {
throw new OrderError(`${this.name}的订单不能退款`);
}
}
class PendingPayment extends OrderState {
readonly name = "待支付";
override pay(order: Order): void {
order.go(new Paid());
}
override cancel(order: Order): void {
order.go(new Cancelled());
}
}
class AwaitingDeposit extends OrderState { // 预售:先付定金
readonly name = "待付定金";
override pay(order: Order): void {
order.go(new DepositPaid());
}
override cancel(order: Order): void {
order.go(new Cancelled());
}
}
class DepositPaid extends OrderState { // 预售:已付定金,等尾款;没有覆写 ship
readonly name = "已付定金";
override pay(order: Order): void { // 付尾款
order.go(new Paid());
}
override cancel(order: Order): void {
order.go(new Cancelled());
}
}
function refundPart(order: Order, amount: number): void { // 已支付、部分退款共用
order.refunded += amount;
order.go(order.refunded >= order.amount ? new Refunded() : new PartiallyRefunded());
}
class Paid extends OrderState {
readonly name = "已支付";
override ship(order: Order): void {
order.go(new Shipped());
}
override refund(order: Order, amount: number): void {
refundPart(order, amount);
}
}
class PartiallyRefunded extends OrderState { // 退了一部分,剩下的照常发货
readonly name = "部分退款";
override ship(order: Order): void {
order.go(new Shipped());
}
override refund(order: Order, amount: number): void {
refundPart(order, amount);
}
}
class Shipped extends OrderState {
readonly name = "已发货";
override confirm(order: Order): void {
order.go(new Completed());
}
}
class Completed extends OrderState {
readonly name = "已完成";
}
class Cancelled extends OrderState {
readonly name = "已取消";
}
class Refunded extends OrderState {
readonly name = "已退款";
}
class Order {
refunded = 0; // 已退金额
state: OrderState;
trail: string[];
constructor(readonly orderId: string, readonly amount: number, presale = false) { // amount:订单金额(元)
this.state = presale ? new AwaitingDeposit() : new PendingPayment();
this.trail = [this.state.name];
}
go(state: OrderState): void { // 只由状态类调用
this.state = state;
this.trail.push(state.name);
}
pay(): void {
this.state.pay(this);
}
ship(): void {
this.state.ship(this);
}
confirm(): void {
this.state.confirm(this);
}
cancel(): void {
this.state.cancel(this);
}
refund(amount: number): void {
this.state.refund(this, amount);
}
show(): string {
return `${this.orderId}:${this.trail.join(" → ")}`;
}
}
function attempt(order: Order, label: string, action: () => void): void {
try {
action();
} catch (e) {
if (!(e instanceof OrderError)) throw e;
console.log(`${order.orderId} ${label}被拒:${e.message}`);
}
}
const p = new Order("P006", 300, true);
p.pay(); // 付了定金
attempt(p, "发货", () => p.ship()); // 尾款还没付:拒绝
p.pay(); // 付尾款
p.ship();
const d = new Order("D004", 100);
d.pay();
d.refund(30);
d.ship();
d.confirm();
attempt(d, "退款", () => d.refund(70));
const e = new Order("E007", 100);
e.pay();
e.refund(30);
e.refund(70);
for (const o of [p, d, e]) console.log(o.show());
// 输出:
// P006 发货被拒:已付定金的订单不能发货
// D004 退款被拒:已完成的订单不能退款
// P006:待付定金 → 已付定金 → 已支付 → 已发货
// D004:待支付 → 已支付 → 部分退款 → 已发货 → 已完成
// E007:待支付 → 已支付 → 部分退款 → 已退款

状态模式版:Order 把操作交给当前状态,每个状态一个类

看黄色的三个类:预售和部分退款加的就是这三个状态类。已有的状态类里,只有转到新状态的地方要动,浅红的 Paid:它的 refund 改成调 refund_part,才能转到部分退款。④ 给退款加了 amount 参数,这是接口变了,Order.refund、OrderState.refund、Paid.refund 都跟着改;Order 构造时还要按 presale 选初始状态。PendingPayment、Shipped 等其余状态类一行没动。DepositPaid 没有覆写 ship(),调用时走到基类的默认实现,报错。

逐条对照 21.3:

  • ① 付定金、付尾款:写在 AwaitingDeposit.pay 和 DepositPaid.pay 里,PendingPayment.pay 不用动。
  • ② 发货:哪个状态能发货,就在哪个类里写 ship,现在只有 Paid 和 PartiallyRefunded 两个。新状态默认不能发货,要放行得自己写,不会因为“不在拦截名单里”就被放过去。
  • ③ 取消:三个能取消的状态各写一个 cancel,其余默认报错。
  • ④ 退款:Paid 和 PartiallyRefunded 共用 refund_part,退完还是没退完,在这一个函数里决定。
  • 事故:P006 付了定金就发货,报“已付定金的订单不能发货”;付完尾款才发得出去。
  • 再加一个状态,比如“退款审核中”:加一个类,覆写它允许的操作,再在转到它的那个状态类里写一行 order.go(...)。

有三件事要说清楚:

  • 类变多了。九个状态九个类,其中三个只有一个名字。不算空行和注释,比 21.3 改完的直觉写法多出一半以上。
  • 转换规则也散了。“已支付之后能变成什么”写在 Paid 里;“哪些状态能变成已取消”,要翻 PendingPayment、AwaitingDeposit、DepositPaid 三个类才知道。单个状态能做什么一目了然,整个流程反而看不全。流程一复杂,就要配一张状态图,像下面这张。
  • 状态图要跟着代码改。图是另外画的,代码里加了一条转换而图没改,图就会误导人。评审时对着图逐条核对代码,笨,但有效。

订单的状态图:九个状态和全部转换,黄色是新加的

图里每一条箭头对应某个状态类里的一行 order.go(...),没有箭头的操作就是默认报错。可以拿它和上面的代码一条条对:比如从“已付定金”出去只有 pay 和 cancel 两条,没有 ship。

状态模式换来的是新状态默认什么都不能做,允许什么要明写出来,加一个状态就是加一个类。代价是类多、流程散在各个类里,要靠状态图把它拼回来。

GoF 对状态的定义是:允许一个对象在其内部状态改变时改变它的行为,对象看起来似乎修改了它的类。

换成本章的话:同一个 order.ship(),订单已支付时就发货,已付定金时就报错,像是换了一个类,其实只是 order.state 指向的对象换了。“内部状态改变”由状态类自己触发:Paid.ship 做完事,把订单切到 Shipped。

状态的结构:Context 把请求交给当前 State,State 决定下一个是谁

看 ConcreteStateA 指向 ConcreteStateB 的那条虚线:具体状态知道下一个状态是谁。和第 17 章策略的结构图相比,不同在两处:没有 Client 来挑状态,多了 A 指向 B 的这条虚线。

角色 在本章案例里是谁 职责
上下文(Context) Order 对外提供操作,持有当前状态,把操作交给它;提供 go() 供状态切换
状态(State) OrderState 声明每个状态都要回应的操作,本章还给了“不允许”的默认实现
具体状态(ConcreteState) PendingPayment、Paid、DepositPaid 等九个类 实现本状态允许的操作,决定转到哪个状态

常见场景:

  • 订单、工单、审批这类业务流程,状态多,每个状态能做的事不同。
  • 网络连接和协议。连接在“空闲、请求中、等响应”或“连接中、已连接、关闭中”之间切换,每个阶段能调的方法不同,21.6 的两个例子都是。
  • 游戏角色。站立、奔跑、跳跃、受伤时,按同一个键效果不同。
  • 界面组件的加载状态。加载中、成功、失败、空数据,各自显示什么、点按钮做什么都不同。

还有一种常见写法叫表驱动的状态机(table-driven state machine):把“当前状态加操作得到下一个状态”写成一张转换表,再配几行查表的代码:

TRANSITIONS = { # (当前状态, 操作) → 下一个状态
("待支付", "pay"): "已支付",
("待支付", "cancel"): "已取消",
("待付定金", "pay"): "已付定金",
("已付定金", "pay"): "已支付",
("已支付", "ship"): "已发货",
("已发货", "confirm"): "已完成",
# ……其余各行
}
def fire(order: Order, action: str) -> None:
nxt = TRANSITIONS.get((order.status, action))
if nxt is None: # 表里没有的组合,就是不允许
raise OrderError(f"{order.status}的订单不能{action}")
order.status = nxt

表里没有的组合就是不允许,这一点和状态模式一样安全;整个流程在一张表里,比翻九个类好看全,表也能存进配置文件。它适合规则多、每一步只是“换个状态”的时候。一旦某一步要做的事各不相同,比如退款要累计已退金额,再按退没退完去不同的状态,一行表就放不下了,得给表加“条件”列、“动作”列,越加越像一门小语言。这时每个状态一个类更直接。

例一:http.client 的 HTTPConnection(Python)

Section titled “例一:http.client 的 HTTPConnection(Python)”

Python 标准库的 http.client.HTTPConnection 发一个请求分几步:putrequest() 写请求行,putheader() 逐个写请求头,endheaders() 把请求发出去,getresponse() 读响应。顺序不能乱:请求行没写就不能写请求头,上一个请求的响应没读就不能开始下一个。它内部用一个字段记着连接走到了哪一步,取值是 _CS_IDLE、_CS_REQ_STARTED、_CS_REQ_SENT 三个字符串常量;每个方法开头检查它,顺序错了抛 CannotSendRequest 或 CannotSendHeader,读响应太早抛 ResponseNotReady,大致如此。

真实代码用的是 21.2 那种写法:一个字段加几个 if。下面把它改写成状态对象,异常直接用标准库里的那几个类;后半段用真的 HTTPConnection 按同样的顺序调前四步,putrequest 和 putheader 只往缓冲区里写,不会连网:

from __future__ import annotations
import http.client
from http.client import CannotSendHeader, CannotSendRequest, ResponseNotReady
class ConnState: # 模拟 HTTPConnection 的三个内部状态;真实代码用字符串加 if,这里改成状态对象
name = ""
def putrequest(self, conn: MiniConnection) -> None:
raise CannotSendRequest(self.name)
def putheader(self, conn: MiniConnection) -> None:
raise CannotSendHeader()
def endheaders(self, conn: MiniConnection) -> None:
raise CannotSendHeader()
def getresponse(self, conn: MiniConnection) -> None:
raise ResponseNotReady(self.name)
class Idle(ConnState): # 对应 _CS_IDLE
name = "Idle"
def putrequest(self, conn: MiniConnection) -> None:
conn.state = RequestStarted()
class RequestStarted(ConnState): # 对应 _CS_REQ_STARTED:可以继续加请求头
name = "Request-started"
def putheader(self, conn: MiniConnection) -> None:
pass # 状态不变
def endheaders(self, conn: MiniConnection) -> None: # 真实代码在这里才连网、发出请求
conn.state = RequestSent()
class RequestSent(ConnState): # 对应 _CS_REQ_SENT:只能等响应
name = "Request-sent"
def getresponse(self, conn: MiniConnection) -> None:
conn.state = Idle() # 读完响应,连接可以发下一个请求
class MiniConnection:
def __init__(self) -> None:
self.state: ConnState = Idle()
def call(self, method: str) -> str:
try:
getattr(self.state, method)(self)
return f"{method}:ok → {self.state.name}"
except http.client.ImproperConnectionState as e:
return f"{method}:{type(e).__name__}({e})"
if __name__ == "__main__":
conn = MiniConnection()
for m in ["putheader", "putrequest", "putheader", "putrequest", "endheaders", "getresponse"]:
print(conn.call(m))
real = http.client.HTTPConnection("example.com") # 真的 HTTPConnection:下面四步只写缓冲区,不连网
steps = {"putheader": lambda: real.putheader("Accept", "*/*"), "putrequest": lambda: real.putrequest("GET", "/")}
for m in ["putheader", "putrequest", "putheader", "putrequest"]:
try:
steps[m]()
print(f"真实 {m}:ok")
except http.client.ImproperConnectionState as e:
print(f"真实 {m}:{type(e).__name__}({e})")
# 输出:
# putheader:CannotSendHeader()
# putrequest:ok → Request-started
# putheader:ok → Request-started
# putrequest:CannotSendRequest(Request-started)
# endheaders:ok → Request-sent
# getresponse:ok → Idle
# 真实 putheader:CannotSendHeader()
# 真实 putrequest:ok
# 真实 putheader:ok
# 真实 putrequest:CannotSendRequest(Request-started)

MiniConnection 是上下文,Idle、RequestStarted、RequestSent 是具体状态,对应本章的 Order 和 Paid 这些类;基类默认抛异常,对应 OrderState 的“不允许”。最后四行是真实 HTTPConnection 的输出,和模拟版前四行的结果一致。

例二:浏览器的 WebSocket(TypeScript)

Section titled “例二:浏览器的 WebSocket(TypeScript)”

浏览器的 WebSocket 有个只读属性 readyState,取值是 CONNECTING(0)、OPEN(1)、CLOSING(2)、CLOSED(3)。新建的连接先是 CONNECTING,握手完成变成 OPEN;调 close() 后进入 CLOSING,关闭完成变成 CLOSED。send() 的行为随状态不同:CONNECTING 时调用会抛出名为 InvalidStateError 的 DOMException;OPEN 时正常发送;CLOSING、CLOSED 时不报错,数据被丢弃,只是 bufferedAmount 会增加,大致如此。下面用状态对象模拟它,网络事件用直接替换状态来代替:

class InvalidStateError extends Error {} // 浏览器里是 name 为 "InvalidStateError" 的 DOMException
abstract class SocketState { // 模拟 WebSocket 的四个 readyState
abstract readonly readyState: number;
abstract readonly label: string;
send(_ws: MiniWebSocket, data: string): void { // OPEN 以外的默认行为:CLOSING、CLOSED 时静默丢弃
console.log(`${this.label}:丢弃 ${JSON.stringify(data)}`);
}
close(_ws: MiniWebSocket): void {} // CLOSING、CLOSED 时再调 close 什么也不做
}
class Connecting extends SocketState {
readonly readyState = 0;
readonly label = "CONNECTING";
override send(): void {
throw new InvalidStateError("Still in CONNECTING state.");
}
override close(ws: MiniWebSocket): void { // 握手没完成就关:放弃握手
ws.state = new Closing();
}
}
class Open extends SocketState {
readonly readyState = 1;
readonly label = "OPEN";
override send(_ws: MiniWebSocket, data: string): void {
console.log(`OPEN:发出 ${JSON.stringify(data)}`);
}
override close(ws: MiniWebSocket): void {
ws.state = new Closing();
}
}
class Closing extends SocketState {
readonly readyState = 2;
readonly label = "CLOSING";
}
class Closed extends SocketState {
readonly readyState = 3;
readonly label = "CLOSED";
}
class MiniWebSocket {
state: SocketState = new Connecting();
get readyState(): number {
return this.state.readyState;
}
send(data: string): void {
this.state.send(this, data);
}
close(): void {
this.state.close(this);
}
}
const ws = new MiniWebSocket();
try {
ws.send("hello"); // 还在握手
} catch (e) {
if (!(e instanceof InvalidStateError)) throw e;
console.log(`readyState ${ws.readyState}:InvalidStateError(${e.message})`);
}
ws.state = new Open(); // 模拟网络事件:握手完成
ws.send("hello");
ws.close();
ws.send("bye"); // 正在关闭
ws.state = new Closed(); // 模拟网络事件:关闭握手完成
console.log(`readyState ${ws.readyState}`);
// 输出:
// readyState 0:InvalidStateError(Still in CONNECTING state.)
// OPEN:发出 "hello"
// CLOSING:丢弃 "bye"
// readyState 3

MiniWebSocket 是上下文,四个状态类各管自己的 send() 和 close()。Closing、Closed 没有覆写任何方法,用的都是基类的默认行为,和本章三个终结状态只有一个名字一样。

TS 里还可以不写类,用可辨识联合(discriminated union)表示状态,让编译器检查每种状态都处理到了,第 29 章再讲。

  • 只有两三个状态,行为差别小。比如商品只有“上架、下架”两个状态,只有一个方法关心它。拆成状态类,一个 if 变成一个基类加两个子类,读代码的人要多翻两三个类。例一里真实的 HTTPConnection 也没拆:三个状态,这几个状态多年来基本没变,一个字段加几个 if 就够。
  • 转换规则常由运营配置。比如审批流“金额超过一万要多一级审批”,下个月可能改成五千,或者某个部门再多一级。每次调整都要改状态类、发版本,运营等不起。这时把转换写成 21.5 那样的表,存进数据库或配置文件;规则再复杂(会签、超时自动流转),就用现成的工作流引擎(workflow engine)。
模式 意图 区别
状态(State) 对象的行为随内部状态切换 对象自己换:状态类做完事把上下文切到下一个状态,各个状态知道谁接在后面
策略(Strategy) 把一族可互换的算法各自封装 结构几乎一样,但由调用方挑好交给上下文,策略之间互不认识。见第 17 章
表驱动的状态机 用一张“状态加操作得到下一个状态”的表描述流程 转换集中在一张表里,好看全、好配置;每一步只能做简单的事,复杂动作还得写代码

两组都能用一个问题当场判断:

  • 状态还是策略:问“谁来换”。这和第 17 章 17.9 用的是同一个问题。本章的 Paid.ship 做完事写着 order.go(Shipped()),是状态自己把订单换到下一个,Paid 知道下一个是 Shipped,这是状态。第 17 章给 Checkout 换优惠,是外面的 pick_strategy 挑好交进去,FullReduction 里从不提别的策略,那是策略。
  • 状态类还是转换表:问“每一步除了换状态,还要做多少事”。如果每一步只是换个状态,最多记一行日志,写成表,一眼看全。如果像退款那样要算金额、按结果去不同的状态,或者同一个操作在各个状态下做的事完全不同,写成状态类。两者也能混用:表只管“能不能转、转到哪”,每个状态要做的事另写成函数。

客服工单 Ticket:新建的工单开始处理后变成处理中;处理中的可以挂起(等用户补充信息),挂起的可以恢复成处理中;处理中的处理完提交确认,变成待确认;待确认的可以关闭。其他组合都不允许,要报错,比如新建的不能直接关闭,挂起的不能提交确认,关闭的不能再开始处理。

写出 Ticket 和各个状态类,操作叫 start、suspend、resume、resolve、close。依次执行“关闭、开始、挂起、提交确认、恢复、提交确认、关闭、开始”,打印被拒的操作和最后的状态轨迹。Python 和 TS 各写一版。

提示:先画状态图,五个状态、五条箭头;再照着图写类,基类给每个操作写好“不允许”,每条箭头在它出发的那个状态类里写成一个方法。

  • 状态模式把每个状态写成一个类,上下文把操作交给当前状态,由状态决定做什么、下一个是谁。
  • 基类默认“不允许”,新状态允许什么要明写;不会再有“不在拦截名单里就放行”的事故。
  • 加一个状态就是加一个类,已有的类只在转到它的地方加一行;代价是类多、转换散在各类里,复杂流程要配一张状态图。
  • 状态少、行为差别小,一个字段加 if 就够;规则多而动作简单,或者要让运营配置,用表驱动的状态机。
  • 和策略结构一样,区别是谁来换:调用方挑的是策略,对象自己切的是状态。