Python-设计模式实践教程-四-

81 阅读55分钟

Python 设计模式实践教程(四)

原文:Practical Python Design Patterns

协议:CC BY-NC-SA 4.0

十四、观察者模式

诺顿,你知道吗,我一直在观察你。—艾迪·墨菲,神志不清

如果你做了第十二章的目标健身操练习,你会注意到减少某些方法中使用的线条数量是多么困难。如果对象与许多其他对象耦合得太紧,这尤其困难;也就是说,一个对象过于依赖它对其他对象内部的了解。因此,方法有太多与所讨论的方法没有严格关系的细节。

也许此刻这看起来有点模糊。请允许我澄清。假设您有一个系统,用户能够完成挑战,完成后他们会获得积分,获得一般经验积分,并且还会获得用于完成任务的技能的技能积分。每项任务都需要在相关的地方进行评估和评分。花点时间想想你将如何实现这样一个系统。一旦你写下了基本的架构,看看下面的例子。

我们想要的是某种方式来跟踪用户的收入和支出,用户到目前为止积累的经验量,以及对获得特定徽章或成就有影响的分数。你可以把这个系统想象成某种培养习惯的挑战应用,或者任何一个曾经创造过的角色扮演游戏。


task_completer.py

class Task(object):

    def __init__(self, user, _type):
        self.user = user
        self._type = _type

    def complete(self):
        self.user.add_experience(1)
        self.user.wallet.increase_balance(5)

        for badge in self.user.badges:
            if self._type == badge._type:
                badge.add_points(2)

class User(object):

    def __init__(self, wallet):
        self.wallet = wallet
        self.badges = []
        self.experience = 0

    def add_experience(self, amount):
        self.experience += amount

    def __str__(self):
        return "Wallet\t{}\nExperience\t{}\n+ Badges +\n{}\n++++++++++++++++".format(
            self.wallet,
            self.experience,
            "\n".join([ str(x) for x in self.badges])
        )

class Wallet(object):

    def __init__(self):
        self.amount = 0

    def increase_balance(self, amount):
        self.amount += amount

    def decrease_balance(self, amount):
        self.amount -= amount

    def __str__(self):
        return str(self.amount)

class Badge(object):

    def __init__(self, name, _type):
        self.points = 0
        self.name = name
        self._type = _type
        self.awarded = False

    def add_points(self, amount):
        self.points += amount

        if self.points > 3:
            self.awarded = True

    def __str__(self):
        if self.awarded:
            award_string = "Earned"
        else:
            award_string = "Unearned"

        return "{}: {} [{}]".format(
            self.name,
            award_string,
            self.points
        )

def main():
    wallet = Wallet()
    user = User(wallet)

    user.badges.append(Badge("Fun Badge", 1))
    user.badges.append(Badge("Bravery Badge", 2))
    user.badges.append(Badge("Missing Badge", 3))

    tasks = [Task(user, 1), Task(user, 1), Task(user, 3)]
    for task in tasks:
        task.complete()

    print(user)

if __name__ == "__main__":
    main()

在输出中,我们可以看到添加到钱包、经验和徽章的相关值,一旦清除了阈值,就会授予正确的徽章。

Wallet  15
Experience      3
+ Badges +
Fun Badge: Earned [4]
Bravery Badge: Unearned [0]
Missing Badge: Unearned [2]
++++++++++++++++

每当任务完成时,这个非常基本的实现都有一组相当复杂的计算要执行。在前面的代码中,我们有一个实现得相当好的体系结构,但是评估函数仍然很笨拙,我敢肯定,您已经有了这样的感觉,一旦它开始工作,您就不想再处理它了。我还认为为这种方法编写测试并不有趣。对系统的任何添加都意味着改变这种方法,迫使进行更多的计算和评估。

如前所述,这是紧密耦合系统的一个症状。Task对象必须了解每一个points对象,以便能够将正确的点数或信用分配给正确的子系统。我们希望从task complete方法的主要部分中移除每个规则的评估,并将更多的责任放在子系统上,这样它们就可以根据自己的规则而不是一些预见对象的规则来处理数据变更。

要做到这一点,我们要向一个更加解耦的系统迈出第一步,如下所示:


task_semi_decoupled.py

class Task(object):

    def __init__(self, user, _type):
        self.user = user
        self._type = _type

    def complete(self):
        self.user.complete_task(self)
        self.user.wallet.complete_task(self)
        for badge in self.user.badges:
            badge.complete_task(self)

class User(object):

    def __init__(self, wallet):
        self.wallet = wallet
        self.badges = []
        self.experience = 0

    def add_experience(self, amount):
        self.experience += amount

    def complete_task(self, task):
        self.add_experience(1)

    def __str__(self):
        return "Wallet\t{}\nExperience\t{}\n+ Badges +\n{}\n++++++++++++++++".format(
            self.wallet,
            self.experience,
            "\n".join([ str(x) for x in self.badges])
        )

class Wallet(object):

    def __init__(self):
        self.amount = 0

    def increase_balance(self, amount):
        self.amount += amount

    def decrease_balance(self, amount):
        self.amount -= amount

    def complete_task(self, task):
        self.increase_balance(5)

    def __str__(self):
        return str(self.amount)

class Badge(object):

    def __init__(self, name, _type):
        self.points = 0
        self.name = name
        self._type = _type
        self.awarded = False

    def add_points(self, amount):
        self.points += amount

        if self.points > 3:
            self.awarded = True

    def complete_task(self, task):
        if task._type == self._type:
            self.add_points(2)

    def __str__(self):
        if self.awarded:
            award_string = "Earned"
        else:
            award_string = "Unearned"

        return "{}: {} [{}]".format(
            self.name,
            award_string,
            self.points
        )

def main():
    wallet = Wallet()
    user = User(wallet)

    user.badges.append(Badge("Fun Badge", 1))
    user.badges.append(Badge("Bravery Badge", 2))
    user.badges.append(Badge("Missing Badge", 3))

    tasks = [Task(user, 1), Task(user, 1), Task(user, 3)]
    for task in tasks:
        task.complete()

    print(user)

if __name__ == "__main__":
    main()

这将导致与之前相同的输出。这已经是一个好得多的解决方案了。现在,评估发生在与它们相关的对象中,这更接近于包含在对象健美操练习中的规则。我希望在实践这些类型的代码体操的价值方面,以及它如何使你成为一名更好的程序员方面,对你有一点启发。

让我感到困扰的是,每当一种新的徽章、信用、警告或任何你能想到的东西被添加到系统中时,处理程序就会被改变。理想的情况是,如果有某种挂钩机制,不仅允许您注册新的子系统,然后根据需要动态地对它们进行评估,而且还可以让您不必对主任务系统进行任何更改。

回调函数的概念对于实现这种新的动态水平非常有用。我们将向一个集合中添加通用回调函数,每当任务完成时都会运行这些函数。现在系统更加动态,因为这些系统可以在运行时添加。系统中的对象将会更加分离。


task_with_callbacks.py

class Task(object):

    def __init__(self, user, _type):
        self.user = user
        self._type = _type
        self.callbacks = [
            self.user,
            self.user.wallet,
        ]
        self.callbacks.extend(self.user.badges)

    def complete(self):
        for item in self.callbacks:
            item.complete_task(self)

class User(object):

    def __init__(self, wallet):
        self.wallet = wallet
        self.badges = []
        self.experience = 0

    def add_experience(self, amount):
        self.experience += amount

    def complete_task(self, task):
        self.add_experience(1)

    def __str__(self):
        return "Wallet\t{}\nExperience\t{}\n+ Badges +\n{}\n++++++++++++++++".format(
            self.wallet,
            self.experience,
            "\n".join([ str(x) for x in self.badges])
        )

class Wallet(object):

    def __init__(self):
        self.amount = 0

    def increase_balance(self, amount):
        self.amount += amount

    def decrease_balance(self, amount):
        self.amount -= amount

    def complete_task(self, task):
        self.increase_balance(5)

    def __str__(self):
        return str(self.amount)

class Badge(object):

    def __init__(self, name, _type):
        self.points = 0
        self.name = name
        self._type = _type
        self.awarded = False

    def add_points(self, amount):
        self.points += amount

        if self.points > 3:
            self.awarded = True

    def complete_task(self, task):
        if task._type == self._type:
            self.add_points(2)

    def __str__(self):
        if self.awarded:
            award_string = "Earned"
        else:
            award_string = "Unearned"

        return "{}: {} [{}]".format(
            self.name,
            award_string,
            self.points
        )

def main():
    wallet = Wallet()
    user = User(wallet)

    user.badges.append(Badge("Fun Badge", 1))
    user.badges.append(Badge("Bravery Badge", 2))
    user.badges.append(Badge("Missing Badge", 3))

    tasks = [Task(user, 1), Task(user, 1), Task(user, 3)]
    for task in tasks:
        task.complete()

    print(user)

if __name__ == "__main__":
    main()

现在你有了一个任务完成时要回调的对象列表,任务不需要知道回调列表中对象的更多信息,只需要知道它们有一个complete_task()方法,将刚刚完成的任务作为参数。

每当您想要动态地将调用源从被调用的代码中分离出来时,这是一条可行之路。这个问题是另一个非常常见的问题,如此常见,以至于这是另一个设计模式——观察者模式。如果你从我们正在看的问题后退一步,仅仅从一般意义上考虑观察者模式,它看起来会像这样。

模式中有两种类型的对象,一个是可以被其他类监视的Observable类,另一个是当两个类连接的Observable对象发生变化时会被警告的Observer类。

