>>> from env_helper import info; info()
页面更新时间: 2023-12-27 09:32:31
运行环境:
    Linux发行版本: Debian GNU/Linux 12 (bookworm)
    操作系统内核: Linux-6.1.0-16-amd64-x86_64-with-glibc2.36
    Python版本: 3.11.2

5.4. 用状态模式美化代码

所谓状态模式,就是当一个对象的内在状态改变时允许改变其行为,但这个对象看起来 像是改变了其类。 状态模式主要用于控制一个对象状态的条件表达式过于复杂的情况,其可 把状态的判断逻辑转移到表示不同状态的一系列类中, 进而把复杂的判断逻辑简化。 得益于Python语言的动态性,状态模式的Python实现与C++等语言的版本比起来简单 得多。 举个例子,一个人,工作日和周日的日常生活是不同的。

>>> def workday():
>>>     print('work hard!')
>>>
>>>
>>> def weekend():
>>>     print('play harder!')
>>>
>>>
>>> class People(object): pass
>>>
>>>
>>> people = People()
>>>
>>> # while True:
>>>
>>> for jj in range(5):
>>>
>>>     for i in range(1, 10):
>>>         if i == 6:
>>>             people.day = weekend
>>>         if i == 1:
>>>             people.day = workday
>>>         people.day()
work hard!
work hard!
work hard!
work hard!
work hard!
play harder!
play harder!
play harder!
play harder!
work hard!
work hard!
work hard!
work hard!
work hard!
play harder!
play harder!
play harder!
play harder!
work hard!
work hard!
work hard!
work hard!
work hard!
play harder!
play harder!
play harder!
play harder!
work hard!
work hard!
work hard!
work hard!
work hard!
play harder!
play harder!
play harder!
play harder!
work hard!
work hard!
work hard!
work hard!
work hard!
play harder!
play harder!
play harder!
play harder!

运行上述代码,输出如下:

work hard!
work hardi
work hard!
work hard!
work hard!
play harder!
play harder!
......

就这样,通过在不同的条件下将实例的方法(即行为)替换掉,就实现了状态模式。但 是这个简单的例子仍然有以下缺陷:

  • 査询对象的当前状态很麻烦。

  • 状态切换时需要对原状态做一些清扫工作,而对新的状态需要做一些初始化工作,因 为每个状态需要做的事情不同,全部写在切换状态的代码中必然重复,所以需要一个 机制来简化。

python-state包通过几个辅助函数和修饰函数很好地解决了这个问题t并且定义了一个简 明状态机框架。先用pip安装它。

然后用它改写之前的例子。

from state import curr,switch,stateful,State,behavior @stateful class People(object):

class Workday(State):

default = True @behavior def day(self):

print('I work hard.')

class Weekend(State):

@behavior def day(self):

print('play harder!')

people = People() while True:

for i in range(1,8):
if i == 6:

switch(people, People.Weekend)

if i == 1:

switch(people, People.Workday)

people.day()

怎么样?是不是感觉好像比应用模式之前的代码还要长?这是因为例子太简单了,后 面再给大家展示更贴近真实业务需求的例子。现在我们先按下这个不表,单看最后一行的 people.day()。people是People的一个实例,但是People并没有定义day()方法啊?为了解决 这个疑惑,需要我们从头看起3

首先是@stateful这个修饰函数,它包含了许多“黑魔法”,其中最重要的是重载了被修 饰类的__getattr__()方法从而使得People的实例能够调用当前状态类的方法。被@stateful 修饰后的类的实例是带有状态的,能够使用curr()査询当前状态,也可以使用switch()进行 状态切换。接下来继续往下看,可以看到类Workday继续自State类,这个State类也是来自 于state包,从其派生的子类能够使用__begin____end__状态转换协议,通过重载这两个 协议,子类能够自定义进人和离开当前状态时对宿主(在本例中即people)的初始化和清理工 作。对于一个@stateful类而言,有一个默认的状态(即其实例初始化后的第一个状态),通过 类定义的default属性标识,default设置为True的类成为默认状态。@behavior修饰函数用以修 饰状态类的方法,其实它是内置函数staticmethod的别名。为什么要将状态类的方法实现为 静态方法呢?因为state包的原则是状态类只有行为,没有状态(状态都保存在宿主上>,这样 可以更好地实现代码重用。那么day()方法既然是静态的,为什么有self参数?这其实是因 为self并不是Python的关键字,在这里使用self有助于理解状态类的宿主是People的实例。

至此,读者对state这个简单的包就基本上了解清楚了。下面讲一个来自真实业务的例 子,看它如何美化原有的代码。在网络编程中,通常有一个User类,每一个在线用户都有一 个User的实例与之对应。User有一些方法,需要确保用户登录之后才能调用,比如査看用 户信息。这些方法大概像这样:

class User(object):
def signin (self,usr,pwd):

... self._signin = True

def do sth (self,*a,**kw):
if not self._signin:

raise NeedSignin()

...

真实项目中,类似do_sth()的业务代码数量不少,如果每个函数前两行都是if..raise…, 以确保调用场景正确,那么可以想象得出来代码该有多么难看(代码一重复就不好看了)。这 时候程序员会选择使用decorator来修饰这些业务代码。

def ensure_signin(func):
def _func(self, *ar **kw):
if not self._sigrkin:

raise NeedSignin()

return func(self,*a,**kw)

return _func

@ensure_signin def do_sth(self, *a,**kw):

...

上述代码看上去很完美的解决方案,而且@ensure_signin相当Pythonic但是想象一 下,某些地方,你除了要确定登录之外,还需要确定是否在战斗副本中,角色是否已经死 亡……等等。想象一下,十个八个方法,每个方法上面都顶着四五个修饰函数,该有多么丑 陋!这就是状态模式可以美化的地方。

@stateful class User(object):

class NeedSignin(State):

default = True @behavior def signin(self, usr, pwd):

... switch(self, Player.Signin)

class Signin(State):

@behavior def move(self,dst): ... @behavior def atk(self, other):...

可以看到,当用户登录以后,就切换到了 Player.Signin状态,而在Signin状态的行为是 不需要做是否已经登录的判断的,这是因为除了登录成功,User的实例无法跳转到Signin状 态,反过来说就是只要当前状态是Signin,那必定已经登录,自然无须再验证。

可以看到,通过状态模式,可以像decorator—样去掉if…raise…上下文判断,但比它更 榉的是真的一个if…raise…都没有了。另外,需要多重判断的时候要给一个方法戴上四五顶 “帽子”的情况也没有了,还通过把多个方法分派到不同的状态类,消灭掉一般情况下Player 总是一个巨类的“坏味道”,保持类的短小,更容易维护和重用。不过这些都比不上一个更 大的好处:当调用当前状态不存在的行为时,出错信息抛出的是AttributeError,从而避免把 问题变为复杂的逻辑错误,让程序员更容易找到出错位置,进而修正问题。