状态模式练习——电灯、售货机与订单状态
按《Python 设计模式——状态模式》的建议,通过三道练习巩固:① 电灯开/关两状态;② 自动售货机(未投币/已投币/缺货);③ 订单(待支付/已支付/已取消)。每步都有完整可运行代码和验证要点。
练习一:电灯开/关(两状态)
目的
体会上下文把请求委托给当前状态,状态类在处理后自己决定“下一状态是谁”并调用 context.set_state(下一状态);上下文里不写 if 判断当前是开还是关。
要求
- 定义 State 接口,有方法 switch(lamp)(按开关时的行为)。
- 实现 Lamp(上下文):有 set_state(state)、press_switch()(内部调 self._state.switch(self))。
- 实现 OffState:switch 时打印“关 -> 开”,并 lamp.set_state(OnState())。
- 实现 OnState:switch 时打印“开 -> 关”,并 lamp.set_state(OffState())。
- 客户代码:创建 Lamp,初始设为 OffState,连续调用三次 press_switch(),验证输出为“关->开”“开->关”“关->开”。
参考答案
from abc import ABC, abstractmethod
class State(ABC):
@abstractmethod
def switch(self, lamp: "Lamp"):
pass
class Lamp:
def __init__(self):
self._state = None
def set_state(self, state: State):
self._state = state
def press_switch(self):
if self._state:
self._state.switch(self)
class OffState(State):
def switch(self, lamp: Lamp):
print(" 关 -> 开")
lamp.set_state(OnState())
class OnState(State):
def switch(self, lamp: Lamp):
print(" 开 -> 关")
lamp.set_state(OffState())
# 使用
lamp = Lamp()
lamp.set_state(OffState())
lamp.press_switch() # 关 -> 开
lamp.press_switch() # 开 -> 关
lamp.press_switch() # 关 -> 开
验证要点
- 三次 press_switch 的输出依次为 关->开、开->关、关->开。
- 确认:Lamp 内部没有 if 判断“当前是开还是关”,只做 self._state.switch(self);状态转移逻辑全部在状态类里。
练习二:自动售货机(未投币 / 已投币 / 缺货)
目的
三种状态、三种操作(投币、按按钮、退币);每种状态只写自己该做的行为和“下一步转到谁”;缺货时按按钮要切换到缺货状态,且无货时不能再出货。
要求
- 定义 State 接口:insert_coin(machine)、press_button(machine)、eject_coin(machine)。
- 实现 VendingMachine(上下文):有 _count(商品数量)、set_state、insert_coin / press_button / eject_coin(委托给 _state)、release_product()(count 减 1 并打印“出货”)。
- 实现 NoCoinState:投币 -> HasCoinState;按按钮/退币提示“请先投币”“未投币无法退币”。
- 实现 HasCoinState:按按钮 -> 若 count>0 则 release_product 并设为 NoCoinState,否则打印“缺货”并设为 SoldOutState;退币 -> NoCoinState;重复投币提示“已投币…”。
- 实现 SoldOutState:投币/按按钮提示“缺货”;退币提示“未投币无法退币”(缺货且未投币)。
- 客户代码:机器 _count=1,初始 NoCoinState;依次:投币、按按钮(出货)、再投币、按按钮(缺货并切到 SoldOut)、再按按钮(打印缺货)。验证输出与状态转移符合预期。
参考答案
from abc import ABC, abstractmethod
class State(ABC):
@abstractmethod
def insert_coin(self, machine: "VendingMachine"):
pass
@abstractmethod
def press_button(self, machine: "VendingMachine"):
pass
@abstractmethod
def eject_coin(self, machine: "VendingMachine"):
pass
class VendingMachine:
def __init__(self):
self._state = None
self._count = 0
def set_state(self, state: State):
self._state = state
def insert_coin(self):
self._state.insert_coin(self)
def press_button(self):
self._state.press_button(self)
def eject_coin(self):
self._state.eject_coin(self)
def release_product(self):
if self._count > 0:
self._count -= 1
print(" 出货")
class NoCoinState(State):
def insert_coin(self, machine: VendingMachine):
print(" 投币成功")
machine.set_state(HasCoinState())
def press_button(self, machine: VendingMachine):
print(" 请先投币")
def eject_coin(self, machine: VendingMachine):
print(" 未投币,无法退币")
class HasCoinState(State):
def insert_coin(self, machine: VendingMachine):
print(" 已投币,请按按钮或退币")
def press_button(self, machine: VendingMachine):
if machine._count > 0:
machine.release_product()
machine.set_state(NoCoinState())
else:
print(" 缺货")
machine.set_state(SoldOutState())
def eject_coin(self, machine: VendingMachine):
print(" 退币")
machine.set_state(NoCoinState())
class SoldOutState(State):
def insert_coin(self, machine: VendingMachine):
print(" 缺货,暂无法投币")
def press_button(self, machine: VendingMachine):
print(" 缺货")
def eject_coin(self, machine: VendingMachine):
print(" 未投币,无法退币")
# 使用
machine = VendingMachine()
machine._count = 1
machine.set_state(NoCoinState())
machine.insert_coin() # 投币成功
machine.press_button() # 出货
machine.insert_coin() # 投币成功
machine.press_button() # 缺货
machine.press_button() # 缺货
验证要点
- 第一次按按钮输出 出货,第二次按按钮(已无货)输出 缺货,之后在 SoldOutState 下再按仍输出 缺货。
- 确认:VendingMachine 的 insert_coin / press_button / eject_coin 只委托给 self._state,不写 if 当前是哪种状态;所有分支与转移都在 NoCoinState / HasCoinState / SoldOutState 里。
练习三:订单状态(待支付 / 已支付 / 已取消)
目的
订单有三种状态;支付和取消在不同状态下行为不同;状态类负责“能否操作”和“转移到哪一状态”。
要求
- 定义 OrderState 接口:pay(order)、cancel(order)。
- 实现 Order(上下文):set_state、pay()、cancel()(都委托给 _state)。
- 实现 PendingState:pay -> 打印“支付成功”并设为 PaidState;cancel -> 打印“订单已取消”并设为 CancelledState。
- 实现 PaidState:pay -> 打印“已支付,无需重复支付”;cancel -> 打印“已支付,无法取消”。
- 实现 CancelledState:pay / cancel 都打印“已取消,无法…”类提示。
- 客户代码:订单初始 PendingState;调用 pay(),再调用 cancel(),验证第二次 cancel 输出“已支付,无法取消”;另建一订单,先 cancel() 再 pay(),验证“已取消”类提示。
参考答案
from abc import ABC, abstractmethod
class OrderState(ABC):
@abstractmethod
def pay(self, order: "Order"):
pass
@abstractmethod
def cancel(self, order: "Order"):
pass
class Order:
def __init__(self):
self._state = None
def set_state(self, state: OrderState):
self._state = state
def pay(self):
self._state.pay(self)
def cancel(self):
self._state.cancel(self)
class PendingState(OrderState):
def pay(self, order: Order):
print(" 支付成功")
order.set_state(PaidState())
def cancel(self, order: Order):
print(" 订单已取消")
order.set_state(CancelledState())
class PaidState(OrderState):
def pay(self, order: Order):
print(" 已支付,无需重复支付")
def cancel(self, order: Order):
print(" 已支付,无法取消")
class CancelledState(OrderState):
def pay(self, order: Order):
print(" 已取消,无法支付")
def cancel(self, order: Order):
print(" 已取消")
# 使用
order = Order()
order.set_state(PendingState())
order.pay() # 支付成功
order.cancel() # 已支付,无法取消
order2 = Order()
order2.set_state(PendingState())
order2.cancel() # 订单已取消
order2.pay() # 已取消,无法支付
验证要点
- 第一个订单:pay 后 cancel 输出 已支付,无法取消。
- 第二个订单:先 cancel 再 pay,输出 已取消,无法支付。
- 确认:Order 只做 self._state.pay(self) / self._state.cancel(self),不根据状态写 if-else;谁能支付、谁能取消、转到哪一状态都在各状态类里。
三步汇总与自检
| 练习 | 重点 | 关键点 |
|---|---|---|
| 一 | 电灯开/关 | 上下文委托 switch;状态类内 set_state(另一状态);两状态互相切换。 |
| 二 | 售货机三状态 | 投币/按按钮/退币在不同状态下不同;HasCoin 按按钮根据 count 切 NoCoin 或 SoldOut。 |
| 三 | 订单三状态 | 待支付可支付/取消;已支付、已取消只提示不可操作;转移只在 Pending 发生。 |
自检问题
-
上下文为什么不需要 if 判断“当前是哪个状态”?
因为“当前状态”已经用 多态 表示:当前是哪个状态,就调用哪个状态类的实现;行为差异由不同状态类体现,上下文只负责 委托 和 set_state。 -
状态转移应该写在哪里?
写在具体状态类里:在处理完请求后,若需要切换,就调用 context.set_state(下一状态);上下文只提供 set_state,不决定“切到谁”。 -
若要多一个状态(如售货机加“维修中”),应该改哪些地方?
新增一个 ConcreteState(如 MaintenanceState),实现同一套方法;在需要进入该状态的地方(如某操作或初始化)调用 context.set_state(MaintenanceState());上下文和已有状态类的委托逻辑不必改,符合开闭原则。
做完以上三道练习,再对照《状态模式》文档中的示例,对状态模式会掌握得比较扎实。建议每道题先自己写一遍,再对照参考答案和验证要点检查。