在最初的“四人帮”一书中,观察者设计模式被定义如下:一种软件设计模式,其中一个称为主题的对象维护一个称为观察者的依赖者列表,并自动通知它们任何状态变化,通常是通过调用它们的方法之一。它主要用于实现分布式事件处理系统[Gamma,e;赫尔姆河;约翰逊河;Vlissides,j;设计模式:可重用的面向对象软件的要素。

在代码中,它看起来像这样:

import abc

class Observer(object):
    __metaclass__ = abc.ABCMeta

    @abc.abstractmethod
    def update(self, observed): pass

class ConcreteObserver(Observer):

    def update(self, observed):
        print("Observing: " + observed)

class Observable(object):

    def __init__(self):
        self.observers = set()

    def register(self, observer):
        self.observers.add(observer)

    def unregister(self, observer):
        self.observers.discard(observer)

    def unregister_all(self):
        self.observers = set()

    def update_all(self):
        for observer in self.observers:
            observer.update(self)

使用Abstract基类产生了一个类似 Java 的接口,它迫使观察者实现update()方法。正如我们之前看到的,由于 Python 的动态特性,不需要Abstract基类,因此前面的代码可以被更 Python 化的版本替换:

class ConcreteObserver(object):

    def update(self, observed):
        print("Observing: " + observed)

class Observable(object):

    def __init__(self):
        self.observers = set()

    def register(self, observer):
        self.observers.add(observer)

    def unregister(self, observer):
        self.observers.discard(observer)

    def unregister_all(self):
        self.observers = set()

    def update_all(self):
        for observer in self.observers:
            observer.update(self)

在前面的代码中,Observable将观察它的所有对象的记录保存在一个名为 observers 的列表中,每当Observable中发生相关变化时,它只需为每个观察者运行update()方法。每次调用update()函数并将Observable对象作为参数传递时,您都会注意到这一点。这是一般的做法,但是任何参数,甚至没有参数,都可以传递给观察者,代码仍然遵循观察者模式。

如果我们能够向不同的对象发送不同的参数,那将会非常有趣,因为如果我们不再需要传递已经发生变化的整个对象,我们将会大大提高调用的效率。让我们利用 Python 的动态特性使我们的观察者模式变得更好。为此,我们将回到我们的用户任务系统。


general_observer.py

class ConcreteObserver(object):

    def update(self, observed):
        print("Observing: {}".format(observed))

class Observable(object):

    def __init__(self):
        self.callbacks = set()

    def register(self, callback):
        self.callbacks.add(callback)

    def unregister(self, callback):
        self.callbacks.discard(callback)

    def unregister_all(self):
        self.callbacks = set()

    def update_all(self):
        for callback in self.callbacks:
            callback(self)

def main():
    observed = Observable()
    observer1 = ConcreteObserver()

    observed.register(lambda x: observer1.update(x))

    observed.update_all()

if __name__ == "__main__":
    main()

虽然有很多方法可以将Observable的状态改变时可能发生的动作串联起来,但这只是两个具体的例子。

有时,您可能会发现您更希望系统的其余部分在特定的时间更新,而不是在对象发生变化时更新。为了方便这个需求,我们将在一个受保护的变量中添加一个changed标志(记住,Python 不会显式阻止对这个变量的访问;更多的是约定俗成),可以根据需要设置和取消设置。然后,定时功能将只处理变化,并在期望的时间提醒Observable对象的观察者。您可以使用 observer 模式的任何实现并结合标志。在下面的例子中,我使用了函数观察器。作为练习,在带有观察者对象集的示例中实现changed的标志。

import time

class ConcreteObserver(object):

    def update(self, observed):
        print("Observing: {}".format(observed))

class Observable(object):

    def __init__(self):
        self.callbacks = set()
        self.changed = False

    def register(self, callback):
        self.callbacks.add(callback)

    def unregister(self, callback):
        self.callbacks.discard(callback)

    def unregister_all(self):
        self.callbacks = set()

    def poll_for_change(self):
        if self.changed:
            self.update_all

    def update_all(self):
        for callback in self.callbacks:
            callback(self)

def main():
    observed = Observable()
    observer1 = ConcreteObserver()

    observed.register(lambda x: observer1.update(x))

    while True:
        time.sleep(3)
        observed.poll_for_change()

if __name__ == "__main__":
    main()

观察者模式解决的问题是,一组对象必须对其他对象的状态变化做出响应,并且在不引起系统内部更多耦合的情况下这样做。从这个意义上来说,观察者模式关心的是事件的管理或者对某种对象网络中的状态变化的响应。

我提到了耦合,所以让我澄清一下当我们谈论耦合时的含义。一般来说,当我们谈论对象之间的耦合程度时,我们指的是一个对象相对于与之交互的其他对象所需的知识程度。对象耦合得越松散,它们对彼此的了解就越少,面向对象的系统就越灵活。松散耦合的系统在对象之间具有较少的相互依赖性,因此更容易更新和维护。通过减少对象之间的耦合,您可以进一步降低在代码的某个部分进行更改会对代码的其他部分产生意想不到的后果的风险。由于对象之间不相互依赖,单元测试和故障排除变得更加容易。

使用观察者模式的其他好地方包括传统的模型-视图-控制器设计模式,您将在本书的后面遇到,以及数据的文本描述,每当底层数据发生变化时,需要更新该描述。

通常,只要单个对象(Observable)和一组观察者之间存在发布-订阅关系,就有了一个很好的观察者模式的候选对象。这种类型架构的一些其他例子是您在网上找到的许多不同类型的提要,如新闻提要、Atom、RSS 和播客。有了所有这些,发布者不关心谁是订阅者,因此您有一个自然的关注点分离。观察者模式通过提高订阅者和发布者之间的分离程度,使得在运行时添加或删除订阅者变得容易。

现在,让我们从本章开始使用观察者模式的 pythonic 实现来实现我们的跟踪系统。请注意,向代码中添加新类是多么容易,更新和更改任何现有类的过程是多么简单。

class Task(object):

    def __init__(self, user, _type):
        self.observers = set()
        self.user = user
        self._type = _type

    def register(self, observer):
        self.observers.add(observer)

    def unregister(self, observer):
        self.observers.discard(observer)

    def unregister_all(self):
        self.observers = set()

    def update_all(self):
        for observer in self.observers:
            observer.update(self)

class User(object):

    def __init__(self, wallet):
        self.wallet = wallet
        self.badges = []
        self.experience = 0

    def add_experience(self, amount):
        self.experience += amount

    def update(self, observed):
        self.add_experience(1)

    def __str__(self):
        return "Wallet\t{}\nExperience\t{}\n+ Badges +\n{}\n++++++++++++++++".format(
            self.wallet,
            self.experience,
            "\n".join([ str(x) for x in self.badges])
        )

class Wallet(object):

    def __init__(self):
        self.amount = 0

    def increase_balance(self, amount):
        self.amount += amount

    def decrease_balance(self, amount):
        self.amount -= amount

    def update(self, observed):
        self.increase_balance(5)

    def __str__(self):
        return str(self.amount)

class Badge(object):

    def __init__(self, name, _type):
        self.points = 0
        self.name = name
        self._type = _type
        self.awarded = False

    def add_points(self, amount):
        self.points += amount

        if self.points > 3:
            self.awarded = True

    def update(self, observed):
        if observed._type == self._type:
            self.add_points(2)

    def __str__(self):
        if self.awarded:
            award_string = "Earned"
        else:
            award_string = "Unearned"

        return "{}: {} [{}]".format(
            self.name,
            award_string,
            self.points
        )

def main():
    wallet = Wallet()
    user = User(wallet)

    badges = [
        Badge("Fun Badge", 1),
        Badge("Bravery Badge", 2),
        Badge("Missing Badge", 3)
    ]

    user.badges.extend(badges)

    tasks = [Task(user, 1), Task(user, 1), Task(user, 3)]

    for task in tasks:
        task.register(wallet)
        task.register(user)
        for badge in badges:
            task.register(badge)

    for task in tasks:
        task.update_all()

    print(user)

if __name__ == "__main__":
    main()

临别赠言

到目前为止,您应该对 observer 模式有了清晰的理解,并且对在现实世界中实现这种模式如何让您构建更健壮、更易于维护并且易于扩展的系统有了一种感觉。

练习

  • 使用观察者模式来建模一个系统,在这个系统中,观察者可以订阅股票市场中的股票,并根据股票价格的变化做出买入/卖出决策。
  • 在带有观察者对象集的示例上实现changed的标志。

十五、状态模式

Under pressure. -Queen, Under Pressure

考虑软件问题的一个非常有用的工具是状态图。在状态图中,您构建一个图形,其中节点表示系统的状态,边是系统中一个节点与另一个节点之间的转换。状态图是有用的,因为它们让你在给定某些输入的情况下直观地思考系统的状态。你还会被引导去考虑你的系统从一种状态转换到下一种状态的方式。

由于我们在本书前面提到了创建游戏,我们可以将玩家角色建模为一个状态机。玩家可能以站立的状态开始。按向左或向右箭头键可能会将状态更改为向左移动或向右移动。向上箭头可以将角色从他们所处的任何状态带入跳跃状态,同样,向下按钮可以将角色带入蹲伏状态。尽管这是一个不完整的例子,你应该开始理解我们如何使用状态图。很明显,当向上箭头键被按下时,角色会向上跳,然后再下来;如果键没有被释放,角色将再次跳跃。释放任何一个键都将使角色从原来的状态回到站立状态。

另一个更正式的例子是 ATM 机。自动柜员机的简化状态机可能包括以下状态:

  • 等待
  • 接受卡
  • 接受 PIN 输入
  • 验证 PIN
  • 拒绝销
  • 获取交易选择
  • 每个事务的每个部分的一组状态
  • 正在完成交易
  • 返回卡
  • 打印单据
  • 配药单

特定的系统和用户操作将导致自动柜员机从一种状态转移到下一种状态。将卡插入机器将导致机器从waiting状态转换到accepting_card状态,依此类推。

一旦你为一个系统绘制了一个完整的状态图,你就会对系统的每一步应该做什么有一个相当完整的了解。您还将清楚地知道您的系统应该如何从一个步骤转移到另一个步骤。剩下的工作就是将状态图翻译成代码。

将图表转换成可运行代码的一个简单方法是创建一个表示状态机的对象。对象将有一个状态属性,这将决定它如何对输入做出反应。如果我们编写游戏角色的第一个例子,它看起来会像这样。

Window 系统有 curses 的问题,但是可以通过安装 Cygwin 来解决,Cygwin 提供了一个类似 Linux 的终端,可以很好地与 curses 库 curses 一起工作。要下载 Cygwin,请前往位于 https://www.cygwin.com/ 的主网站,下载与您的机器匹配的当前 DLL 版本。如果你发现你必须重新设置 Python,只要按照本书开头安装指南的 Linux 部分的步骤操作就可以了。

import time

import curses

def main():
    win = curses.initscr()
    curses.noecho()

    win.addstr(0, 0, "press the keys w a s d to initiate actions")
    win.addstr(1, 0, "press x to exit")
    win.addstr(2, 0, "> ")
    win.move(2, 2)

    while True:
        ch = win.getch()
        if ch is not None:
            win.move(2, 0)
            win.deleteln()
            win.addstr(2, 0, "> ")  
            if ch == 120:
                break

            elif ch == 97:   # a

                print("Running Left")
            elif ch == 100:  # d

                print("Running Right")
            elif ch == 119:  # w

                print("Jumping")
            elif ch == 115:  # a

                print("Crouching")
            else:
                print("Standing")
        time.sleep(0.05)

if __name__ == "__main__":
    main()

作为练习,你可以使用第四章的pygame模块扩展这段代码,创建一个可以在屏幕上跑和跳的角色,而不仅仅是打印出角色当前正在做的动作。看一下pygame文档,了解如何从一个文件(sprite sheet)加载一个 sprite 图像,而不仅仅是在屏幕上绘制一个简单的块,这可能是有用的。

到目前为止,您已经知道我们用来决定如何处理输入的if语句有问题。对你的代码感来说很难闻。由于状态图是面向对象系统的非常有用的表示,并且由此产生的状态机非常普遍,所以可以肯定有一种设计模式可以清除这种代码味道。您正在寻找的设计模式是状态模式。

状态模式

在抽象层次上,所有面向对象的系统都关心系统中的参与者,以及每个参与者的行为如何影响其他参与者和整个系统。这就是为什么状态机在模拟一个对象的状态和引起所述对象反应的事物时如此有用。

在面向对象的系统中,状态模式用于封装基于对象内部状态的行为变化。这种封装解决了我们在前面的例子中看到的单一条件语句。

这听起来很棒,但是如何实现呢?

你需要的是国家本身的某种代表。有时,您甚至希望所有的状态共享一些通用的功能,这些功能将被编码到State基类中。然后,您必须为状态机中的每个离散状态创建一个具体的State类。其中的每一个都有某种处理函数来处理输入并引起状态转换,还有一个函数来完成该状态所需的动作。

在下面的代码片段中,我们定义了一个空基类,具体的State类将从该基类继承。注意,和前面的章节一样,Python 的 duck-typing 系统允许您在这个最基本的实现中删除State类。为了清楚起见,我在代码片段中包含了基类State。具体的状态也缺少动作方法,因为这个实现不采取动作,因此这些方法将是空的。

class State(object):
    pass

class ConcreteState1(State):

    def __init__(self, state_machine):
        self.state_machine = state_machine

    def switch_state(self):
        self.state_machine.state = self.state_machine.state2

class ConcreteState2(State):

    def __init__(self, state_machine):
        self.state_machine = state_machine

    def switch_state(self):
        self.state_machine.state = self.state_machine.state1

class StateMachine(object):

    def __init__(self):
        self.state1 = ConcreteState1(self)
        self.state2 = ConcreteState2(self)
        self.state = self.state1

    def switch(self):
        self.state.switch_state()

    def __str__(self):
        return str(self.state)

def main():
    state_machine = StateMachine()
    print(state_machine)

    state_machine.switch()
    print(state_machine)

if __name__ == "__main__":
    main()

从结果中,您可以看到从ConcreteState1ConcreteState2的切换发生在哪里。StateMachine类表示执行的上下文,并向外界提供一个单一的接口。

<__main__.ConcreteState1 object at 0x7f184f7a7198>
<__main__.ConcreteState2 object at 0x7f184f7a71d0>

随着你在成为更好的程序员的道路上前进,你会发现自己花越来越多的时间去思考如何测试某些代码结构。状态机也不例外。那么,我们可以测试状态机的什么呢?

  1. 状态机正确初始化
  2. 每个具体的State类的动作方法做它应该做的事情,比如返回正确的值
  3. 对于给定的输入,机器转移到正确的后续状态
  4. Python 包括一个非常可靠的单元测试框架,毫不奇怪地被称为 unittest。

为了测试我们的通用状态机,我们可以使用下面的代码:

import unittest

class GenericStatePatternTest(unittest.TestCase):
    def setUp(self):
        self.state_machine = StateMachine()

    def tearDown(self):
        pass

    def test_state_machine_initializes_correctly(self):
        self.assertIsInstance(self.state_machine.state, ConcreteState1)

    def test_switch_from_state_1_to_state_2(self):
        self.state_machine.switch()

        self.assertIsInstance(self.state_machine.state, ConcreteState2)

    def test_switch_from_state2_to_state1(self):
        self.state_machine.switch()
        self.state_machine.switch()

        self.assertIsInstance(self.state_machine.state, ConcreteState1)

if __name__ == '__main__':
    unittest.main()

试验一下断言的选项,看看您还能想到什么有趣的测试。

我们现在将回到我们在本章开始时讨论的玩家角色是跑还是走的问题。我们将实现与之前相同的功能,但是这一次我们将使用状态模式。

import curses
import time

class State(object):
    def __init__(self, state_machine):
        self.state_machine = state_machine

    def switch(self, in_key):
        if in_key in self.state_machine.mapping:
            self.state_machine.state = self.state_machine.mapping[in_key]
        else:
            self.state_machine.state = self.state_machine.mapping["default"]

class Standing(State):
    def __str__(self):
        return "Standing"

class RunningLeft(State):
    def __str__(self):
        return "Running Left"

class RunningRight(State):
    def __str__(self):
        return "Running Right"

class Jumping(State):
    def __str__(self):
        return "Jumping"

class Crouching(State):
    def __str__(self):
        return "Crouching"

class StateMachine(object):

    def __init__(self):
        self.standing = Standing(self)
        self.running_left = RunningLeft(self)
        self.running_right = RunningRight(self)
        self.jumping = Jumping(self)
        self.crouching = Crouching(self)

        self.mapping = {
            "a": self.running_left,
            "d": self.running_right,
            "s": self.crouching,
            "w": self.jumping,
            "default": self.standing,
        }

        self.state = self.standing

    def action(self, in_key):
        self.state.switch(in_key)

    def __str__(self):
        return str(self.state)

def main():
    player1 = StateMachine()
    win = curses.initscr()
    curses.noecho()

    win.addstr(0, 0, "press the keys w a s d to initiate actions")
    win.addstr(1, 0, "press x to exit")
    win.addstr(2, 0, "> ")
    win.move(2, 2)

    while True:
        ch = win.getch()
        if ch is not None:
            win.move(2, 0)
            win.deleteln()
            win.addstr(2, 0, "> ")  
            if ch == 120:
                break

            player1.action(chr(ch))
            print(player1.state)
        time.sleep(0.05)

if __name__ == "__main__":
    main()

你对修改后的代码有什么看法?你喜欢它的什么?你学到了什么?你认为有哪些可以改进的地方?

我想鼓励你开始在线查看代码,并问自己这些问题。你经常会发现,从阅读别人的代码中学到的东西比你在网上找到的所有教程都要多。这也是你在学习中又向前迈进了一大步的地方,当你开始批判性地思考你在网上找到的代码,而不是仅仅将其复制到你的项目中,并希望得到最好的结果。

临别赠言

在这一章中,我们发现了如何将 Python 中的实际代码与解决多种类型问题的抽象工具(即状态机)紧密地联系起来。

所有的状态机都是由状态和基于某些输入的从一个状态到另一个状态的转换组成的。通常,在转换到另一个状态之前,状态机也会在一个状态中执行一些动作。

我们还研究了可以用来在 Python 中构建自己的状态机的实际代码。

这里有一些快速简单的方法来构建你自己的基于状态机的解决方案。

  1. 识别您的机器可能处于的状态,例如runningwalking,或者在交通灯的情况下,redyellowgreen
  2. 确定您期望每个状态的不同输入。
  3. 根据输入绘制从当前状态到下一个状态的转换。注意转换线上的输入。
  4. 定义机器在每种状态下采取的行动。
  5. 将共享动作抽象到基类State中。
  6. 为您确定的每个状态实现具体的类。
  7. 实现一组转换方法来处理每个状态的预期输入。
  8. 实现机器在每个状态下需要采取的动作。记住,这些动作存在于具体的State类和基类State中。

这就是了——一个完全实现的状态机,它与解决问题的抽象图有一对一的关系。

练习

  • 通过使用 pygame 为玩家角色扩展简单的状态机来创建一个可视化版本。为玩家加载一个精灵,然后为跳跃动作建立一些基本的物理模型。
  • 探索作为 Python unittest 库的一部分的不同类型的assert语句。

十六、策略模式

在沉默中行动,只有在该说将死的时候才说话。—未知

有时,你可能会发现自己处于一个需要在不同的解决问题的方法之间转换的位置。本质上,您希望能够在运行时选择一个策略,然后执行它。每种策略可能都有自己的优势和劣势。假设您想将两个值缩减为一个值。假设这些值是数值,您有几个减少它们的选项。例如,考虑使用简单的加法和减法作为减少这两个数的策略。姑且称之为arg1arg2。一个简单的解决方案是这样的:

def reducer(arg1, arg2, strategy=None):
    if strategy == "addition":
        print(arg1 + arg2)
    elif strategy == "subtraction":
        print(arg1 - arg2)
    else:
        print("Strategy not implemented...")

def main():
    reducer(4, 6)
    reducer(4, 6, "addition")
    reducer(4, 6, "subtraction")

if __name__ == "__main__":
    main()

该解决方案会导致以下结果:

Strategy not implemented...
10
-2

这就是我们想要的。可悲的是,我们遇到了与前几章相同的问题,即每当我们想向缩减器添加另一个策略时,我们必须向函数添加另一个elif语句以及另一个代码块来处理该策略。这是一个扩展语句的可靠方法。我们更希望有一个更加模块化的解决方案,允许我们动态地传递新的策略,而不必修改使用或执行该策略的代码。

正如你现在所期望的,我们已经有了一个设计模式。

这种设计模式被恰当地命名为策略模式,因为它允许我们编写使用某种策略的代码,在运行时选择,除了知道它遵循某种执行签名之外,不知道关于该策略的任何事情。

我们再次求助于一个物体来帮助我们解决这个问题。我们还将触及 Python 将函数视为一等公民的事实,这将使该模式的实现比原始实现更加清晰。

我们将从策略模式的传统实现开始。

class StrategyExecutor(object):
    def __init__(self, strategy=None):
        self.strategy = strategy

    def execute(self, arg1, arg2):
        if self.strategy is None:
            print("Strategy not implemented...")
        else:
            self.strategy.execute(arg1, arg2)

class AdditionStrategy(object):
    def execute(self, arg1, arg2):
        print(arg1 + arg2)

class SubtractionStrategy(object):
    def execute(self, arg1, arg2):
        print(arg1 - arg2)

def main():
    no_strategy = StrategyExecutor()
    addition_strategy = StrategyExecutor(AdditionStrategy())
    subtraction_strategy = StrategyExecutor(SubtractionStrategy())

    no_strategy.execute(4, 6)
    addition_strategy.execute(4, 6)
    subtraction_strategy.execute(4, 6)

if __name__ == "__main__":
    main()

这再次导致所需的输出:

Strategy not implemented...
10
-2

至少我们处理了冗长的 if 语句,以及每次添加另一个策略时更新executor函数的需要。这是朝着正确方向迈出的良好一步。我们的系统更加松散,程序的每个部分只处理它所关心的执行部分,而不关心系统中的其他元素。

在传统的实现中,我们使用了 duck 类型,正如我们在本书中多次使用的那样。现在,我们将使用另一个强大的 Python 工具来编写干净的代码——使用函数,就像它们是任何其他值一样。这意味着我们可以将一个函数传递给Executor类,而不需要先将函数包装在自己的类中。从长远来看,这不仅会大大减少我们必须编写的代码量,而且还会使我们的代码更容易阅读和测试,因为我们可以将参数传递给函数,并断言它们返回我们期望的值。

class StrategyExecutor(object):

    def __init__(self, func=None):
        if func is not None:
            self.execute = func

    def execute(self, *args):
        print("Strategy not implemented...")

def strategy_addition(arg1, arg2):
    print(arg1 + arg2)

def strategy_subtraction(arg1, arg2):
    print(arg1 - arg2)

def main():
    no_strategy = StrategyExecutor()
    addition_strategy = StrategyExecutor(strategy_addition)
    subtraction_strategy = StrategyExecutor(strategy_subtraction)

    no_strategy.execute(4, 6)
    addition_strategy.execute(4, 6)
    subtraction_strategy.execute(4, 6)

if __name__ == "__main__":
    main()

同样,我们得到了所需的结果:

Strategy not implemented...
10
-2

既然我们已经在传递函数,我们不妨利用拥有一级函数的优势,在实现中放弃Executor对象,留给我们一个非常优雅的动态策略问题的解决方案。

def executor(arg1, arg2, func=None):
    if func is None:
        print("Strategy not implemented...")
    else:
        func(arg1, arg2)

def strategy_addition(arg1, arg2):
    print(arg1 + arg2)

def strategy_subtraction(arg1, arg2):
    print(arg1 - arg2)

def main():
    executor(4, 6)
    executor(4, 6, strategy_addition)
    executor(4, 6, strategy_subtraction)

if __name__ == "__main__":
    main()

和以前一样,您可以看到输出符合要求:

Strategy not implemented...
10
-2

我们创建了一个函数,它可以接受一对参数和一个在运行时减少它们的策略。乘法或除法策略,或者任何将两个值缩减为一个值的二元运算,都可以定义为一个函数并传递给缩减器。我们不会冒在我们的执行程序中蔓延开来并导致代码腐烂的风险。

关于前面的代码片段,有一件事有点麻烦,那就是 executor 中的else语句。我们知道,由于程序内部发生了一些事情,我们通常不会在终端中打印文本。代码更有可能返回一些值。由于 print 语句用于以有形的方式展示我们在策略模式中处理的概念,我将实现策略模式,在 main 函数中使用 print 语句简单地打印出执行器的结果。这将允许我们使用早期返回来摆脱悬空的else,从而更好地清理我们的代码。

我们代码的清理版本现在看起来像这样:

def executor(arg1, arg2, func=None):
    if func is None:
        return "Strategy not implemented..."

    return func(arg1, arg2)

def strategy_addition(arg1, arg2):
    return arg1 + arg2

def strategy_subtraction(arg1, arg2):
    return arg1 - arg2

def main():
    print(executor(4, 6))
    print(executor(4, 6, strategy_addition))
    print(executor(4, 6, strategy_subtraction))

if __name__ == "__main__":
    main()

我们再次测试代码产生的输出,我们在本章中看到:

Strategy not implemented...
10
-2

的确如此。现在,我们有了一个干净、清晰、简单的方法来在相同的环境中实施不同的策略。executor函数现在还使用早期返回来丢弃无效状态,这使得函数在单个级别上实际执行最佳情况时更容易阅读。

在现实世界中,你可能会考虑使用不同的策略来评估股票市场和做出购买决定;或者,你可以看看不同的寻路技术和转换策略。

临别赠言

破窗理论在代码中和在现实生活中一样有效。在你的代码中永远不要容忍破碎的窗口。破碎的窗口,如果你想知道的话,是那些你知道不正确并且不容易维护或扩展的代码片段。这是那种你真的想在 TODO 注释上打耳光的代码,你知道你需要回来修复,但永远不会。要成为一名更好的程序员,你需要对代码库的状态负责。在你拥有它之后,它应该比以前更好,更干净,更容易维护。下一次,当你想把待办事项作为一个陷阱留给那些从你身后走过的可怜的开发人员时,卷起你的袖子,完成你心中的修复。下一个不得不处理这些代码的可怜的编码员可能就是你,然后你会感谢之前来清理这些东西的编码员。

练习

  • 看看你是否能实现一个迷宫生成器,它将使用“#”和“”分别打印一个迷宫来表示墙壁和路径。在生成迷宫之前,让用户从三个策略中选择一个。使用策略模式将生成器策略传递给迷宫生成器。然后使用该策略生成迷宫。

十七、模板方法模式

没有重复的成功只不过是未来失败的伪装。—兰迪·盖奇在《如何建立一个多层次的赚钱机器:网络营销的科学》

在生活中,就像在编码中一样,有一些模式,一些你可以一步一步重复并得到预期结果的行为片段。在更复杂的情况下,特定步骤的细节可能会有所不同,但总体结构保持不变。在生活中,你可以编写标准的操作程序或者设计经验法则(或者心智模型,如果你就是这么做的话)。当我们写代码时,我们转向代码重用。

代码重用是带来了几乎无处不在的对面向对象编程的痴迷的乐土。梦想是你不仅在一个特定的项目中遵循 DRY 原则,而且在多个项目中遵循 DRY 原则,在你进行的过程中建立一套工具。每个项目不仅会让你成为一名更好的程序员,还会在你不断增长的武器库中产生另一套工具。有些人会争辩说,这本书的内容违反了 DRY 原则,因为一组出现足够多次的模式的存在意味着这些问题不需要再次解决。

完全的代码重用还没有实现,任何有几年经验的程序员都可以证明这一点。编程语言的激增和不断重新构思表明,这远远不是一个已解决的问题。

我不想打击士气,但是您需要意识到我们作为开发人员需要务实,并且我们经常需要做出不符合世界应该的方式的决定。对这些决定感到恼火,并渴望更好的解决方案,这不是问题,除非它阻止你做实际的工作。也就是说,使用像策略模式这样的模式允许我们改进做事的方式,而不需要重写整个程序。正如我们在本书中所讨论的,关注点的分离是另一种在事情发生变化时减少工作量的方法——事情总是在变化。

当我们确定了在各种情况下需要采取的行动的固定模式,每种情况都有其自身的细微差别时,我们该怎么办?

功能是配方的一种形式。考虑一下计算 n 的模式!,其中 n!= n * n-1 …… 1 其中 n > 2 否则 n!是 1。对于 n = 4 的情况,我们可以简单地写出:

fact_5 = 5 * 4 * 3 * 2 * 1

如果您感兴趣的唯一阶乘是 5 的阶乘,这没问题,但是有一个问题的一般解决方案会更有帮助,例如:

def fact(n):
    if n < 2:
        return 1

    return n * fact(n-1)

def main():
    print(fact(0))
    print(fact(1))
    print(fact(5))

if __name__ == "__main__":
    main()

我们在这里看到的是一个插图,其中一组特定的步骤导致了一个可预测的结果。函数在捕捉算法或一组步骤的想法方面非常出色,这些算法或步骤会导致某组输入值的预期结果。当我们开始考虑可能比简单地遵循一组预先定义的指令更复杂的情况时,这确实变得更加困难。另一个需要考虑的问题是,如果您想以不同的方式使用相同的步骤,或者使用更有效的算法来实现这些步骤,您应该怎么做。从阶乘函数的简单例子中可能看不清楚,所以让我们看一个更复杂的例子。

你的销售点系统突然流行起来,现在客户都想有各种搞笑的东西。这些请求的主要部分是与他们一直使用的第三方系统接口,以跟踪库存水平和价格变化。他们对替换现有系统不感兴趣,因为已经有客户在使用它,所以您必须进行集成。

您坐下来,确定与远程系统集成所需的步骤。

  • 在销售点和第三方系统之间同步库存项目。
  • 将交易发送给第三方。

这是一组简化的步骤,但对我们来说已经足够了。

在我们以前拥有的简单函数世界中,我们可以为流程中的每个步骤编写代码,并将每个步骤的代码放在单独的函数中,每个函数都可以在适当的时间被调用。看这里:

def sync_stock_items():
    print("running stock sync between local and remote system")
    print("retrieving remote stock items")
    print("updating local items")
    print("sending updates to third party")

def send_transaction(transaction):
    print("send transaction: {0!r}".format(transaction))

def main():
    sync_stock_items()

    send_transaction(
        {
            "id": 1,
            "items": [
                {
                    "item_id": 1,
                    "amount_purchased": 3,
                    "value": 238
                }
            ],
        }
    )

if __name__ == "__main__":
    main()

结果看起来像这样:

running stock sync between local and remote system
retrieving remote stock items
updating local items
sending updates to third party
send transaction: {'items': [{'amount_purchased': 3, 'item_id': 1, 'value': 238}], 'id': 1}

接下来,我们将根据现实世界中发生的事情来评估代码。如果有一个第三方系统需要集成,那么还会有其他的。如果您要集成的不是一个而是两个第三方应用,该怎么办?

您可以做简单的事情,创建几个杂乱无章的if语句,将函数内部的流量引导到您需要满足的三个系统中的每一个,产生类似下面的不完整代码片段的东西,包含它只是为了演示杂乱无章的if对代码整洁性的影响。

def sync_stock_items(system):
    if system == "system1":
        print("running stock sync between local and remote system1")
        print("retrieving remote stock items from system1")
        print("updating local items")
        print("sending updates to third party system1")
    elif system == "system2":
        print("running stock sync between local and remote system2")
        print("retrieving remote stock items from system2")
        print("updating local items")
        print("sending updates to third party system2")
    elif system == "system3":
        print("running stock sync between local and remote system3")
        print("retrieving remote stock items from system3")
        print("updating local items")
        print("sending updates to third party system3")
    else:
        print("no valid system")

def send_transaction(transaction, system):
    if system == "system1":
        print("send transaction to system1: {0!r}".format(transaction))
    elif system == "system2":
        print("send transaction to system2: {0!r}".format(transaction))
    elif system == "system3":
        print("send transaction to system3: {0!r}".format(transaction))
    else:
        print("no valid system")

我们在 main 函数中包含的测试用例的输出类似于下面的输出。请注意,由于字典不是按照它们的关键字排序的,所以每台机器打印的字典中条目的顺序可能会有所不同。重要的是整体结构保持不变,键值也是如此。

==========
running stock sync between local and remote system1
retrieving remote stock items from system1
updating local items
sending updates to third party system1
send transaction to system1: ({'items': [{'item_id': 1, 'value': 238, 'amount_purchased': 3}], 'id': 1},)
==========
running stock sync between local and remote system2
retrieving remote stock items from system2
updating local items
sending updates to third party system2
send transaction to system2: ({'items': [{'item_id': 1, 'value': 238, 'amount_purchased': 3}], 'id': 1},)
==========
running stock sync between local and remote system3
retrieving remote stock items from system3
updating local items
sending updates to third party system3
send transaction to system3: ({'items': [{'item_id': 1, 'value': 238, 'amount_purchased': 3}], 'id': 1},)

不仅要传递执行特定功能所需的参数,还要传递与系统当前用户相关的服务名称。从我们之前的讨论中也可以明显看出,从长远来看,以这种方式构建任何非平凡规模的系统都将是一场灾难。还剩下什么选择?因为这是一本关于设计模式的书,我们想提出一个使用设计模式来解决问题的解决方案——一个易于维护、更新和扩展的解决方案。我们希望能够添加新的第三方提供者,而无需对现有代码进行任何更改。我们可以尝试为这三个功能中的每一个实现策略模式,如下所示:

def sync_stock_items(strategy_func):
    strategy_func()

def send_transaction(transaction, strategy_func):
    strategy_func(transaction)

def stock_sync_strategy_system1():
    print("running stock sync between local and remote system1")
    print("retrieving remote stock items from system1")
    print("updating local items")
    print("sending updates to third party system1")

def stock_sync_strategy_system2():
    print("running stock sync between local and remote system2")
    print("retrieving remote stock items from system2")
    print("updating local items")
    print("sending updates to third party system2")

def stock_sync_strategy_system3():
    print("running stock sync between local and remote system3")
    print("retrieving remote stock items from system3")
    print("updating local items")
    print("sending updates to third party system3")

def send_transaction_strategy_system1(transaction):
    print("send transaction to system1: {0!r}".format(transaction))

def send_transaction_strategy_system2(transaction):
    print("send transaction to system2: {0!r}".format(transaction))

def send_transaction_strategy_system3(transaction):
    print("send transaction to system3: {0!r}".format(transaction))

def main():
    transaction = {
            "id": 1,
            "items": [
                {
                    "item_id": 1,
                    "amount_purchased": 3,
                    "value": 238
                }
            ],
        },

    print("="*10)
    sync_stock_items(stock_sync_strategy_system1)
    send_transaction(
        transaction,
        send_transaction_strategy_system1
    )
    print("="*10)
    sync_stock_items(stock_sync_strategy_system2)
    send_transaction(
        transaction,
        send_transaction_strategy_system2
    )
    print("="*10)
    sync_stock_items(stock_sync_strategy_system3)
    send_transaction(
        transaction,
        send_transaction_strategy_system1
    )

if __name__ == "__main__":
    main()

我们在 main 函数中包含的测试用例得到了与使用多个if语句的版本相同的结果。

==========
running stock sync between local and remote system1
retrieving remote stock items from system1
updating local items
sending updates to third party system1
send transaction to system1: ({'items': [{'item_id': 1, 'amount_purchased': 3, 'value': 238}], 'id': 1},)
==========
running stock sync between local and remote system2
retrieving remote stock items from system2
updating local items
sending updates to third party system2
send transaction to system2: ({'items': [{'item_id': 1, 'amount_purchased': 3, 'value': 238}], 'id': 1},)
==========
running stock sync between local and remote system3
retrieving remote stock items from system3
updating local items
sending updates to third party system3
send transaction to system1: ({'items': [{'item_id': 1, 'amount_purchased': 3, 'value': 238}], 'id': 1},)

关于这个实现,有两件事困扰着我。首先,我们所遵循的功能显然是同一个流程中的步骤,因此,它们应该存在于单个实体中,而不是分散的。我们也不希望传递一个基于系统的策略,这个系统是每一步的目标,因为这是对 DRY 原则的另一种违反。相反,我们需要做的是实现另一种称为模板方法模式的设计模式。

模板方法完全按照它在盒子上所说的去做。它提供了一个方法模板,可以按照这个模板一步一步地实现一个特定的过程,然后通过简单地修改几个细节,这个模板就可以用在许多不同的场景中。

从最普遍的意义上来说,模板方法模式在实现时看起来应该是这样的:

import abc

class TemplateAbstractBaseClass(metaclass=abc.ABCMeta):

    def template_method(self):
        self._step_1()
        self._step_2()
        self._step_n()

    @abc.abstractmethod
    def _step_1(self): pass

    @abc.abstractmethod
    def _step_2(self): pass

    @abc.abstractmethod
    def _step_3(self): pass

class ConcreteImplementationClass(TemplateAbstractBaseClass):

    def _step_1(self): pass

    def _step_2(self): pass

    def _step_3(self): pass

这是使用 Python 的Abstract基础类库真正有用的第一个例子。到目前为止,我们忽略了其他不太动态的语言经常使用的基类,因为我们可以依赖 duck-typing 系统。使用模板方法,事情有点不同。用于执行流程的方法包含在Abstract基类(ABC)中,从这个类继承的所有类都被强制以自己的方式实现每一步的方法,但是所有子类都会有相同的执行方法(除非因为某种原因被覆盖)。

现在,让我们用这个想法来实现使用模板方法模式的第三方集成。

import abc

class ThirdPartyInteractionTemplate(metaclass=abc.ABCMeta):

    def sync_stock_items(self):
        self._sync_stock_items_step_1()
        self._sync_stock_items_step_2()
        self._sync_stock_items_step_3()
        self._sync_stock_items_step_4()

    def send_transaction(self, transaction):
        self._send_transaction(transaction)

    @abc.abstractmethod
    def _sync_stock_items_step_1(self): pass

    @abc.abstractmethod
    def _sync_stock_items_step_2(self): pass

    @abc.abstractmethod
    def _sync_stock_items_step_3(self): pass

    @abc.abstractmethod
    def _sync_stock_items_step_4(self): pass

    @abc.abstractmethod
    def _send_transaction(self, transaction): pass

class System1(ThirdPartyInteractionTemplate):
    def _sync_stock_items_step_1(self):
        print("running stock sync between local and remote system1")

    def _sync_stock_items_step_2(self):    
        print("retrieving remote stock items from system1")

    def _sync_stock_items_step_3(self):
        print("updating local items")

    def _sync_stock_items_step_4(self):
        print("sending updates to third party system1")

    def _send_transaction(self, transaction):
        print("send transaction to system1: {0!r}".format(transaction))

class System2(ThirdPartyInteractionTemplate):
    def _sync_stock_items_step_1(self):
        print("running stock sync between local and remote system2")

    def _sync_stock_items_step_2(self):    
        print("retrieving remote stock items from system2")

    def _sync_stock_items_step_3(self):
        print("updating local items")

    def _sync_stock_items_step_4(self):
        print("sending updates to third party system2")

    def _send_transaction(self, transaction):
        print("send transaction to system2: {0!r}".format(transaction))

class System3(ThirdPartyInteractionTemplate):
    def _sync_stock_items_step_1(self):
        print("running stock sync between local and remote system3")

    def _sync_stock_items_step_2(self):    
        print("retrieving remote stock items from system3")

    def _sync_stock_items_step_3(self):
        print("updating local items")

    def _sync_stock_items_step_4(self):
        print("sending updates to third party system3")

    def _send_transaction(self, transaction):
        print("send transaction to system3: {0!r}".format(transaction))

def main():
    transaction = {
            "id": 1,
            "items": [
                {
                    "item_id": 1,
                    "amount_purchased": 3,
                    "value": 238
                }
            ],
        },

    for C in [System1, System2, System3]:
        print("="*10)
        system = C()
        system.sync_stock_items()
        system.send_transaction(transaction)

if __name__ == "__main__":
    main()

同样,我们的测试代码产生了我们希望的输出。与上一节一样,您的实现返回的字典中的键值对的顺序可能不同,但是结构和值将保持一致。

==========
running stock sync between local and remote system1
retrieving remote stock items from system1
updating local items
sending updates to third party system1
send transaction to system1: ({'items': [{'amount_purchased': 3, 'value': 238, 'item_id': 1}], 'id': 1},)
==========
running stock sync between local and remote system2
retrieving remote stock items from system2
updating local items
sending updates to third party system2
send transaction to system2: ({'items': [{'amount_purchased': 3, 'value': 238, 'item_id': 1}], 'id': 1},)
==========
running stock sync between local and remote system3
retrieving remote stock items from system3
updating local items
sending updates to third party system3
send transaction to system3: ({'items': [{'amount_purchased': 3, 'value': 238, 'item_id': 1}], 'id': 1},)

临别赠言

知道在哪里使用哪种模式,以及在哪里不依赖于本书或任何其他书中的任何模式,是一种直觉。就像武术一样,只要有机会,练习实现这些模式是很有帮助的。当你实施它们的时候,要知道什么是有效的,什么是无效的。模式在哪里帮助了你,在哪里阻碍了你的进步?你的目标是发展一种感觉,用一个清楚的和已知的实现,一个特定的模式可以解决的问题,以及模式会在哪里碍事。

因为我刚刚提到了知识和实验的需要,所以我建议您查看您最喜欢的编辑器的文档,并找出它是否有某种代码片段功能,您可以使用它来处理您发现自己重复编写的一些样板代码。这可能包括您用来在新文件中开始编码的序言,或者围绕main()函数的结构。这又回到了一个想法,如果你每天都要使用一个工具,你真的需要掌握这个工具。舒适地使用代码片段,尤其是创建自己的代码片段,是获得额外的工具杠杆和删除一些浪费的击键的好地方。

练习

  • 实现第四个系统,这一次看看当你省略其中一个步骤时会发生什么。
  • 探索与试图保持两个系统同步相关的挑战,而不能停止整个世界并很好地将一切联系在一起;除了其他因素,你还需要考虑比赛条件。
  • 想想其他一些系统,在这些系统中,您知道需要采取什么步骤,但是每个步骤中要做什么的细节因情况而异。
  • 实现一个基于基本模板模式的系统来模拟您在前面的练习中想到的情况。

十八、访问者模式

我想相信。—X 档案

因为 Python 可以在很多地方找到,所以有一天你可能想做一点家庭自动化。找几台单板和微型计算机,把它们连接到一些硬件传感器和执行器上,很快你就有了一个设备网络,全部由你控制。网络中的每个项目都有自己的功能,每个项目都执行不同的交互,如测量家中的光线水平或检查温度。您可以将每个设备的功能封装为与物理有线网络相匹配的虚拟网络中的一个对象,然后像对待任何其他对象网络一样对待整个系统。本章将关注实现的这一方面,假设所有的元素都已经被连接起来,并被建模为系统中的对象。

我们模拟的网络将包含以下组件:

  • 恒温器
  • 温度调节器
  • 前门门锁
  • 咖啡机
  • 卧室灯
  • 厨房灯
  • 时钟

假设每个对象类都具有检查与其连接的设备状态的功能。这些函数返回不同的值。当无法联系到灯光控制器时,灯光可能会返回1表示“灯光打开”,返回0表示“灯光关闭”,并返回一条错误消息-1。恒温器可能会返回它正在读取的实际温度,如果它离线,则返回None。前门锁和灯类似,1上锁,0解锁,-1出错。咖啡机具有分别使用从-1 到 4 的整数的错误、关闭、开启、冲泡、等待和加热状态。温度调节器有加热、冷却、开、关和错误状态。如果设备断开连接,时钟返回None,或者返回 Python 时间值形式的时间。

与具体实例相关的类看起来会像这样:

import random

class Light(object):
    def __init__(self):
        pass

    def get_status(self):
        return random.choice(range(-1,2))

class Thermostat(object):
    def __init__(self):
        pass

    def get_status(self):
        temp_range = [x for x in range(-10, 31)]
        temp_range.append(None)
        return random.choice(temp_range)

class TemperatureRegulator(object):
    def __init__(self):
        pass

    def get_status(self):
        return random.choice(['heating', 'cooling', 'on', 'off', 'error'])

class DoorLock(object):
    def __init__(self):
        pass

    def get_status(self):
        return random.choice(range(-1,2))

class CoffeeMachine(object):
    def _init_(self):
        pass

    def get_status(self):
        return random.choice(range(-1,5))

class Clock(object):
    def __init__(self):
        pass

    def get_status(self):
        return "{}:{}".format(random.randrange(24), random.randrange(60))

def main():
    device_network = [
        Thermostat(),
        TemperatureRegulator(),
        DoorLock(),
        CoffeeMachine(),
        Light(),
        Light(),
        Clock(),
    ]

    for device in device_network:
        print(device.get_status())

if __name__ == "__main__":
    main()

输出有些混乱,如下所示:

0
off
-1
2
0
-1
9:26

这比您在现实世界中遇到的输出类型要干净得多,但这很好地体现了现实世界设备的混乱本质。我们现在有了一个设备网络的模拟。我们可以转移到我们真正感兴趣的部分,也就是用这些设备做一些事情。

我们感兴趣的第一件事是检查连接到对象的设备的状态,以确定设备是否在线。让我们创建一个包含网络中节点的平面集合,然后实现一个is_online方法来告诉我们设备是否在线。然后,我们可以依次查看每个设备,并询问它是否在线。我们还为每个构造函数添加了一个name参数,这样我们可以很容易地看到我们当前正在处理什么设备。

import random

class Light(object):
    def __init__(self, name):
        self.name = name

    def get_status(self):
        return random.choice(range(-1,2))

    def is_online(self):
        return self.get_status() != -1

class Thermostat(object):
    def __init__(self, name):
        self.name = name

    def get_status(self):
        temp_range = [x for x in range(-10, 31)]
        temp_range.append(None)
        return random.choice(temp_range)

    def is_online(self):
        return self.get_status() is not None

class TemperatureRegulator(object):
    def __init__(self, name):
        self.name = name

    def get_status(self):
        return random.choice(['heating', 'cooling', 'on', 'off', 'error'])

    def is_online(self):
        return self.get_status() != 'error'

class DoorLock(object):
    def __init__(self, name):
        self.name = name

    def get_status(self):
        return random.choice(range(-1,2))

    def is_online(self):
        return self.get_status() != -1

class CoffeeMachine(object):
    def __init__(self, name):
        self.name = name

    def get_status(self):
        return random.choice(range(-1,5))

    def is_online(self):
        return self.get_status() != -1

class Clock(object):
    def __init__(self, name):
        self.name = name

    def get_status(self):
        return "{}:{}".format(random.randrange(24), random.randrange(60))

    def is_online(self):
        return True

def main():
    device_network = [
        Thermostat("General Thermostat"),
        TemperatureRegulator("Thermal Regulator"),
        DoorLock("Front Door Lock"),
        CoffeeMachine("Coffee Machine"),
        Light("Bedroom Light"),
        Light("Kitchen Light"),
        Clock("System Clock"),
    ]

    for device in device_network:
        print("{} is online: \t{}".format(device.name, device.is_online()))

if __name__ == "__main__":
    main()

因为模拟响应是随机生成的,所以您不应该期望您的输出与这里给出的输出完全匹配,但是输出的大致形状应该保持不变。

General Thermostat is online:   True
Thermal Regulator is online:    True
Front Door Lock is online:      True
Coffee Machine is online:       False
Bedroom Light is online:        False
Kitchen Light is online:        True
System Clock is online:         True

我们现在要添加一个引导序列来打开所有设备,并使它们进入初始状态,比如将时钟设置为 00:00,启动咖啡机,关闭所有的灯,打开但不设置温度系统的任何设置。我们会让前门保持原样。

由于我们现在对系统的实际状态感兴趣,我们将移除get_status方法,代之以在__init__()方法中将 status 属性设置为随机值。

您应该注意的另一件事是,我们现在从 unittest 库导入了TestCase类,这允许我们编写测试来确保在将引导序列应用到设备之后,该设备确实处于我们期望的状态。这不是一个关于单元测试的深入教程,但是对你来说很有帮助,这样你就可以更好地使用 Python 中的测试工具。

import random
import unittest

class Light(object):
    def __init__(self, name):
        self.name = name
        self.status = self.get_status()

    def get_status(self):
        return random.choice(range(-1,2))

    def is_online(self):
        return self.status != -1

    def boot_up(self):
        self.status = 0

class Thermostat(object):
    def __init__(self, name):
        self.name = name
        self.status = self.get_status()

    def get_status(self):
        temp_range = [x for x in range(-10, 31)]
        temp_range.append(None)
        return random.choice(temp_range)

    def is_online(self):
        return self.status is not None

    def boot_up(self):
        pass

class TemperatureRegulator(object):
    def __init__(self, name):
        self.name = name
        self.status = self.get_status()

    def get_status(self):s
        return random.choice(['heating', 'cooling', 'on', 'off', 'error'])

    def is_online(self):
        return self.status != 'error'

    def boot_up(self):
        self.status = 'on'

class DoorLock(object):
    def __init__(self, name):
        self.name = name
        self.status = self.get_status()

    def get_status(self):
        return random.choice(range(-1,2))

    def is_online(self):
        return self.status != -1

    def boot_up(self):
        pass

class CoffeeMachine(object):
    def __init__(self, name):
        self.name = name
        self.status = self.get_status()

    def get_status(self):
        return random.choice(range(-1,5))

    def is_online(self):
        return self.status != -1

    def boot_up(self):
        self.status = 1

class Clock(object):
    def __init__(self, name):
        self.name = name
        self.status = self.get_status()

    def get_status(self):
        return "{}:{}".format(random.randrange(24), random.randrange(60))

    def is_online(self):
        return True

    def boot_up(self):
        self.status = "00:00"

class HomeAutomationBootTests(unittest.TestCase):
    def setUp(self):
        self.thermostat = Thermostat("General Thermostat")
        self.thermal_regulator = TemperatureRegulator("Thermal Regulator")
        self.front_door_lock = DoorLock("Front Door Lock")
        self.coffee_machine = CoffeeMachine("Coffee Machine")
        self.bedroom_light = Light("Bedroom Light")
        self.system_clock = Clock("System Clock")

    def test_boot_thermostat_does_nothing_to_state(self):
        state_before = self.thermostat.status
        self.thermostat.boot_up()
        self.assertEqual(state_before, self.thermostat.status)

    def test_boot_thermal_regulator_turns_it_on(self):
        self.thermal_regulator.boot_up()
        self.assertEqual(self.thermal_regulator.status, 'on')

    def test_boot_front_door_lock_does_nothing_to_state(self):
        state_before = self.front_door_lock.status
        self.front_door_lock.boot_up()
        self.assertEqual(state_before, self.front_door_lock.status)

    def test_boot_coffee_machine_turns_it_on(self):
        self.coffee_machine.boot_up()
        self.assertEqual(self.coffee_machine.status, 1)

    def test_boot_light_turns_it_off(self):
        self.bedroom_light.boot_up()
        self.assertEqual(self.bedroom_light.status, 0)

    def test_boot_system_clock_zeros_it(self):
        self.system_clock.boot_up()
        self.assertEqual(self.system_clock.status, "00:00")

if __name__ == "__main__":
    unittest.main()

将程序从命令行运行时的执行函数设置为unittest.main()告诉 Python 寻找unittest.TestCase类的实例,然后在该类中运行测试。我们设置了六个测试,运行测试后的状态如下所示:

......
----------------------------------------------------------------------
Ran 6 tests in 0.001s

OK

每一个都让我们实现我们期望的功能,但是如果我们想要实现不同的概要文件,系统会是什么样子呢?说 person1 和 person2 合租房子,他们有不同的偏好,比如起床时间,睡觉时间,早上或晚上应该做什么。他们也有不同的温度,他们觉得最舒服。由于他们是友好的人,他们同意在两个人都在家的时候采取折中的方式,但是当一个人或另一个人在家的时候,他们希望将系统设置为他们的完美配置。

我们到目前为止看到的系统的一个简单扩展将会看到这样的实现:

import random
import unittest

class Light(object):
    def __init__(self, name):
        self.name = name
        self.status = self.get_status()

    def get_status(self):
        return random.choice(range(-1,2))

    def is_online(self):
        return self.status != -1

    def boot_up(self):
        self.status = 0

    def update_status(self, person_1_home, person_2_home):
        if person_1_home:
            if person_2_home:
                self.status = 1
            else:
                self.status = 0
        elif person_2_home:
            self.status = 1
        else:
            self.status = 0

class Thermostat(object):
    def __init__(self, name):
        self.name = name
        self.status = self.get_status()

    def get_status(self):
        temp_range = [x for x in range(-10, 31)]
        temp_range.append(None)
        return random.choice(temp_range)

    def is_online(self):
        return self.status is not None

    def boot_up(self):
        pass

    def update_status(self, person_1_home, person_2_home):
        pass

class TemperatureRegulator(object):
    def __init__(self, name):
        self.name = name
        self.status = self.get_status()

    def get_status(self):
        return random.choice(['heating', 'cooling', 'on', 'off', 'error'])

    def is_online(self):
        return self.status != 'error'

    def boot_up(self):
        self.status = 'on'

    def update_status(self, person_1_home, person_2_home):
        if person_1_home:
            if person_2_home:
                self.status = 'on'
            else:
                self.status = 'heating'
        elif person_2_home:
            self.status = 'cooling'
        else:
            self.status = 'off'

class DoorLock(object):
    def __init__(self, name):
        self.name = name
        self.status = self.get_status()

    def get_status(self):
        return random.choice(range(-1,2))

    def is_online(self):
        return self.status != -1

    def boot_up(self):
        pass

    def update_status(self, person_1_home, person_2_home):
        if person_1_home:
            self.status = 0
        elif person_2_home:
            self.status = 1
        else:
            self.status = 1

class CoffeeMachine(object):
    def __init__(self, name):
        self.name = name
        self.status = self.get_status()

    def get_status(self):
        return random.choice(range(-1,5))

    def is_online(self):
        return self.status != -1

    def boot_up(self):
        self.status = 1

    def update_status(self, person_1_home, person_2_home):
        if person_1_home:
            if person_2_home:
                self.status = 2
            else:
                self.status = 3
        elif person_2_home:
            self.status = 4
        else:
            self.status = 0

class Clock(object):
    def __init__(self, name):
        self.name = name
        self.status = self.get_status()

    def get_status(self):
        return "{}:{}".format(random.randrange(24), random.randrange(60))

    def is_online(self):
        return True

    def boot_up(self):
        self.status = "00:00"

    def update_status(self, person_1_home, person_2_home):
        if person_1_home:
            if person_2_home:
                pass

            else:
                "00:01"
        elif person_2_home:
            "20:22"
        else:
            pass

作为练习,为人员 1 和人员 2 在或不在的这些状态编写测试。确保你在这些测试中涵盖了所有可能的选项。

这已经是一个混乱的实现,你知道我们要做一些清理。我们可以使用一个状态机来实现这个系统,但是为房子的每个居住状态的每个设备实现一个状态机似乎是错误的;这也是大量没有价值的工作。一定有更好的方法。

为了让我们到达那里,让我们考虑我们想要做什么。

访问者模式

再一次,我们将把一个复杂的功能分解成更离散的部分,然后以一种不需要彼此非常熟悉的方式抽象这些部分。当您花时间优化、扩展和清理现有系统时,您会一次又一次地看到这种分离和隔离的模式。

有必要提一下 Martin Fowler 对开发基于微服务的架构的看法。Fowler 认为,首先必须开发 monolith,因为在开始时,你不知道哪些元素将结合起来形成良好的微服务,哪些元素可以保持分离。当你在一个系统上工作和成长时,一个单独的对象变成了一个对象的网络。当这些网络变得过于复杂时,您就要以在小范围内没有任何意义的方式重构、分离和清理代码。

这是“你不会需要它”原则(YAGNI)的另一个高级实例,它只是恳求开发者不要构建不会被使用的功能或结构。这很像一个年轻的开发人员,他在创建一个新的对象后,立即着手添加创建、读取、更新和删除功能,即使该对象永远不会被更新(或者任何适用的业务规则)。立即将任何功能或想法抽象成某种元类会在您的系统中留下大量死代码,这些代码需要维护、推理、调试和测试,但从未真正使用过。新的团队成员将需要费力地阅读这些代码,直到在整个系统被分析之后才意识到这是浪费时间和精力。如果这种类型的代码在系统中扩散,那么完全重写的压力会变得更大,更多的时间会被浪费。如果再次实现死代码,新的开发人员将直接面对同样的问题。简而言之,不要写你不打算使用的代码。

一个很好的经验法则是,等到你必须第三次做同样的事情(或非常相似的事情)时,再进行抽象。这条规则的另一面是,一旦你在项目中第三次遇到相同的需求或问题,你不应该在没有抽象出解决方案的情况下继续,这样无论何时你遇到这种情况,你都会有一个现成的解决方案。你现在找到了平衡。

记住这一点,假设您需要修改对象两次,以允许在整个对象集合上执行某些功能,现在您有了第三个算法,您希望针对该结构实现该算法。很明显,您希望从数据结构的角度抽象出正在实现的算法。理想情况下,您希望能够动态添加新的算法,并使它们相对于相同的数据结构执行,而无需对构成所述数据结构元素的类进行任何更改。

查看访问者模式的一般实现,并对代码有所了解。在这段代码之后,我们将深入研究细节。

import abc

class Visitable(object):
    def accept(self, visitor):
        visitor.visit(self)

class CompositeVisitable(Visitable):
    def __init__(self, iterable):
      self.iterable = iterable

    def accept(self, visitor):
      for element in self.iterable:
        element.accept(visitor)

      visitor.visit(self)

class AbstractVisitor(object):
    __metaclass__ = abc.ABCMeta

    @abc.abstractmethod
    def visit(self, element):
        raise NotImplementedError("A visitor needs to define a visit method")

class ConcreteVisitable(Visitable):
    def __init__(self):
        pass

class ConcreteVisitor(AbstractVisitor):
    def visit(self, element):
      pass

我们现在可以回到我们的朋友合住一所房子的例子。让我们使用 visitor 模式为它们中的每一个抽象系统设置。为此,我们将为室内人员的三种潜在配置中的每一种配置创建一个访问者,然后在访问每台设备并进行相应设置之前检查谁在家。

import abc
import random
import unittest

class Visitable(object):
    def accept(self, visitor):
        visitor.visit(self)

class CompositeVisitable(Visitable):
    def __init__(self, iterable):
      self.iterable = iterable

    def accept(self, visitor):
      for element in self.iterable:
        element.accept(visitor)

      visitor.visit(self)

class AbstractVisitor(object):
    __metaclass__ = abc.ABCMeta

    @abc.abstractmethod
    def visit(self, element):
        raise NotImplementedError("A visitor need to define a visit method")

class Light(Visitable):
    def __init__(self, name):
        self.name = name
        self.status = self.get_status()

    def get_status(self):
        return random.choice(range(-1,2))

    def is_online(self):
        return self.status != -1

    def boot_up(self):
        self.status = 0

class LightStatusUpdateVisitor(AbstractVisitor):
    def __init__(self, person_1_home, person_2_home):
        self.person_1_home = person_1_home
        self.person_2_home = person_2_home

    def visit(self, element):
        if self.person_1_home:
            if self.person_2_home:
                element.status = 1
            else:
                element.status = 0
        elif self.person_2_home:
            element.status = 1
        else:
            element.status = 0

class Thermostat(Visitable):
    def __init__(self, name):
        self.name = name
        self.status = self.get_status()

    def get_status(self):
        temp_range = [x for x in range(-10, 31)]
        temp_range.append(None)
        return random.choice(temp_range)

    def is_online(self):
        return self.status is not None

    def boot_up(self):
        pass

class ThermostatStatusUpdateVisitor(AbstractVisitor):
    def __init__(self, person_1_home, person_2_home):
        self.person_1_home = person_1_home
        self.person_2_home = person_2_home

    def visit(self, element):
        pass

class TemperatureRegulator(Visitable):
    def __init__(self, name):
        self.name = name
        self.status = self.get_status()

    def get_status(self):
        return random.choice(['heating', 'cooling', 'on', 'off', 'error'])

    def is_online(self):
        return self.status != 'error'

    def boot_up(self):
        self.status = 'on'

class TemperatureRegulatorStatusUpdateVisitor(AbstractVisitor):
    def __init__(self, person_1_home, person_2_home):
        self.person_1_home = person_1_home
        self.person_2_home = person_2_home

    def visit(self, element):
        if self.person_1_home:
            if self.person_2_home:
                element.status = 'on'
            else:
                element.status = 'heating'
        elif self.person_2_home:
            element.status = 'cooling'
        else:
            element.status = 'off'

class DoorLock(Visitable):
    def __init__(self, name):
        self.name = name
        self.status = self.get_status()

    def get_status(self):
        return random.choice(range(-1,2))

    def is_online(self):
        return self.status != -1

    def boot_up(self):
        pass

class DoorLockStatusUpdateVisitor(AbstractVisitor):
    def __init__(self, person_1_home, person_2_home):
        self.person_1_home = person_1_home
        self.person_2_home = person_2_home

    def visit(self, element):
        if self.person_1_home:
            element.status = 0
        elif self.person_2_home:
            element.status = 1
        else:
            element.status = 1

class CoffeeMachine(Visitable):
    def __init__(self, name):
        self.name = name
        self.status = self.get_status()

    def get_status(self):
        return random.choice(range(-1,5))

    def is_online(self):
        return self.status != -1

    def boot_up(self):
        self.status = 1

class CoffeeMachineStatusUpdateVisitor(AbstractVisitor):
    def __init__(self, person_1_home, person_2_home):
        self.person_1_home = person_1_home
        self.person_2_home = person_2_home

    def visit(self, element):
        if self.person_1_home:
            if self.person_2_home:
                element.status = 2
            else:
                element.status = 3
        elif self.person_2_home:
            element.status = 4
        else:
            element.status = 0

class Clock(Visitable):
    def __init__(self, name):
        self.name = name
        self.status = self.get_status()

    def get_status(self):
        return "{}:{}".format(random.randrange(24), random.randrange(60))

    def is_online(self):
        return True

    def boot_up(self):
        self.status = "00:00"

class ClockStatusUpdateVisitor(AbstractVisitor):
    def __init__(self, person_1_home, person_2_home):
        self.person_1_home = person_1_home
        self.person_2_home = person_2_home

    def visit(self, element):
        if self.person_1_home:
            if self.person_2_home:
                pass

            else:
                element.status = "00:01"
        elif self.person_2_home:
            element.status = "20:22"
        else:
            pass

class CompositeVisitor(AbstractVisitor):
    def __init__(self, person_1_home, person_2_home):
        self.person_1_home = person_1_home
        self.person_2_home = person_2_home

    def visit(self, element):
        try:
            c = eval("{}StatusUpdateVisitor".format(element.__class__.__name__))
        except:
            print("Visitor for {} not found".format(element.__class__.__name__))
        else:
            visitor = c(self.person_1_home, self.person_2_home)
            visitor.visit(element)

class MyHomeSystem(CompositeVisitable):
    pass

class MyHomeSystemStatusUpdateVisitor(AbstractVisitor):
    def __init__(self, person_1_home, person_2_home):
        self.person_1_home = person_1_home
        self.person_2_home = person_2_home

    def visit(self, element):
        pass

class HomeAutomationBootTests(unittest.TestCase):
    def setUp(self):
        self.my_home_system = MyHomeSystem([
            Thermostat("General Thermostat"),
            TemperatureRegulator("Thermal Regulator"),
            DoorLock("Front Door Lock"),
            CoffeeMachine("Coffee Machine"),
            Light("Bedroom Light"),
            Clock("System Clock"),
        ])

    def test_person_1_not_home_person_2_not_home(self):
        expected_state = map(
            str,
            [
                self.my_home_system.iterable[0].status,
                'off',
                1,
                0,
                0,
                self.my_home_system.iterable[5].status
            ]
        )
        self.visitor = CompositeVisitor(False, False)
        self.my_home_system.accept(self.visitor)
        retrieved_state = sorted([str(x.status) for x in self.my_home_system.iterable])
        self.assertEqual(retrieved_state, sorted(expected_state))

    def test_person_1_home_person_2_not(self):
        expected_state = map(
            str,
            [
                self.my_home_system.iterable[0].status,
                'heating',
                0,
                3,
                0,
                "00:01"
            ]
        )
        self.visitor = CompositeVisitor(True, False)
        self.my_home_system.accept(self.visitor)
        retrieved_state = sorted([str(x.status) for x in self.my_home_system.iterable])
        self.assertEqual(retrieved_state, sorted(expected_state))

    def test_person_1_not_home_person_2_home(self):
        expected_state = map(
            str,
            [
                self.my_home_system.iterable[0].status,
                'cooling',
                1,
                4,
                1,
                "20:22"
            ]
        )
        self.visitor = CompositeVisitor(False, True)
        self.my_home_system.accept(self.visitor)
        retrieved_state = sorted([str(x.status) for x in self.my_home_system.iterable])
        self.assertEqual(retrieved_state, sorted(expected_state))

    def test_person_1_home_person_2_home(self):
        expected_state = map(
            str,
            [
                self.my_home_system.iterable[0].status,
                'on',
                0,
                2,
                1,
                self.my_home_system.iterable[5].status
            ]
        )
        self.visitor = CompositeVisitor(True, True)
        self.my_home_system.accept(self.visitor)
        retrieved_state = sorted([str(x.status) for x in self.my_home_system.iterable])
        self.assertEqual(retrieved_state, sorted(expected_state))

if __name__ == "__main__":
    unittest.main()

我对前面的代码有几点注意。首先,如果您从用户那里获取输入,永远不要使用eval函数,因为 Python 会盲目地执行传递给eval的字符串中的任何内容。第二,我们选择通过使用要访问的类名来平衡eval的一般性质,这在 Python 中不是最好的方式,因为现在你依赖于命名约定和魔法,这是不明确的。尽管如此,您还是可以实现这个模式。

如果您认为状态机可能有助于改善这种情况,那么您没有错。这就给我们带来了一个看起来很明显,但可能一开始就不那么明显的东西,那就是你可以在一个实现中使用一个以上的设计模式。在这种情况下,应该清楚房子的状态类似于一个状态机。我们有四种状态:

  • 人员 1 不在家,人员 2 不在家
  • 人 1 不在家,人 2 在家
  • 人 1 在家,人 2 不在家
  • 一号人物在家,二号人物在家

每当一个人离家或到家时,系统的状态就会改变。因此,为访问者实现一个状态机似乎是一个好主意。

作为练习,尝试在前面的代码片段中实现状态模式。

当您将系统中的数据结构与操作它们的算法分离时,您会获得额外的好处。你可以更容易地坚持开闭原则。根据这一原则,在编写代码时,对象可以扩展(使用继承),但不能更改(通过设置或更改对象中的值,或者更改对象上的方法)。像德莱和 YAGNI 原则一样,开闭原则是你规划和开发系统的指路明灯。违反这些原则也可以作为一个预警系统,表明你正在编写不可维护的代码,这些代码最终会回来伤害你。

临别赠言

有时你只需要写代码,所以一行一行地输入例子,感受一下所表达的思想。每当我发现一个算法或想法的描述,而我不太理解它的描述方式时,我就会使用这个工具。当你坐下来写出代码而不仅仅是阅读它时,会发生一些不同的事情。通常,当您在代码中工作时,以前不清楚的细微差别会暴露出来。

对于您发现复杂或高级算法的设计模式,我建议打出通用版本,然后打出一些特定的实现,最后修改一些细节以适应您自己的示例,而不是您正在查看的文本中的示例。最后,为一个特定的实例从头开始实现模式或算法。

这种类型的工作通常不是编程中有趣的部分,但是为了成为一名更好的程序员,这是你需要做的有意识的练习。

一旦您了解了语法的简单元素和编写习惯性 Python 的更一般的概述,就有一大堆想法可以探索。那个世界是你从能够用语言写单词,或者能够把句子串在一起,到用代码表达思想的地方。从长远来看,这就是你想要变得更好的地方——用 Python——我们为这本书选择的语言——表达你的想法和推理的过程。就像所有的表达形式一样,你做得越多,你就会变得越好,但前提是你要挑战自己。

您甚至可以使用这个过程来探索文档缺乏或不透明的新库。您可以从探索库中包含哪些测试开始,如果没有取得足够的进展,您可以键入库中的一些关键方法和类。你不仅会对正在发生的事情和应该如何使用这个库有一个更好的了解,而且你还可能获得一些有趣的想法,你可以进一步探索。

最后,和所有的想法一样,尝试一下,看看它们在哪里有效,在哪里让你失望。保留适合你大脑的东西,放弃让你犯错的东西,只要你只是在你变得清晰之后放弃一个想法,而不是在你仍然觉得它很难的时候。

练习

  • 使用“临别镜头”一节中的过程,如果你在整本书中没有这样做的话,编写你自己的代码来更深入地理解访问者模式。
  • 在标准库中找到一个有趣的 Python 包,并通过它进行编码,看看这些开发人员做事情的方式与你有什么不同。
  • 针对人员 1 和人员 2 存在或不存在的这些状态编写测试。确保你在这些测试中涵盖了所有可能的选项。
  • 在每个实例中扩展代码,以适应朋友来参加社交聚会。
  • 想想这本书里的其他设计模式;有没有其他方法可以让代码变得更好?如果是这样,使用这种设计模式实现解决方案。
  • 为了明确访问者模式更容易遵守开闭原则的原因,您将向我们的系统添加另一个设备—流媒体娱乐中心。这个设备带有 Python 绑定,但是由于某种原因,我们无法访问设备的内部工作。将此对象添加到我们的网络中,并定义一个虚拟接口来模拟所提供的接口。然后,为家庭中的每个状态添加自定义设置。
  • 将状态模式添加到本章的最后一段代码中,以允许您处理我们讨论的四种场景。你需要如何以不同的方式思考我们在本章中写的代码?然后,决定在这种情况下,实现状态模式会使解决方案更好还是更差。特别考虑一下维护和我们到目前为止讨论过的原则。