python设计模式–状态模式(State Pattern)

Python 设计模式——状态模式(State Pattern)详解

本文面向零基础新手,从概念到“行为随状态变化”,从简单开关到自动售货机,全方位讲解状态模式。


一、什么是状态模式?

1.1 通俗理解

状态模式是一种行为型设计模式,核心思想是:

当一个对象的“行为”会随着其“内部状态”的改变而改变时,把每种状态对应的行为封装到各自的“状态类”里;对象(上下文)持有一个“当前状态”,把请求委托给当前状态处理;状态类在处理时可以根据需要把上下文切换到别的状态。这样用“多态”代替“一堆 if-else 判断状态”,行为清晰、易扩展。

可以这样类比:

  • 不用状态模式:自动售货机有“未投币”“已投币”“出货中”“缺货”等状态,每个操作(投币、按按钮、退币)里都要写 if 当前状态 == 未投币 then ... elif 当前状态 == 已投币 then ...,代码里到处都是状态判断,难维护。
  • 用状态模式:为每种状态写一个类(未投币状态、已投币状态、出货状态、缺货状态);售货机只持有一个“当前状态”对象,投币、按按钮时都交给“当前状态”去处理;每个状态类自己决定“接下来切换到哪个状态”(如投币后从未投币切到已投币)。行为按状态分散到各个状态类里,上下文只负责委托。

所以,状态模式解决的是:对象的行为依赖状态、且状态较多时,用“状态类”封装“该状态下的行为与转移”,避免上下文里塞满 if-else,并便于增加新状态。

1.2 为什么需要状态模式?

场景一:行为强依赖状态,且状态多

例如:订单有“待支付、已支付、已发货、已完成、已取消”;每种状态下“能否支付、能否取消、能否发货”都不同。若在订单类里写 if state == 待支付: ... elif state == 已支付: ...,会又长又难改。用状态模式:每个状态一个类,订单把操作委托给当前状态,每个状态类只关心自己状态下的逻辑和“下一步转到哪”

场景二:状态转移清晰

“投币后从未投币到已投币”“按按钮后从已投币到出货中”,这些转移可以写在状态类里(当前状态处理完请求后,把上下文的 current_state 设为新状态),而不是散落在各处 if 里。

场景三:避免重复、易加新状态

新增一种状态时,只需新增一个状态类并在合适的地方让上下文切换到这个状态,不必改原有状态类里的大段 if-else。

1.3 适用场景(什么时候用?)

场景 说明
对象行为明显依赖状态 同一操作在不同状态下表现不同(如投币在“未投币”和“已投币”下不同)。
状态较多,if-else 泛滥 用多态代替“根据状态分支”。
状态转移有明确规则 可由状态类负责“处理完请求后切换到哪一状态”。

简单记忆行为随状态变状态不少、希望每种状态一个类、逻辑清晰时,用状态模式。


二、状态模式的结构(三个角色)

角色 说明
上下文(Context) 拥有“当前状态”(State 类型);客户请求来时,把请求委托给当前状态处理(如 context.request() 内部调 current_state.handle(context));可提供 set_state(state) 供状态类切换状态。
状态(State) 抽象接口,定义状态下的行为(如 handle(context)、on_enter(context) 等);具体状态实现这些方法。
具体状态(ConcreteState) 实现当前状态下的逻辑;在处理请求时若需要,可调用 context.set_state(新状态) 完成状态转移。

关系可以理解为:

  • 客户只和 Context 打交道(如投币、按按钮),不直接操作状态对象。
  • Context 把请求转给 current_state.xxx(context);具体状态在执行完自己的逻辑后,如需转移则 context.set_state(下一个状态)
  • 状态类之间可以互相“知道”(如“已投币状态”知道“出货状态”是谁),以便切换;也可以由 Context 根据事件返回新状态,看设计偏好。

三、示例一:简单电灯(开 / 关两种状态)

需求:电灯有“开”和“关”两种状态;按开关时,若当前是关则变开,若是开则变关。用两个状态类封装“开时的行为”和“关时的行为”。

3.1 状态接口 + 上下文

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)

3.2 具体状态:开、关

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())

3.3 使用

lamp = Lamp()
lamp.set_state(OffState())  # 初始为关

