红茶的个人站点

  • 首页
  • 专栏
  • 开发工具
  • 其它
  • 隐私政策
Awalon
Talk is cheap,show me the code.
  1. 首页
  2. 编程语言
  3. Python
  4. 正文

设计模式 with Python 10:状态模式

2021年7月8日 1467点热度 1人点赞 0条评论

如果你接触过UML的状态图,应该会对状态图或者状态机有所了解,我们今天讨论的状态模式就是这种设计的落地方案。

和之前的讲解一样,我们从一个具体案例“饮料售卖机”开始进行讨论。

饮料售卖机

我们这里指的饮料售卖机就是街边常见的那种自动售货机,大概长这样:

查看源图像

老实的售卖机仅支持投币的方式,但新的机型支持网络支付,简单起见我们开发这这款仅支持投币,且只售卖一种饮料,且一块钱一瓶。这种简化会给我们少很多麻烦,毕竟我们不是真的要做一个饮料设备制造商😀

显然,饮料机在不同的状态支持不同的操作,整个购买饮料的过程就是通过一系列操作将饮料机从一个状态变为另一个状态,分析这种状态流转的问题使用UML状态图是最方便的:

image-20210708122232486

状态图的工程文件见饮料机-状态图.vsdx。

上面是我绘制的饮料机的简单状态图,不同于一般的状态图,这里没有初始和结束状态,没有结束状态很好理解,初始状态其实可以是售罄状态或者是等待投币状态,可以将这两个状态的任何一个视为一个开始状态。

有了状态图以后,我们先来看如果不使用状态模式,整个饮料机系统会是什么样。

from .drink_mache_status import DrinkMacheStatus
​
​
class DrinkMachine:
    def __init__(self) -> None:
        self.status: DrinkMacheStatus = DrinkMacheStatus.SOLD_OUT
        self.drinkNums: int = 0
​
    def coin(self) -> None:
        """用户投币"""
        if(self.status == DrinkMacheStatus.WAITING_FOR_MONEY):
            print("用户投入一枚硬币")
            self.status = DrinkMacheStatus.HAS_MONEY
        elif self.status == DrinkMacheStatus.HAS_MONEY:
            print("饮料机中已经有硬币了,不能重复投币")
        elif self.status == DrinkMacheStatus.POP_OUT_DRINK:
            print("饮料机正在出饮料,现在不能投币,请耐心等待")
        elif self.status == DrinkMacheStatus.SOLD_OUT:
            print("饮料机已经售罄,请联系供应商备货")
        else:
            print("饮料机正在处理中,目前不能投币,请等待")
​
    def backCoin(self) -> None:
        """退币操作"""
        if self.status == DrinkMacheStatus.HAS_MONEY:
            print("机器退回一枚硬币")
            self.status = DrinkMacheStatus.WAITING_FOR_MONEY
        else:
            print("饮料机没有硬币,请先投币")
​
    def clickBtn(self) -> None:
        """按动出饮料按钮"""
        if self.status == DrinkMacheStatus.HAS_MONEY:
            print("饮料机正在出饮料")
            self.status = DrinkMacheStatus.POP_OUT_DRINK
            self.__popDrink()
        elif self.status == DrinkMacheStatus.WAITING_FOR_MONEY:
            print("请先投币")
        elif self.status == DrinkMacheStatus.SOLD_OUT:
            print("饮料机已经售罄,请联系供货商")
        else:
            print("饮料机正在执行其它操作,请等待")
​
    def __popDrink(self) -> None:
        """出饮料,饮料机内部操作"""
        if self.status == DrinkMacheStatus.POP_OUT_DRINK:
            if self.drinkNums > 0:
                self.drinkNums -= 1
                print("饮料机吐出一瓶饮料")
            if self.drinkNums <= 0:
                self.status = DrinkMacheStatus.SOLD_OUT
                print("已经售罄")
            else:
                self.status = DrinkMacheStatus.WAITING_FOR_MONEY
        else:
            print("内部错误,请联系设备制造商")
​
    def addDrink(self, drinkNum: int) -> None:
        """装填饮料
        drinkNum: 饮料数目
        """
        if self.status == DrinkMacheStatus.SOLD_OUT:
            print("增加{}瓶饮料".format(drinkNum))
            self.drinkNums = drinkNum
            self.status = DrinkMacheStatus.WAITING_FOR_MONEY
        else:
            print("饮料还有,无需装填")
  • 这里仅展示饮料机关键代码,完整代码见drink_machine_v1。

  • __popDrink操作为系统内部操作,不是面向用户的,所以定义为伪私有方法。

可以看到为了对于某个状态下的非法输入(即该状态下的允许之外的操作),我们需要写大量的if/else语句进行判断,当然如果不需要精确判断状态以返回合适的提示信息,这里完全可以只使用一个else并笼统地返回非法操作这样地提示信息即可,但就现实情况来讲,如果你提供这样的直接面向普通消费者的饮料机的话,估计会被光速退货...

