状态模式练习

状态模式练习——电灯、售货机与订单状态

按《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_stateinsert_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_statepay()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 发生。

自检问题

  1. 上下文为什么不需要 if 判断“当前是哪个状态”?
    因为“当前状态”已经用 多态 表示:当前是哪个状态,就调用哪个状态类的实现;行为差异由不同状态类体现,上下文只负责 委托set_state

  2. 状态转移应该写在哪里?
    写在具体状态类里:在处理完请求后,若需要切换,就调用 context.set_state(下一状态);上下文只提供 set_state,不决定“切到谁”。

  3. 若要多一个状态(如售货机加“维修中”),应该改哪些地方?
    新增一个 ConcreteState(如 MaintenanceState),实现同一套方法;在需要进入该状态的地方(如某操作或初始化)调用 context.set_state(MaintenanceState())上下文和已有状态类的委托逻辑不必改,符合开闭原则。

做完以上三道练习,再对照《状态模式》文档中的示例,对状态模式会掌握得比较扎实。建议每道题先自己写一遍,再对照参考答案和验证要点检查。

发表评论