第 21 章 · 状态(State)
一句话:对象有几种状态,每种状态写成一个类,对象把操作交给当前状态去做,由状态决定下一步变成谁。 解决的痛:每个方法里都有一串
if status == …,加一个状态要把所有方法翻一遍;漏判一处,那个方法就按“其余情况”处理,不报错,直到出事。
21.1 一个场景
Section titled “21.1 一个场景”你在电商公司做订单系统。一个订单从下单到收货要经过几种状态(state):
- 待支付:刚下单,买家还没付钱,可以付款,也可以取消;
- 已支付:可以发货;买家反悔了,也可以退款;
- 已发货:等买家确认收货;
- 已完成、已取消、已退款:订单结束,什么操作都不能再做。
订单上有五个操作:pay(付款)、ship(发货)、confirm(确认收货)、cancel(取消)、refund(退款)。同一个操作,在不同状态下结果不同:已支付的订单能发货,待支付的不能;待支付的能取消,已发货的不能。不允许的操作要报错,不能悄悄执行。
客服查问题时最常问“这个订单经历了哪些状态”,所以订单上还记一条状态轨迹 trail,每变一次状态追加一项。本章不连数据库,订单就是内存里的对象,金额以元为单位。
21.2 先按直觉写
Section titled “21.2 先按直觉写”状态用一个字符串字段 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() 一看就知道。只有四五个状态、规则也不常变的时候,这样写清楚直接,大多数人第一版都这么写。
21.3 需求变了
Section titled “21.3 需求变了”运营要做两件事:一是预售,预售商品下单时先付定金,到发售日再付尾款,付完尾款才能发货;二是部分退款,一个订单买了几件,缺货一件就先退那一件的钱,剩下的照常发货,退完全款才算已退款。
这带来三个新状态:待付定金、已付定金、部分退款。Order 构造时多一个 presale 参数,预售订单从“待付定金”开始;再加一个 refunded 字段记已退金额。五个方法里要改的有四个:
pay():加两个分支,待付定金付款后变成已付定金,已付定金再付款(付尾款)变成已支付。ship():原来的写法是“列出不能发货的状态,其余一律发货”,新状态要逐个判断该不该拦。cancel():待付定金、已付定金也能取消,加进条件里。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 能在部分退款后发货也不是谁想清楚了,是同一行“其余一律发货”碰巧放行了它。
问题的形状是:每加一个状态,每个方法都要重新判断一遍;漏判一处不报错,那个状态就按“其余情况”走。
图中 ①–④ 就是上面四处,浅红是被改动的方法,红色加粗的框是那次事故:已付定金的订单落进了 ship() 的“其余一律发货”。
21.4 用状态模式重写
Section titled “21.4 用状态模式重写”换个思路:不在每个方法里问“现在是什么状态”,而是让每个状态自己回答“在我这里,这个操作能不能做、做完变成什么”。
基类 OrderState 给五个操作都写好默认实现:报错“某某状态的订单不能某某”。每个状态一个类,只覆写本状态允许的操作,做完事调 order.go(下一个状态)。Order 不再判断状态,只持有当前状态对象,把每个操作原样交给它:
from __future__ import annotations
from abc import ABCfrom 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:待支付 → 已支付 → 部分退款 → 已退款看黄色的三个类:预售和部分退款加的就是这三个状态类。已有的状态类里,只有转到新状态的地方要动,浅红的 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。
状态模式换来的是新状态默认什么都不能做,允许什么要明写出来,加一个状态就是加一个类。代价是类多、流程散在各个类里,要靠状态图把它拼回来。
21.5 模式定义
Section titled “21.5 模式定义”GoF 对状态的定义是:允许一个对象在其内部状态改变时改变它的行为,对象看起来似乎修改了它的类。
换成本章的话:同一个 order.ship(),订单已支付时就发货,已付定金时就报错,像是换了一个类,其实只是 order.state 指向的对象换了。“内部状态改变”由状态类自己触发:Paid.ship 做完事,把订单切到 Shipped。
看 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表里没有的组合就是不允许,这一点和状态模式一样安全;整个流程在一张表里,比翻九个类好看全,表也能存进配置文件。它适合规则多、每一步只是“换个状态”的时候。一旦某一步要做的事各不相同,比如退款要累计已退金额,再按退没退完去不同的状态,一行表就放不下了,得给表加“条件”列、“动作”列,越加越像一门小语言。这时每个状态一个类更直接。
21.6 真实系统里的例子
Section titled “21.6 真实系统里的例子”例一: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.clientfrom 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 3MiniWebSocket 是上下文,四个状态类各管自己的 send() 和 close()。Closing、Closed 没有覆写任何方法,用的都是基类的默认行为,和本章三个终结状态只有一个名字一样。
TS 里还可以不写类,用可辨识联合(discriminated union)表示状态,让编译器检查每种状态都处理到了,第 29 章再讲。
21.7 什么时候别用
Section titled “21.7 什么时候别用”- 只有两三个状态,行为差别小。比如商品只有“上架、下架”两个状态,只有一个方法关心它。拆成状态类,一个
if变成一个基类加两个子类,读代码的人要多翻两三个类。例一里真实的HTTPConnection也没拆:三个状态,这几个状态多年来基本没变,一个字段加几个if就够。 - 转换规则常由运营配置。比如审批流“金额超过一万要多一级审批”,下个月可能改成五千,或者某个部门再多一级。每次调整都要改状态类、发版本,运营等不起。这时把转换写成 21.5 那样的表,存进数据库或配置文件;规则再复杂(会签、超时自动流转),就用现成的工作流引擎(workflow engine)。
21.8 容易混淆的模式
Section titled “21.8 容易混淆的模式”| 模式 | 意图 | 区别 |
|---|---|---|
| 状态(State) | 对象的行为随内部状态切换 | 对象自己换:状态类做完事把上下文切到下一个状态,各个状态知道谁接在后面 |
| 策略(Strategy) | 把一族可互换的算法各自封装 | 结构几乎一样,但由调用方挑好交给上下文,策略之间互不认识。见第 17 章 |
| 表驱动的状态机 | 用一张“状态加操作得到下一个状态”的表描述流程 | 转换集中在一张表里,好看全、好配置;每一步只能做简单的事,复杂动作还得写代码 |
两组都能用一个问题当场判断:
- 状态还是策略:问“谁来换”。这和第 17 章 17.9 用的是同一个问题。本章的
Paid.ship做完事写着order.go(Shipped()),是状态自己把订单换到下一个,Paid知道下一个是Shipped,这是状态。第 17 章给Checkout换优惠,是外面的pick_strategy挑好交进去,FullReduction里从不提别的策略,那是策略。 - 状态类还是转换表:问“每一步除了换状态,还要做多少事”。如果每一步只是换个状态,最多记一行日志,写成表,一眼看全。如果像退款那样要算金额、按结果去不同的状态,或者同一个操作在各个状态下做的事完全不同,写成状态类。两者也能混用:表只管“能不能转、转到哪”,每个状态要做的事另写成函数。
21.9 练一练
Section titled “21.9 练一练”客服工单 Ticket:新建的工单开始处理后变成处理中;处理中的可以挂起(等用户补充信息),挂起的可以恢复成处理中;处理中的处理完提交确认,变成待确认;待确认的可以关闭。其他组合都不允许,要报错,比如新建的不能直接关闭,挂起的不能提交确认,关闭的不能再开始处理。
写出 Ticket 和各个状态类,操作叫 start、suspend、resume、resolve、close。依次执行“关闭、开始、挂起、提交确认、恢复、提交确认、关闭、开始”,打印被拒的操作和最后的状态轨迹。Python 和 TS 各写一版。
提示:先画状态图,五个状态、五条箭头;再照着图写类,基类给每个操作写好“不允许”,每条箭头在它出发的那个状态类里写成一个方法。
21.10 小结
Section titled “21.10 小结”- 状态模式把每个状态写成一个类,上下文把操作交给当前状态,由状态决定做什么、下一个是谁。
- 基类默认“不允许”,新状态允许什么要明写;不会再有“不在拦截名单里就放行”的事故。
- 加一个状态就是加一个类,已有的类只在转到它的地方加一行;代价是类多、转换散在各类里,复杂流程要配一张状态图。
- 状态少、行为差别小,一个字段加
if就够;规则多而动作简单,或者要让运营配置,用表驱动的状态机。 - 和策略结构一样,区别是谁来换:调用方挑的是策略,对象自己切的是状态。