但那还不是最糟糕的,最糟糕的是这样的代码毫无拓展性,如果我们需要给饮料机添加一个新的功能,比如说每次买饮料都有10%的抽奖机会,如果中奖会出两瓶饮料。如果是上面的设计,我们需要添加一个枚举值,并且对于所有代码进行审查,谨慎地考虑哪些if语句需要添加新地状态判断,这无疑是相当糟糕的。

而状态模式正是为了处理此类问题而出现的。

使用状态模式

我们需要先创建状态的抽象基类:

from abc import ABC, abstractclassmethod
​
​
class DrinkMachineStatus(ABC):
    def __init__(self, drinkMachine: "DrinkMachine") -> None:
        super().__init__()
        self.drinkMache: "DrinkMachine" = drinkMachine
​
    @abstractclassmethod
    def coin(self) -> None:
        """投币"""
        pass
​
    @abstractclassmethod
    def backCoin(self) -> None:
        """退币"""
        pass
​
    @abstractclassmethod
    def clickBtn(self) -> None:
        """按动出饮料按钮"""
        pass
​
    @abstractclassmethod
    def popOutDrink(self) -> None:
        """出饮料,系统内部调用"""
        pass
​
    @abstractclassmethod
    def addDrink(self, num: int) -> None:
        """添加饮料
        num: 添加的饮料数目
        """
        pass

再创建一个具体的状态类:

from .drink_machine_status import DrinkMachineStatus
​
​
class WaitingForMoney(DrinkMachineStatus):
    """待投币状态"""
​
    def coin(self) -> None:
        print("投入一枚硬币")
        self.drinkMache.status = self.drinkMache.HAS_MONEY
​
    def backCoin(self) -> None:
        print("现在饮料机中没有硬币,请先投币")
​
    def clickBtn(self) -> None:
        print("必须先投币才可以买饮料")
​
    def popOutDrink(self) -> None:
        print("系统错误,请联系设备制造商")
​
    def addDrink(self, num: int) -> None:
        print("饮料机中有饮料,无需备货")

其它状态类都可以照此创建,完整代码见drink_machine_v2。

这里有个小技巧,可以先创建所有空的状态类,最后再实现其中的方法。

最后创建饮料机:

from .drink_machine_status.pop_out import PopOut
from .drink_machine_status.waiting_for_money import WaitingForMoney
from .drink_machine_status.drink_machine_status import DrinkMachineStatus
from .drink_machine_status.sold_out import SoldOut
from .drink_machine_status.has_money import HasMoney
​
​
class DrinkMachine:
    def __init__(self) -> None:
        # 加载所有的饮料机状态
        self.WAITING_FOR_MONEY = WaitingForMoney(self)
        self.SOLD_OUT = SoldOut(self)
        self.POP_OUT = PopOut(self)
        self.HAS_MONEY = HasMoney(self)
        # 初始化饮料机
        self.status: DrinkMachineStatus = self.SOLD_OUT
        self.drinkNum: int = 0
​
    def coin(self):
        self.status.coin()
​
    def backCoin(self):
        self.status.backCoin()
​
    def clickBtn(self):
        self.status.clickBtn()
​
    def addDrink(self, num: int):
        self.status.addDrink(num)

可以看到,这个模式的关键在于我们通过统一的状态抽象基类定义了一组拥有相同接口的状态类,这些状态之间可以通过这些接口进行状态转换。要想使用这组状态很简单,只需要客户端拥有一个状态引用即可,然后对相关状态的操作只需要委托给状态引用就可以了。

这里有这么几个细节需要注意:

  • 状态的抽象基类DrinkMachineStatus中的注解DrinkMachine只能使用这种延迟注解,不能通过from/import的方式引入DrinkMachine,因为那样会造成循环引用。

  • 所有的四个饮料机状态以对象属性的方式在DrinkMachine的构造方法中创建,这是为了实现复用,如果不这么做的话就需要在状态转换的时候临时创建一个新的状态实例,那样是没有必要的。至于为什么是对象属性而非类属性,这是因为我们这里的DrinkMachineStatus需要持有一个饮料机的引用,所以不能是饮料机的类属性。

  • 饮料机状态类DrinkMachineStatus需要改变饮料机的状态,所以必须要有饮料机的引用,为了方便,这里直接在初始化方法中接收并保存饮料机的引用,当然你也可以修改为在每次调用时候传入的方式。

  • 除了像上面这样在单个饮料机内共享状态实例,也可以在多个饮料机之间共享,如果要那样做,可以将饮料机状态实例化为饮料机类的类属性,并且在每次调用状态实例的方法的时候传入当前饮料机的引用。

  • 这里的代码风格是Python方式的,所以少了很多Getter和Setter类型的方法,因为Python可以通过属性修饰符来对已有属性的访问进行修改且不影响已有代码,这点是Java之类的语言所不具有的。

  • 因为“已投币"这个状态有两个操作可以执行:"按动按钮"和”退币“,所以这里没办法像策略模式中那样更进一步用一个函数直接取代状态,如果每一个状态都仅有一个操作流转到另一个状态,那样可以使用一组函数来代替状态,进行状态间流转,当然,这只是我个人的一个想法。

  • 这里的饮料机进行了各种简化,如果感兴趣的话,你可以尝试完善,比如支持多种饮料,或者支持一次投入多个硬币,饮料单价也可以动态调整之类的。

