返回介绍

建议53:用状态模式美化代码

发布于 2024-01-30 22:19:09 字数 3707 浏览 0 评论 0 收藏 0

所谓状态模式,就是当一个对象的内在状态改变时允许改变其行为,但这个对象看起来像是改变了其类。状态模式主要用于控制一个对象状态的条件表达式过于复杂的情况,其可把状态的判断逻辑转移到表示不同状态的一系列类中,进而把复杂的判断逻辑简化。

得益于Python语言的动态性,状态模式的Python实现与C++等语言的版本比起来简单得多。举个例子,一个人,工作日和周日的日常生活是不同的。

def workday():
     print 'work hard!'
def weekend():
     print 'play harder!'
class People(object):pass
people = People()
while True:
    for i in xrange(1, 8):
      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!
...

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

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

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

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

pip install state

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

from state import curr, switch, stateful,State, behavior
@stateful
class People(object):
    class Workday(State):
      default = True
      @behavior
      def day(self):
           print 'work hard.'
    class Weekend(State):
       @behavior
       def day(self):
           print 'play harder!'
people = People()
while True:
    for i in xrange(1, 8):
      if i == 6:
          switch(people, People.Weekend)
      if i == 1:
          switch(people, People.Workday)
      people.day()

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

首先是@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, *a, **kw):
        if not self._signin:
           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,从而避免把问题变为复杂的逻辑错误,让程序员更容易找到出错位置,进而修正问题。

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文