lamp.press_switch()  # 关 -> 开
lamp.press_switch()  # 开 -> 关
lamp.press_switch()  # 关 -> 开

要点:Lamp 不写 if 当前是开还是关,只委托给 _state.switch(self);状态类自己决定“下一状态是谁”并 set_state。


四、示例二:自动售货机(四种状态)

需求:售货机有“未投币”“已投币”“出货中”“缺货”。操作:投币、按按钮、退币。规则:未投币时投币->已投币,已投币时按按钮->出货中、退币->未投币,出货后->未投币或缺货;缺货时按按钮无效。用四个状态类分别处理。

4.1 状态接口 + 上下文(售货机)

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 = 2  # 商品数量,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("  出货")

4.2 具体状态(每态只写自己该做的事与转移)

这里用三个状态:未投币、已投币、缺货。每种状态只处理自己的逻辑,并在适当时调用 machine.set_state(下一状态) 完成转移。

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("  未投币,无法退币")

说明:若需要“出货中”作为单独状态(如配合动画),可增加 DispensingState,在 HasCoinState 按按钮后先 set_state(DispensingState()) 并 release_product,再由 DispensingState 在“进入”或下次被调时根据 count 切回 NoCoinState 或 SoldOutState。

4.3 使用

machine = VendingMachine()
machine._count = 1  # 只放 1 件商品,便于演示缺货
machine.set_state(NoCoinState())

machine.insert_coin()   # 投币成功
machine.press_button()  # 出货,count 变为 0,回到未投币
machine.insert_coin()   # 再次投币
machine.press_button()  # 缺货,并切换到 SoldOutState
machine.press_button()  # 缺货(已在缺货状态)

要点:每种状态只写自己状态下的 insert/press/eject 逻辑和“下一步转到谁”;Context(VendingMachine)不写状态判断,只委托给 _state。


五、示例三:订单状态(待支付、已支付、已取消)

需求:订单有“待支付”“已支付”“已取消”。操作:支付、取消。规则:待支付可支付->已支付、可取消->已取消;已支付和已取消不能再支付、取消。用三个状态类。

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("  已取消")

要点:订单只委托 pay/cancel 给当前状态;每个状态类内写好“能做什么、不能做什么、转到哪”。


六、状态模式 vs 其他

模式 目的 与状态的区别
状态 行为随内部状态变,状态类封装“该状态下的行为与转移” 强调“状态切换”、同一请求在不同状态下不同表现。
策略 算法可替换,客户选一种策略 策略一般不主动把上下文改成另一种策略;状态会在处理中切换上下文的状态。
条件分支 直接 if state == … 状态模式用多态代替分支,每种状态一个类。

简单记忆:状态 = 当前状态决定行为 + 状态类负责转移 + 用多态代替 if-else


七、常见问题与注意点

7.1 谁负责创建状态对象?

可以每个状态类用单例(同一状态共用一个实例),或每次切换时 new 新状态。单例省内存、适合无实例字段的状态;若状态要带参数可用 new。

7.2 状态类之间要不要互相引用?

可以。例如 HasCoinState 知道 NoCoinState、SoldOutState,在 press_button 里 set_state(NoCoinState()) 或 set_state(SoldOutState());也可以由 Context 根据条件返回新状态,状态类只告诉 Context “要切换”,由 Context 决定切到谁,减少状态类之间的依赖。

7.3 何时不用状态模式?

状态很少(如一两个)、行为简单时,直接 if-else 即可;状态多、行为复杂、转移清晰时再用状态模式更划算。


八、小结

  • 状态模式:对象行为随内部状态变化时,把每种状态的行为和转移封装到状态类上下文持有一个当前状态,把请求委托给当前状态,由状态类决定是否切换状态,从而用多态代替大量 if-else。
  • 三个角色:上下文(持有当前状态、委托请求、提供 set_state)、状态(接口)、具体状态(实现行为与转移)。
  • 典型用途:自动售货机、订单流程、TCP 连接、工作流、游戏角色等“多状态、行为依状态变化”的场景。
  • 实现要点:上下文不根据状态写分支,只 current_state.xxx(context);状态类在处理后按规则 context.set_state(下一状态)。

建议先写“电灯开/关”,再写“售货机”或“订单状态”,体会“委托给当前状态、状态负责转移”,这样对状态模式会掌握得比较扎实。

发表评论