状态模式

关于饮料机的讨论我们先告一段落,这里总结一下状态模式:

image-20210708151213121

状态模式的UML表示很简单,并且和我们介绍的第一个模式设计模式 with Python1:策略模式极为相似。但这两者侧重点不同,策略模式是以可以灵活地替换策略为目的,而状态模式更关心如何控制一组状态之间的转换。

此外,状态基类Status可以是抽象类也可以是接口,这取决具体实现,一般来说状态类本身是会控制其客户端程序StatusMachine的状态流转的,方便起见会持有一个StatusMachine的引用,如果设计成抽象类并且实现相应的构造方法会简化子类的实现。

扩展饮料机

我们之前说了,使用状态模式可以给系统带来灵活性,那么现在我们就通过给饮料机增加一个新功能来体现这一点。

我们这里要加入的新功能是每次出饮料时候进行抽奖,有10%的几率出两瓶饮料。

我们先来看状态图应该如何修改:

image-20210708153003547

这里增加了一个中奖的状态,当然我们可以不增加新状态,直接在出饮料的操作中进行一些逻辑判断,但增加新的护照那个图是有好处的,比如说如果促销活动过去了,上面通知说要去掉抽奖功能,如果你使用的是前一种设计就会少很多麻烦。

先添加一个新状态:

from .drink_machine_status import DrinkMachineStatus
class Winner(DrinkMachineStatus):
    def coin(self) -> None:
        print("饮料机正在投放饮料,请稍后再试")
​
    def backCoin(self) -> None:
        print("饮料机正在投放饮料,请稍后再试")
​
    def clickBtn(self) -> None:
        print("饮料机正在执行投放饮料操作,请勿重复操作")
​
    def popOutDrink(self) -> None:
        print("正在出饮料")
        if self.drinkMache.drinkNum > 0:
            self.drinkMache.drinkNum -= 1
            print("吐出一瓶饮料")
        if self.drinkMache.drinkNum > 0:
            self.drinkMache.drinkNum -= 1
            print("吐出一瓶饮料")
        if self.drinkMache.drinkNum > 0:
            self.drinkMache.status = self.drinkMache.WAITING_FOR_MONEY
        else:
            self.drinkMache.status = self.drinkMache.SOLD_OUT
​
    def addDrink(self, num: int) -> None:
        print("当前饮料机还有饮料,无需备货")

这个状态几乎和PopOut状态一致,只不过在吐出饮料的时候会出两瓶。

这里使用重复两次吐一瓶的操作是有意为之,为了避免可能的饮料机中存货不够两瓶的情况。

在饮料机中添加新的状态:

class DrinkMachine:
    def __init__(self) -> None:
        # 加载所有的饮料机状态
        self.WAITING_FOR_MONEY = WaitingForMoney(self)
        self.SOLD_OUT = SoldOut(self)
        self.POP_OUT = PopOut(self)
        self.HAS_MONEY = HasMoney(self)
        self.WINNER = Winner(self)

在HasMoney状态中添加判断是否中奖了的逻辑:

from .drink_machine_status import DrinkMachineStatus
import random
​
​
class HasMoney(DrinkMachineStatus):
    def coin(self) -> None:
        print("已经有硬币了,不能重复投币")
​
    def backCoin(self) -> None:
        print("退还硬币给用户")
        self.drinkMache.status = self.drinkMache.WAITING_FOR_MONEY
​
    def clickBtn(self) -> None:
        print("用户按下饮料购买按钮")
        if 1 == random.randint(1,10):
            self.drinkMache.status = self.drinkMache.WINNER
        else:
            self.drinkMache.status = self.drinkMache.POP_OUT
        self.drinkMache.status.popOutDrink()
​
    def popOutDrink(self) -> None:
        print("内部故障,请联系设备制造商")
​
    def addDrink(self, num: int) -> None:
        print("现在饮料机还有饮料,无需备货")

完整代码见drink_machine_v3。

现在可以试一试你的手气了,我是第8次成功的,你呢?

关于状态模式的讨论到此完毕,谢谢阅读。

本作品采用 知识共享署名 4.0 国际许可协议 进行许可
标签: 设计模式
最后更新:2022年4月7日

魔芋红茶

加一点PHP,加一点Go,加一点Python......

点赞
< 上一篇
下一篇 >

文章评论

取消回复

*

code

COPYRIGHT © 2021 icexmoon.cn. ALL RIGHTS RESERVED.
本网站由提供CDN加速/云存储服务

Theme Kratos Made By Seaton Jiang

宁ICP备2021001508号

宁公网安备64040202000141号