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

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

原文:Practical Python Design Patterns

协议:CC BY-NC-SA 4.0

一、在开始之前

设计模式帮助你从别人的成功中学习,而不是从自己的失败中学习。—马克·约翰逊

世界正在改变。

当你读这篇文章的时候,全世界的人都在学习编程,但大多很糟糕。大量的“程序员”涌向市场,你无法阻止他们。和所有的事情一样,你很快就会看到,随着程序员供给的增加,雇佣程序员的成本会降低。简单地写几行代码或改变一个软件的某些东西(就像电子表格或文字处理大师的角色)将成为一项基本技能。

如果你打算以程序员为生,这个问题会因互联网而变得更糟。你不仅要担心你所在地区的程序员,还要担心来自世界各地的程序员。

你知道谁不担心像蘑菇一样涌现的数百个代码训练营产生的数百万程序员吗?

大师们

虽然每个人都可以写作,但世界上仍然有不同的写作方式。虽然每个人都可以使用 Excel,但是仍然有财务建模者。当每个人都能编码时,世界上仍然会有彼得·诺维格斯。

编程是一种工具。有一段时间,仅仅知道如何使用这个工具就让你变得有价值。那个时代已经结束了。这是一个新的现实,也是许多行业多年来不得不面对的现实。当一个工具是新的,没有人想使用它。然后,一些人看到了价值,这使他们比那些不会使用工具的人好两倍。然后,就流行起来了。不知不觉中,每个人都可以使用互联网。突然间,能够创建一个网站变得没有价值了。所有提供咨询、收取高额服务费的企业都被边缘化了。然而,无论市场发生什么,花时间掌握工具的人都能茁壮成长。他们能够做到这一点,是因为他们能够在各个层面上超越普通人——工作质量、开发速度和最终结果的美感。

现在,我们看到了大众对简单事物的理解。下一步将是把容易的事情自动化。在生活的每个领域,简单的任务之所以简单,是因为它们不需要创造力和深刻的理解,由于这些特点,它们正是将首先交给计算机的任务。

你拿起这本书的原因是因为你想成为一名更好的程序员。你想要超越成千上万的对这个和那个的介绍。你已经准备好迈出下一步,去掌握这门手艺。

在编程的新世界中,你希望能够解决大而复杂的问题。要做到这一点,你需要能够在更高的层面上思考。就像国际象棋大师一样,他们能以比大师更大的块来消化游戏,你也需要发展以更大的块来解决问题的技能。当你能够看待一个全新的问题,并迅速将其分解成高层次的部分,即你以前已经解决过的部分,你就成为了一个指数程序员。

当我开始编程时,我还看不懂手册。封面上有一张太空入侵者的图片,它承诺教你如何自己编写一个游戏。这有点吵闹,因为游戏最终是一个for循环和一个if声明,而不是太空入侵者。但是我恋爱了。激情驱使我尽我所能学习编程的一切,这并不多。在掌握了基础知识之后,我停滞不前了,甚至在获得计算机科学学士学位时也是如此。我觉得我所学的只是基础知识的简单重复。我确实学了,但感觉很慢,令人沮丧,好像每个人都在等待什么。

在“真实世界”中,它看起来并没有太大的不同。我开始意识到,如果我想成为一名指数程序员,我必须做一些不同的事情。起初,我想过拿个硕士或博士学位,但最后我决定自己深挖。

我重读了旧的计算理论书籍,它们获得了新的意义和新的生命。我开始定期参加编码挑战,并学习更多编写代码的惯用方法。然后,我开始探索与我自己不同的编程语言和范式。不知不觉中,我对编程的思维已经完全转变了。过去很难的问题变得微不足道,一些看起来不可能的事情变得仅仅是挑战。

我还在学习,还在挖掘。

你也能做到。从你现在正在读的这本书开始。在这本书中,你会发现许多工具和心智模型可以帮助你更好地思考和编码。在你掌握了这套模式之后,积极寻找并掌握不同的工具和技术。尝试不同的框架和语言,找出它们的优点。一旦你能理解是什么让一个人喜欢上一种不同于你经常使用的语言,你可能会发现其中的一些想法可以融入你的思维方式。研究你在任何地方发现的食谱和模式,并把它们转化成你可以反复使用的抽象解决方案。有时候,你可以简单地分析一个你已经使用的解决方案:你能找到一个更优雅的方法来实现这个想法吗,或者这个想法在某些方面有缺陷吗?不断地问自己如何改进你的工具,你的技能,或者你自己。

如果你准备深入挖掘并掌握这门艺术,你会在这本书里找到指导你走向精通之路的想法。我们将在现实环境中探索一套基本的设计模式。当你这样做的时候,你将开始意识到这些设计模式是如何被看作是你将来遇到特定类型的问题时可以使用的构建模块。

我的希望是,你将使用本书中的设计模式作为蓝图;这将帮助你开始收集你自己的问题解决方案。在你读完这本书之后,你应该已经踏上了通往下一个级别的程序员之旅。您还应该了解如何将这组抽象工具翻译成您可能遇到的任何编程语言,以及如何使用其他语言中的思想和解决方案并将其纳入您的思维工具箱。

成为更好的程序员

要成为一名更好的程序员,你需要决定痴迷于精通。每一天都是一个新的机会,让你变得比前一天更好。每一行代码都是改进的机会。你是怎么做到的?

  • 当您必须在一段现有代码上工作时,让代码比您发现它时更好。
  • 尝试每天完成一个快速解决问题的挑战。
  • 寻找机会与比你更优秀的程序员一起工作(开源项目非常适合这一点)。
  • 专注于刻意练习。
  • 只要你能找到借口,就练习系统思考。
  • 收集和分析心智模型。
  • 掌握你的工具,明白它们有什么用途,不应该用来做什么。

刻意练习

安德斯·埃里克森博士研究了那些达到了我们称之为精通的表现水平的人。他发现这些人有几个共同点。第一,正如马尔科姆·格拉德威尔的书《局外人》中所流行的,这些人似乎都花了大量的时间在所讨论的技能上。实际数字各不相同,但投入的时间接近 10,000 小时。这是一段很长的时间,但是仅仅花 10,000 个小时或者 10 年来练习是不够的。有一种非常特殊的练习被证明是大师级演奏所必需的。主宰世界的技巧的关键是有意识的练习。

我们一会儿会看看这与编程有什么关系,但是让我们先看看什么是刻意练习。

刻意的练习是缓慢的、停顿的、逆流的。如果你在练习小提琴,刻意的练习需要非常缓慢地演奏这首曲子,确保你完美地弹奏每一个音符。如果你有意练习网球,这可能意味着在教练的指导下一遍又一遍地做同样的击球,做一些小的调整,直到你能够一次又一次地在那个位置完美地击球。

知识工作的问题在于,标准的刻意练习协议似乎是外来的。与编程中涉及的过程相比,一项体育任务或演奏一种乐器是相当简单的。解构设计和开发软件解决方案的过程要困难得多。很难弄清楚如何实践一种思维方式。其中一个原因是,一个已解决的问题倾向于保持解决状态。我的意思是,作为一个开发者,你很少会被要求去写一个已经写好的软件。找到反复练习的“镜头”很难。

在编程中,你想学习解决问题的不同方法。你希望找到不同的挑战,迫使你用不同的约束来解决同一类型的问题。继续努力,直到你彻底理解问题和可能的解决方案。

本质上,刻意练习有以下几个组成部分:

  • 每次练习都有一个重点。
  • 尝试和反馈之间的距离(时间)越短越好。
  • 做一些你还不能做的事情。
  • 追随前人的足迹。

单焦点

在一次特定的训练中,你只想专注于一件事的原因是,你想把你所有的注意力都放在你希望提高的元素上。你想尽一切努力把它做得完美,哪怕只有一次。然后,你想重复这个过程来得到另一个完美的回答,然后是另一个。每次练习都是对技能的一次强化。你想要强化导致完美结果的模式,而不是那些不太理想的结果。

在这本书的上下文中,我希望你一次只针对一个设计模式。真正寻求理解的不仅仅是模式是如何工作的,还有它最初为什么被使用,以及它如何应用于手头的问题。此外,考虑使用这种设计模式可能能够解决的其他问题。接下来,尝试使用您正在处理的模式来解决其中的一些问题。

你想为问题和解决问题的模式创建一个心理盒子。理想的情况是,你会发展出一种感觉,知道什么时候问题可以放进你已经掌握的盒子里,然后能够快速而容易地解决问题。

快速反馈

经常被忽视的有意识的练习之一是快速反馈循环。反馈越快,联系越强,你就越容易从中学习。这就是为什么像营销和写作这样的东西如此难以掌握。从把信放在纸上到从市场得到反馈之间的时间太长了,以至于无法真正看到实验的效果。对于编程来说,情况并非如此;您可以编写一段代码,然后运行它以获得即时反馈。这使你能够正确处理并最终达成一个可接受的解决方案。如果您更进一步,为您的代码编写可靠的测试,您将获得更多的反馈,并且您能够比每次做出更改都必须手动测试过程更快地找到解决方案。

帮助你更快地从这个过程中学习的另一个技巧是预测你想写的代码块的结果。记下你为什么期望这个特定的结果。现在编写代码,并对照预期结果检查结果。如果不匹配,尝试解释为什么会出现这种情况,以及如何用另一段代码来验证您的解释。然后,测试一下。坚持做下去,直到你掌握为止。

你会发现自己得到了不同层次的反馈,每一个层次都有自己的优点。

第一个层次就是解决方案是否有效。接下来,您可能会开始考虑诸如“该解决方案实施起来有多容易?”或者“这个解决方案适合这个问题吗?”稍后,您可能会寻求外部反馈,其形式可以是简单的代码审查、项目工作或与志同道合的人讨论解决方案。

伸展你自己

你回避的事情是什么?这些是编程中让你感到不舒服的地方。它可能是从磁盘上的文件中读取或者连接到远程 API。无论是图形库还是机器学习系统都没有区别,我们都有不舒服的地方。这些通常是你最需要努力的事情。这里有一些领域会拓展你,迫使你面对自己的弱点和盲点。克服这种不适的唯一方法是深入其中,以多种方式多次使用这个工具,这样你就能开始对它有感觉了。您必须对它非常熟悉,这样您就不必再在堆栈溢出时查找文件打开协议;事实上,你写了一个更好的。你用 GUI 跳绳,一只手绑在背后从数据库中吸取数据。

达到这种精通水平没有捷径;唯一的路是穿过这座山。这就是为什么很少有人成为真正的大师。到达那里意味着花很多时间在不容易的事情上,这不会让你觉得你是不可战胜的。你花了太多时间在这些自我毁灭的地带,以至于很少有任何一门手艺的大师还留有许多傲慢。

那么,你应该先做什么呢?你读前面两段时想到的那件事。

按照本书中的设计模式工作是发现潜在增长领域的另一个好方法。只需从单例模式开始,然后一步步来。

站在巨人的肩膀上

有些人在编程领域做出了惊人的成就。这些人经常在开发者大会上发表演讲,有时也会出现在网上。看看这些人在说什么。试着理解他们是如何处理一个新问题的。在他们演示解决方案时,和他们一起打字。进入他们的大脑,找出是什么让他们跳动。试着解决一个问题,就像你想象他们中的一个会做的那样;你用这种方式想出的解决方案和你自己想出的有什么不同?

真正伟大的开发人员对编程充满热情,不需要太多的刺激就能让他们谈论技术的细节。找出这类开发人员经常出没的用户群,并展开大量对话。保持开放,不断学习。

选择那些强迫你使用你尚未掌握的设计模式的个人项目。好好享受吧。最重要的是,学会热爱这个过程,不要沉迷于一些可感知的结果,而不是花时间成为一名更好的程序员。

你是怎么做到的?

就像列奥纳多·达·芬奇决定以绘画为职业时那样开始。

收到。

没错。首先确定一些有趣的问题,一个已经解决的问题,然后公然复制解决方案。不要复制/粘贴。自己打出来复制解答。让您的副本发挥作用。一旦你做了,全部删除。现在试着从记忆中解决问题,当你的记忆让你失望时,只参考最初的解决方案。这样做,直到您能够在不看原始解决方案的情况下,完美地重现解决方案。如果你在寻找可以复制和借鉴的问题解决方案,Github 就是一座金矿。

一旦你对你找到的解决方案感到满意,试着改进最初的解决方案。你需要学会思考你在那里找到的解决方案——是什么让它们变得好?如何让它们更优雅、更地道、更简单?总是寻找机会以这些方式中的一种使代码变得更好。

接下来,你想把你的新解决方案带到野外。找到使用解决方案的地方。在现实世界中练习迫使你处理不同的约束,那种你自己永远不会想到的约束。它会迫使你以从未想过的方式改变你漂亮、干净的解决方案。有时候,你的代码会崩溃,你会学会欣赏最初解决问题的方式。其他时候,你可能会发现你的解决方案比原来的更好。问问你自己,为什么一个解决方案优于另一个,这个问题和解决方案教会了你什么。

尝试使用不同范式的语言来解决类似的问题,从每种语言中吸取经验,然后自己形成解决方案。如果你像实际解决问题一样关注过程,没有项目会让你置身事外。

从事开源项目的美妙之处在于,通常会有一些人会帮助你,还有一些人会告诉你你的代码到底出了什么问题。评估他们的反馈,学习你所能学到的,抛弃其他的。

航向修正的能力

当探索一个问题时,你有两个选择:继续这样尝试,或者放弃一切,用我学到的重新开始。丹尼尔·卡纳曼在他的《思考,快与慢》一书中解释了沉没成本谬误。这是你继续投资于一个糟糕的投资的地方,因为你已经投资了这么多。这对于一个开发者来说是一个致命的陷阱。让一个两天的项目花上几个月的最快方法是尝试通过一个糟糕的解决方案。如果我们放弃一天、一周或一个月的工作,从零开始,这通常会是一个巨大的损失。

事实是,我们从来没有从零开始,有时您删除的最后 10,000 行代码正是您需要编写的 10,000 行代码,您需要成为一名程序员,用 100 行代码用一个惊人的优雅解决方案解决问题。

你需要培养意志,说够了就够了,然后重新开始,用你所学到的建立一个新的解决方案。

如果一个解决方案是错误的,那么在这个解决方案上花费的时间再多也没有任何意义。你越早意识到这一点越好。

这种自我意识让你有能力知道什么时候再尝试一件事,什么时候朝不同的方向前进。

系统思维

一切都是一个系统。通过理解什么元素组成了系统,它们是如何连接的,以及它们如何相互作用,你就能理解这个系统。通过学习解构和理解系统,你不可避免地教会了自己如何设计和构建系统。每一个软件都表达了这三个核心组件:组成解决方案的元素,以及它们之间的一组连接和交互。

在一个非常基础的层面上,你可以从问你自己开始:“我想要建立的系统的元素是什么?”把这些写下来。然后,写下它们是如何相互联系的。最后,列出这些元素之间的所有相互作用以及起作用的联系。这样做,你就完成了系统的设计。

所有设计模式都处理这三个基本问题:1)元素是什么,它们是如何创建的?2)元素之间有什么联系,或者说结构是什么样子的?3)元素如何交互,或者它们的行为看起来像什么?

还有其他方法来分类设计模式,但是现在使用经典的三个分组来帮助你发展你的系统思维技能。

心理模型

心理模型是外部世界的内在表现。你对世界的模型越精确,你的思维就越有效。地图不是领域,所以拥有更准确的心智模型会让你对世界的看法更准确,你的心智模型越多才多艺,你就能解决越多种多样的问题。在本书中,你将学到一套思维工具,帮助你解决在你的程序员生涯中经常遇到的特定编程问题。

看待心智模型的另一种方式是将它们视为一组概念,组成一个单一的思维单元。设计模式的研究将帮助你开发新的思维模式。一个问题的结构将暗示你需要实现什么样的解决方案来解决这个问题。这就是为什么你要完全清楚你要解决的问题是什么是很重要的。问题定义或描述越好——我的意思是越完整——你对可能的解决方案的暗示就越多。所以,就这个问题问自己一些愚蠢的问题,直到你有了一个清晰简单的问题陈述。

设计模式帮助你从 A(问题陈述)到 C(解决方案),而不必经过 B 和许多其他错误的开始。

适合这项工作的工具

要打破砖墙,你需要一把锤子,但不是随便一把——你需要一把又大又重的长柄锤子。当然,你可以用你用来在音乐盒里钉钉子的那把锤子砸开这面墙,但是用合适的工具敲一个下午的时间,却要花上你好几辈子的时间。

使用错误的工具,任何工作都会变得一团糟,花费的时间也会比预期的长。为这项工作选择合适的工具是一个经验问题。如果你不知道除了你习惯的小锤子还有锤子,你很难想象有人能在几个小时内推倒整面墙。你甚至可以称这样的人为 10 倍破壁人。我想说的是,有了合适的工具,你将能够比那些试图用现有资源凑合的人多做许多倍的工作。花时间和精力扩展你的工具箱,掌握不同的工具是值得的,这样当你遇到一个新问题时,你就知道选择哪一个。

要成为一名编程大师,你需要不断地向你的武器库中添加新的工具,不仅仅是熟悉它们,而是掌握它们。我们已经看了掌握您决定的工具的过程,但是在 Python 的环境中,让我提出一些具体的建议。

Python 生态系统的一些美妙之处是可以直接获得的包。有很多包,但通常你可能遇到的每种类型的问题都有一个或两个明确的领导者。这些包是有价值的工具,您应该每周花几个小时来研究它们。一旦你掌握了本书中的模式,抓住 Numpy 或 Scipy 并掌握它们。然后,向你想象的任何方向前进。下载您感兴趣的包,学习基础知识,然后开始使用已经提到的框架进行试验。它们闪耀在哪里,又缺少了什么?他们特别擅长解决什么样的问题?你将来如何使用它们?你能做什么样的辅助项目来让你在现实世界的场景中尝试这个包?

作为概念的设计模式

四人帮关于设计模式的书似乎是一切开始的地方。他们提出了一个描述设计模式的框架(特别是针对 C++ ),但是描述通常集中在解决方案上,结果许多模式被翻译成多种语言。该书中列出的 23 种设计模式的目标是为面向对象编程中遇到的常见问题编写最佳实践解决方案。因此,解决方案关注于类及其方法和属性。

这些设计模式每一个都代表一个完整的解决方案,它们将变化的东西与不变的东西分开。

有许多人对最初的设计模式持批评态度。其中一位批评家 Peter Norvig 展示了如何用 Lisp 中的语言结构来代替其中的 16 种模式。这些替换在 Python 中也是可能的。

在这本书里,我们将会看到几个原始的设计模式,以及它们如何适应现实世界的项目。我们还将在 Python 语言的上下文中考虑关于它们的争论,有时为了使用该语言的标准解决方案而放弃该模式,有时改变 GoF 解决方案以利用 Python 的强大功能和表达能力,而其他时候只是以 Python 的方式实现原始解决方案。

什么构成了设计模式?

设计模式可以是很多东西,但它们都包含以下元素(鸣谢:彼得·诺维格, http://norvig.com/design-patterns/ppframe.htm ):

  • 模式名称
  • 意图/目的
  • 别名
  • 动机/背景
  • 问题
  • 解决办法
  • 结构
  • 参与者
  • 协作
  • 后果/限制
  • 履行
  • 示例代码
  • 已知用途
  • 相关模式

在附录 A 中,您可以找到我们在本书中讨论的所有设计模式都是根据这些元素构建的。出于可读性和学习过程的考虑,关于设计模式本身的章节不会都遵循这种结构。

分类

设计模式被分为不同的组,以帮助我们作为程序员彼此谈论解决方案的类别,并在讨论这些解决方案时给我们一个共同的语言。这使我们能够清晰地交流,并在讨论中表达我们的主题。

正如本章前面提到的,我们将根据创造、结构和行为模式的原始分组来对设计模式进行分类。这样做不仅是为了坚持一般的做事方式,也是为了帮助你,读者,在他们所处的系统环境中查看模式。

创造型的

第一类处理系统中的元素——特别是它们是如何被创建的。当我们处理面向对象编程时,对象的创建是通过类实例化来实现的。正如您将很快看到的,在解决特定问题时,有不同的特性是可取的,而创建对象的方式对这些特性有重大影响。

结构的

结构模式处理类和对象是如何组成的。可以使用继承来组合对象,以获得新的功能。

行为的

这些设计模式专注于对象之间的交互。

我们将使用的工具

世界正在向 Python 3 转变。这种转变是缓慢而深思熟虑的,就像冰川一样,无法停止。为了这本书,我们将发布 Python 2,拥抱未来。也就是说,您应该能够毫不费力地使用 Python 2 编写大部分代码(这并不是因为书中的代码,而是 Python 核心开发人员出色工作的结果)。

对于 Python,特别是 CPython(默认的),您可以使用 Pip,Python 包安装程序。Pip 与 PyPI(Python 包索引)集成,允许您从包索引下载和安装包,而无需手动下载包、解压缩包、运行 python setup.py install 等。Pip 让为您的环境安装库成为一种乐趣。Pip 还为您处理所有的依赖项,因此没有必要在您尚未安装所需的软件包后到处跑。

当你开始着手第三个项目时,你会需要很多包。并非所有的项目都需要这些包。您将希望保持每个项目的包都被很好地包含。进入 VirtualEnv,这是一个虚拟的 Python 解释器,它将为该解释器安装的包与系统上的其他包隔离开来。您可以将每个项目放在自己的极简空间中,只安装它工作所需的包,而不会干扰您可能正在进行的其他项目。

如何阅读这本书

阅读一本书的方法有很多,尤其是编程书。大多数人在开始读这本书的时候都希望能从头到尾读一遍,结果只是从一个代码示例跳到另一个代码示例。我们都做过。记住这一点,你可以通过多种方式阅读这本书,并从花在这本书上的时间中获得最佳价值。

第一批读者只想快速、可靠地参考 GoF 设计模式的 pythonic 版本。如果这是你,请跳到附录 A,查看每个设计模式的正式定义。当你有时间的时候,你可以回到相关的章节,继续探索你感兴趣的设计模式。

第二类人希望能够找到具体问题的具体解决方案。对于那些读者来说,每一章的设计模式都是从描述模式所解决的问题开始的。这将有助于您决定该模式是否有助于解决您所面临的问题。

最后一组想用这本书来掌握自己的手艺。对于这些读者,我建议你从头开始,通过你的方式编写这本书。把每个例子都打出来。修补每一个解决方案。做所有的练习。看看修改代码后会发生什么。什么东西坏了,为什么?让解决方案更好。一次处理一个模式,并掌握它。然后,找到其他你可以应用新知识的真实环境。

设置 Python 环境

让我们从让 Python 3 环境在您的机器上运行开始。在这一节中,我们将研究在 Linux、Mac 和 Windows 上安装 Python 3。

在 Linux 上

我们使用的命令是针对 Ubuntu 的 apt 包管理器。对于其他不支持 apt 的发行版,可以看看使用另一个包管理器(比如 yum)安装 Python 3 和pip的过程,或者从源代码安装相关的包。

整个安装过程都将使用终端,所以您现在可以开始打开它了。

让我们首先检查您的系统上是否已经安装了 Python 的一个版本。

只需注意:在本书的持续时间内,我将用前导$来表示终端命令;在终端中输入注释时,不要键入该字符或后续空格。

$ python --version

如果您已经安装了 Python 3(Ubuntu 16.04 和更高版本就是这种情况),您可以直接跳到安装 pip 一节。

如果您的系统上安装了 Python 2 或者没有安装 Python,您可以使用以下命令安装 Python 3:

$ sudo apt-get install python3-dev

这将安装 Python 3。您可以像以前一样检查 Python 安装的版本,以验证是否安装了正确的版本:

$ python3 --version

Python 3 现在应该可以在您的系统上使用了。

接下来,我们安装 build essentials 和 Python pip:

$ sudo apt-get install python-pip build-essential

现在,检查pip是否正常工作:

$ pip --version

您应该看到您的系统上安装了一个版本的pip,现在您已经准备好安装virtualenv包了;请跳过 Mac 和 Windows 安装说明。

在 Mac 上

macOS 默认安装了 Python 2 版本,但是我们不需要它。

为了安装 Python 3,我们将使用 Homebrew,这是一个用于 macOS 的命令行包管理器。要做到这一点,我们需要 Xcode,你可以从 Mac AppStore 免费获得。

安装 Xcode 后,打开“终端”应用并安装 Xcode 的命令行工具:

$ xcode-select --install

只需按照弹出窗口中的提示安装 Xcode 的命令行工具。完成后,您就可以安装自制软件了:

$ ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"

如果您还没有安装 XQuartz,您的 macOS 可能会遇到一些错误。如果出现这种情况,可以在这里下载 XQuartz.dmg:https://www.xquartz.org/。然后,检查您是否成功安装了 Homebrew 并且它正在工作:

$ brew doctor

要从终端中的任何文件夹运行brew命令,需要将 Homebrew 路径添加到您的PATH环境变量中。打开或创建~/.bash_profile,并在文件末尾添加以下行:

export PATH=/usr/local/bin:$PATH

关闭并重新打开终端。重新打开后,新的PATH变量将包含在环境中,现在您可以从任何地方调用brew。使用brew查找 Python 可用的包:

brew search python

您现在将看到所有与 Python 相关的包,python3是其中之一。现在,使用下面的brew命令安装 Python 3:

$ brew install python3

最后,您可以检查 Python 3 是否已安装并正在运行:

python3 --version

当你用 Homebrew 安装 Python 的时候,你也安装了相应的包管理器(pip)、Setuptools 和 pyvenv(一个virtualenv的替代品,但是对于这本书你只需要pip)。

检查 pip 是否正常工作:

$ pip --version

如果看到版本信息,说明pip已经成功安装在您的系统上,您可以跳过 Windows 安装部分,跳到使用pip安装 VirtualEnv 的部分。

在窗口上

首先下载 Python 3 Windows 安装程序。你可以在这里下载安装程序: https://www.python.org/

下载完成后,运行安装程序并选择自定义选项。确保选择安装 pip。此外,选择将 Python 添加到环境变量的选项。

随着安装,你也变得空闲,这是一个交互式 Python shell。这使您可以在实时解释器中运行 Python 命令,这对于测试想法或试验新的包来说是非常好的。

安装完成后,打开命令行界面:

windowsbutton-r
cmd

这将打开一个类似于 Mac 和 Linux 上可用的终端。

现在,检查 Python 是否在工作:

$ python --version

如果您看到版本信息,Python 已安装并链接到该路径,这是应该的。

在我们继续之前,检查一下pip是否在工作:

$ pip --version

VirtualEnv(虚拟环境)

在开始安装 VirtualEnv 之前,让我们确保已经安装了最新版本的pip:

$ pip install --upgrade pip

Windows 用户将看到如下命令提示符:

c:\some\directory>

在 Mac 上你有

#

在 Linux 上是这样的

$

在本书中,我将使用$来表示终端命令。

使用pip安装virtualenv包:

$ pip install virtualenv

现在您已经有了virtualenv包,让我们用它来创建一个使用这本书的环境。

在您的终端中,转到您将工作的目录。接下来,创建一个虚拟环境,您将在其中安装本书中的项目所需的包。我们的虚拟环境将被称为ppdpenv;``env只是为了让我们知道它是一个虚拟环境。每当我们激活这个环境并使用pip命令安装一个新的包时,这个新的包将只安装在这个环境中,并且只在这个环境激活时对程序可用。

$ virtualenv -p python3 ppdpenv

在 Windows 上,您需要一个稍微不同的命令:

$ virtualenv -p python ppdpenv

如果 Python 3 不是您的PATH的一部分,您可以使用您想要在您的virtualenv中使用的 Python 可执行文件的完整路径。

安装过程结束后,您将在运行virtualenv命令时所在的目录中拥有一个名为ppdpenv的新目录。

要激活虚拟环境,请在 Mac 和 Linux 上运行以下命令:

$ source ./ppdpenv/bin/activate

在 Windows 上:

$ source ppdpenv\Scripts\activate

最后,检查 Python 的版本,确保安装了正确的版本。

就这样——您已经准备好用 Python 编程了!

要退出虚拟环境,您可以运行以下命令:

$ deactivate

编辑

任何可以保存纯文本文件的文本编辑器都可以用来编写 Python 程序。除了记事本,你可以什么都不用写下一个优步,但这会很痛苦。每当你写一个任意长度的程序时,你需要一个编辑器来突出你的代码,用不同的颜色标记不同的关键字和表达式,这样你就可以很容易的区分它们。

以下是我最喜欢的文本编辑器的简短列表,每个都有一些注释。

原子

Github 开发了一个编辑器,他们称之为 Atom,它是一个简单、漂亮、功能强大的编辑器。它提供了简单的 git 集成和良好的包系统(包括一个实时的 markdown 编译器)。事实上,它是基于电子的,这意味着无论你想在什么平台上工作,你都可以随身携带你的编辑器。如果它能运行 Chromium,它就会运行 Atom。

拿到这里: https://atom.io/

看版台

这款基于 ClojureScript(一种编译成 JavaScript 的 Lisp 方言),有 Vim 和 Emacs 两种模式,所以你可以把自己从鼠标中解放出来。像大多数现代代码编辑器一样,LightTable 也提供了直接的 git 集成。它是开源的,易于定制。

拿到这里: http://lighttable.com/

PyCharm

Jetbrains 为 Python 编辑器制定了行业标准。它提供了很棒的代码补全、Python linter、未使用的导入警告等等,但对我来说,它的致命特性是代码拼写检查,它同时考虑了 Camel 和 Snake 两种情况。

PyCharm 的企业版并不便宜,但他们提供免费的学生和社区许可证。

拿到这里: https://www.jetbrains.com/pycharm/download

精力

大多数基于 UNIX 的操作系统都已经安装了 Vim。Vim 有时被称为你无法摆脱的编辑器(提示:“:q!”让你摆脱)。Vim 最强大的地方在于它的快捷键可以做任何事情,从选择代码块到跳转到代码中的特定行——任何事情都不需要离开键盘。多年来,Vim 积累了大量的扩展、颜色主题和一个成熟的 IDE 梦寐以求的所有特性。它有自己的包管理器,让你完全控制代码编辑的每一个方面。它是免费的,是我见过的所有编辑器中最快的。尽管 Vim 有很多优点,但它很难学——非常难。每当我想写一个快速脚本或者做一些小的改变,并且不想等待其他编辑器/IDE 启动时,我都会使用 Vim。

通过这里设置 Python 的 Vim:https://realpython.com/blog/python/vim-and-python-a-match-made-in-heaven/

编辑器

令人惊叹的操作系统;如果它有一个好的文本编辑器就好了。Emacs 是建立在 Emacs Lisp 之上的,并且是可定制的。你可以用它做任何事情,从发送电子邮件到启动咖啡机,只需一个简单的快捷键组合。像 Vim 一样,它是无鼠标的,但是它有一个更陡峭的学习曲线。那些了解内情的人对此深信不疑,而且理由充分。您拥有现代 IDE 提供的所有代码完成和分屏选项,并且能够在需要时调整系统。您不必等待某个供应商创建一个包来处理一些新语言特性;你可以自己轻松搞定。Easily 用得很不严谨。有了 Emacs,你可以让你的编辑按照你认为应该做的方式去做事情,而编程的很大一部分是因为对目前做事情的方式不满意,而采取不同的方式。

通过阅读以下内容为 Python 开发设置 Emacs:https://realpython.com/blog/python/emacs-the-best-python-editor/

崇高的文本

这与其说是一个真正的建议,不如说是一个荣誉的提及。Sublime 曾经是最好的免费选项(每隔几个救球就有 nag 屏幕)。它有一个漂亮的默认配色方案,启动相当快,并且是可扩展的。它是 Python 就绪的,但它的时代可能已经过去了。

如果你有兴趣看的话,可以在这里得到: https://www.sublimetext.com/3

摘要

最后,您选择使用哪个编辑器并不重要。必须是你觉得舒服的事情。无论你选择什么,花时间掌握这个工具。如果你想富有成效,你必须掌握你每天必须使用的任何工具或流程,不管它看起来有多平凡。我的建议是你选择其中的一个来彻底学习,如果它不是 Vim 或 Emacs,至少学习足够的 Vim 来编辑其中的文本。这会让你花在服务器上编辑软件的时间变得更加愉快。

现在,您已经设置好了环境,并将其与系统的其余部分隔离开来。您也已经安装了您选择的编辑器并准备好了,所以让我们不要浪费任何时间。让我们开始看看一些实用的 Python 设计模式。

二、单例模式

只能有一个!–——康纳·麦克劳德

问题

您学习的第一个调试技术之一是简单地将某些变量的值打印到控制台。这有助于您了解程序内部的情况。当你写的程序变得越来越复杂时,你就不能有效地在脑子里运行整个程序了。这是当你需要在特定时间打印出某些变量的值时,特别是当程序没有像你期望的那样运行时。在程序执行的不同阶段查看程序的输出有助于快速定位和修复错误。

你的代码很快就被print语句弄得乱七八糟,这没什么,直到有一天你部署了你的代码。在服务器上运行您的代码,作为您自己机器上的调度作业,或者作为客户端计算机上的独立软件,这意味着当出现问题时,您再也不能依赖控制台提供反馈。您无法知道哪里出了问题,也无法知道如何重现问题。这将调试从科学领域直接带入了赌博。

最简单的解决方案是用一个命令代替print语句,将输出写到一个文件而不是控制台,如下所示:

with open("filename.log", "a") as log_file:
  log_file.write("Log message goes here.\n")

现在你可以看到当程序出错时发生了什么。你的日志文件就像飞机上的黑匣子:它记录着你的程序的执行情况。当你的程序崩溃时,你可以打开黑盒,看看导致崩溃的原因,以及你应该从哪里开始寻找错误。一行比两行好,所以创建一个函数来处理文件的打开和写入:

def log_message(msg):
  with open("filename.log", "a") as log_file:
    log_file.write("{0}\n".format(msg))

如果您想记录程序的某些状态以供以后查看,您可以使用这个替换语句来代替print语句:

log_message("save this for later")

log_message函数打开一个文件,并将传递给它的消息附加到文件中。这就是干原理在起作用。干?你问。它代表不要重复自己。在最基本的层面上,每当您想要复制和粘贴代码时,您应该会听到头脑中发出一点警报。只要有可能,您应该以这样一种方式重新打包代码,即您可以重用它,而不需要将代码行复制并粘贴到其他地方。

为什么你要重新思考有效的代码呢?为什么不临摹一下,只改一两件呢?如果你的代码在一个地方,你必须改变一些东西,你只需要在那个地方改变它。例如,如果您将日志记录器的代码分散在整个程序中,并且您决定更改将日志写入的文件的名称,那么您必须在许多地方更改该名称。你会发现这是一个灾难的配方;如果您丢失了一行输出代码,您的一些日志消息将会丢失。在编程中,导致错误的首要原因是人。如果您需要处理一个跨越数千甚至数百万行代码的项目,并且您需要更改日志文件名之类的内容,那么在更新过程中,您很容易漏掉这里或那里的一行。只要有可能,就要消除代码中的人机交互。

优秀的程序员写出扎实的代码;伟大的程序员编写的代码能让一般的程序员写出可靠的代码。

使代码更加用户友好的一种方法是在项目中包含一个只包含日志功能的文件。这允许您将日志功能导入到项目中的任何文件中。对项目中日志记录方式的所有更改只需在这一个文件中进行。

让我们创建一个名为logger.py的新文件,并在文件中编写以下代码:

logger.py

def log_message(msg):
  with open("/var/log/filename.log", "a") as log_file:
    log_file.write("{0}\n".format(msg))

现在,我们可以使用新的 logger 函数将日志消息写到文件系统的main_script.py文件中:

main_script.py

import logger

for i in range(4):
  logger.log_message("log message {}".format(i))

文件的第一行- import logger告诉 Python 导入logger.py文件(注意在导入文件时不要添加.py)。导入文件允许您在main_script.py文件中使用logger.py文件中定义的功能。在前面的代码片段中,我们有一个循环,我们告诉 Python 在每次循环运行时写一条消息log message i,其中i是循环在范围中的索引。

打开filename.log文件,验证消息确实如预期的那样被写入。

随着日志消息数量的增加,您会发现想要区分它们。最有用的区别是用于描述导致所述消息的事件严重性的术语。

为了我们的记录器,我们将处理以下级别:

  • 批评的
  • 错误
  • 警告
  • 信息
  • 调试

幸运的是,我们只有一个文件需要更新(我们把这个日志程序移到了它自己的文件中,你不高兴吗?).

在日志文件中,我们希望每条消息前面都加上与该消息相关联的级别。这有助于我们轻松地扫描文件中特定类型的消息。您可以在某些命令行工具中使用这种约定,比如在*中使用grep。nix 环境中只显示特定级别的消息。

升级我们的日志功能后,看起来是这样的:

logger.py

def critical(msg):
  with open("/var/log/filename.log", "a") as log_file:
    log_file.write("[CRITICAL] {0}\n".format(msg))

def error(msg):
  with open("/var/log/filename.log", "a") as log_file:
    log_file.write("[ERROR] {0}\n".format(msg))

def warn(msg):

  with open("/var/log/filename.log", "a") as log_file:
    log_file.write("[WARN] {0}\n".format(msg))

def info(msg):
  with open("/var/log/filename.log", "a") as log_file:
    log_file.write("[INFO] {0}\n".format(msg))

def debug(msg):
  with open("/var/log/filename.log", "a") as log_file:
    log_file.write("[DEBUG] {0}\n".format(msg))

您不需要更改导入代码的方式;您只需使用想要保存的消息级别,如下所示:

test_error_log.py

import logger

try:
  a = 1 / 0

except:
  logger.error("something went wrong")

如果您查看_test_error_log.py__函数的输出,您会看到消息现在将级别作为前缀添加到了行中:

[ERROR] something went wrong

我们的日志项目现在有了一些非常有用的功能,但是我敢打赌,logger.py文件的编写方式会让您感到困扰。你脑子里的警铃刚刚响了,不是吗?

如果是的话,你是完全正确的!

我们不应该复制相同的指令来打开文件和将消息附加到文件,只是每个函数的前缀字符串不同。让我们重构我们的代码,这样我们就不会重复。

重复的部分是我们想要从所有复制代码的方法中提取的部分。下一步是对函数进行足够的一般化,以便每个函数都可以使用它,而不会失去它的任何原始功能。

在每个重复的函数中,前缀是函数与函数之间唯一不同的地方。因此,如果我们要编写一个将消息和级别作为参数的函数,我们可以在每个其他函数中使用该函数,并在每种情况下将其缩减为一行代码。

我们现在有了一个更短、更清晰的记录器,可以在其他项目中使用。

logger.py

def write_log(level, msg):
  with open("/var/log/filename.log", "a") as log_file:
    log_file.write("[{0}] {1}\n".format(level, msg))

def critical(msg):
  write_log("CRITICAL",msg)

def error(msg):
  write_log("ERROR", msg)

def warn(msg):
  write_log("WARN", msg)

def info(msg):
  write_log("INFO", msg)

def debug(msg):
  write_log("DEBUG", msg)

这个看起来更好。它简单明了,因为每个函数只做一件事。write_log函数只是将消息级别和消息文本写入文件。每个日志编写器简单地调用write_log函数,将消息的级别添加到调用中。

我相信你真的开始喜欢我们的小日志了,但是现在最困扰我的是我们用来保存所有日志文件的硬编码。您现在知道了日志记录使您成为一名更好的开发人员,并为您节省了大量时间。所以,这是一个非常好的记录器,你肯定想在你自己的项目中使用它。您希望日志记录器做的最后一件事是将来自不同项目的消息写入同一个文件。

为了避免这个问题,我们可以将文件名作为参数添加到我们在记录消息时调用的函数中。

继续实施这一改变。请注意,下面的代码片段仍然不完整,因为write_log函数仍然带有一个filename参数。

logger.py

def write_log(filename, level, msg):
  with open(filename, "a") as log_file:
    log_file.write("[{0}] {1}\n".format(level, msg))

  def critical(msg):
    write_log("CRITICAL",msg)

  def error(msg):
    write_log("ERROR", msg)

  def warn(msg):
    write_log("WARN", msg)

  def info(msg):
    write_log("INFO", msg)

  def debug(msg):
    write_log("DEBUG", msg)

filename参数不会随着调用的不同而改变,因此反复传递相同的值不是正确的做法。我们从其他函数中提取出write_log函数的真正原因是我们不必重复相同的代码。我们希望设置一次日志记录器,使用它应该记录的日志文件,然后使用它,而不用再注意选择要写入的文件。

输入对象

Python 中的类允许您定义数据和函数的逻辑分组。它们还允许您向记录器添加一些上下文数据(比如您想要写入哪个文件)。

为了充分利用课堂,你需要用一种稍微新的方式思考。

这种将彼此相关的函数和数据分组到一个类中的方式形成了一个蓝图,用于创建数据的特定实例(版本)以及它们相关的函数。一个类的实例称为对象。

为了帮助您理解这一点,请考虑一下我们刚刚开发的记录器。如果我们能够以这样一种方式来一般化这个记录器,即当我们使用它时,我们可以向它发送要使用的文件的名称,我们将有一个蓝图来创建任何记录器(一个类)。当我们进行这个调用时,我们有效地创建了一个新的记录器,它写入一个特定的文件。这个新的记录器称为类的实例。

将数据和改变数据的函数看作一个单一的实体是面向对象编程的基础。

我们现在将实现一个简单的Logger类作为例子。

logger_class.py

class Logger(object):
  """A file-based message logger with the following 
properties

  Attributes:

    file_name: a string representing the full path of the log file to which this logger will write its messages

  """

  def __init__(self, file_name):
    """Return a Logger object whose file_name is *file_name*"""

    self.file_name = file_name

  def _write_log(self, level, msg):
    """Writes a message to the file_name for a specific Logger instance"""

    with open(self.file_name, "a") as log_file:
      log_file.write("[{0}] {1}\n".format(level, msg))

  def critical(self, level, msg):
    self._write_log("CRITICAL",msg)

  def error(self, level, msg):
    self._write_log("ERROR", msg)

  def warn(self, level, msg):
    self._write_log("WARN", msg)

  def info(self, level, msg):
    self._write_log("INFO", msg)

  def debug(self, level, msg):
    self._write_log("DEBUG", msg)

这里发生了很多事情,所以在我向您展示如何在另一个项目中实现这个新的日志记录器之前,先看一下代码。

与我们之前的日志记录器的第一个大的不同是添加了class关键字,它的作用类似于def关键字,因为它告诉 Python 我们现在要定义一个新的类。然后,我们有了类名,当我们想要创建这个类的新对象时,我们将使用这个类名(正如您可能已经猜到的,称为实例)。

面向对象编程(OOP)的主要好处之一是,一旦定义了一个类,就可以重用该类来创建其他类。这个过程称为继承,因为子类从父类(用于创建子类的类)继承特征(数据和功能蓝图)。在 Python 中,每个类最终都继承自object类。在我们的记录器的例子中,我们没有使用其他类作为基础,所以这里我们只说Logger只有object作为它的父类。

接下来,我们有一些用"""字符包装的文本,表示包含的文本是一个文档字符串。某些集成开发环境(ide)和其他程序知道寻找这些字符串,并使用它们来帮助指导想要使用该类的程序员。

类并不是唯一可以利用文档字符串的结构,您还会看到函数也可以有自己的文档字符串,就像__init__函数一样。

每当实例化一个新的Logger对象时,就会调用__ init __。在这种情况下,它将文件名作为参数,并将其与正在创建的实例相关联。当我们想要引用这个文件名时,我们必须告诉对象中的方法在它自己的属性列表中寻找file_nameself关键字用于让代码知道我们引用了与发出调用的对象相关联的属性或方法,因此,它不应该在其他地方寻找这些元素。

关于属性和方法这两个术语,我只做一个简单的说明。属性是与类蓝图中定义的对象相关联的数据。对这些数据执行操作的函数称为方法。当我们将self关键字与一个方法一起使用时,就像这样:

self.some_method()

实际发生的是 Python 解释器调用方法some_method并将对象本身作为变量传递给函数,这就是为什么在对象的方法中有self作为参数,但在进行调用时没有显式地传递它。

另一件要注意的事情是,当引用对象中的属性时,如

self.some_attribute

Python 所做的是调用一个名为__getattr__的方法,并传入对象本身以及它所请求的属性名。您将很快看到一个这样的例子。

__init__方法中,您将看到我们设置了一个名为file_name的属性。这允许我们在需要使用_write_log方法时请求我们第一次创建类时设置的file_name属性的值。

除了我们现在还必须为我们讨论过的参数self做准备之外,其余的都与我们之前所做的很接近。

作为一个良好的实践,__init__方法的结果必须是一个完全初始化的对象。这意味着对象必须可以使用;在它可以执行其功能之前,不应该需要调整一些其他设置或执行一些方法。

如果你对_write_log中的前导下划线感到疑惑,这只是告诉其他程序员这个方法不应该被任何外部程序使用的一个约定;据说是私人的。

现在,我们可以看看这个新的Logger类将如何被使用。

new_script_with_logger.py

from logger_class import Logger

logger_object = Logger("/var/log/class_logger.log")

logger_object.info("This is an info message")

我们现在可以从 Python 文件中导入一个特定的类,方法是告诉解释器我们想要使用哪个文件,然后告诉它应该导入什么类。从不同的包中导入可能会比这更复杂,但是现在只要意识到可以导入想要使用的特定类就足够了。包是一组 Python 文件,它们位于计算机上的同一文件夹中,并提供相似或相关的功能。

要使用Logger蓝图(类)创建一个新的记录器,我们只需使用记录器的名称,并将其设置为类名,然后我们传入__init__函数所需的任何参数。实例化日志记录器后,我们可以简单地使用对象名和相关的日志函数将日志消息写入正确的文件。

运行new_script_with_logger.py将在/var/log/class_logger.log文件中产生以下消息:

[INFO] This is an info message

清理干净

现在,您可能不希望为项目中需要写入一些日志消息的每个部分编写唯一的日志文件。因此,您想要的是以某种方式获取您已经创建的相同记录器(如果有的话),或者创建一个新的记录器(如果还没有的话)。

您希望保留面向对象编程的优点,同时将对象创建过程的控制权从使用 logger 的程序员手中夺走。我们通过控制创建logger对象的过程来做到这一点。

考虑以下控制流程的方式:

singleton_object.py

class SingletonObject(object):
  class __SingletonObject():
    def __init__(self):
      self.val = None

    def __str__(self):
      return "{0!r} {1}".format(self, self.val)

    # the rest of the class definition will follow here, as per the previous logging script

  instance = None

  def __new__(cls):
    if not SingletonObject.instance:
      SingletonObject.instance = SingletonObject.__SingletonObject()

    return SingletonObject.instance

  def __getattr__(self, name):
    return getattr(self.instance, name)

  def __setattr__(self, name):
    return setattr(self.instance, name)

别慌。

乍一看,这里似乎发生了很多事情。让我们浏览一下代码,看看这是如何导致类的单个实例被创建的,或者在类被实例化时被返回的。

房间里的大象是我们SingletonObject类内部的类定义。前导下划线告诉其他程序员这是一个私有类,他们不应该在原始类定义之外使用它。私有类是我们实现记录器功能的地方,这是留给你的,作为本章末尾的一个练习。出于示例的目的,这个类有一个名为val的对象属性和两个方法:__init__,它实例化对象,和__str__,当您在print语句中使用对象时调用它。

接下来,我们看到一个叫做类属性实例的东西,它对于从SingletonObject类实例化的所有对象都是固定的。该属性被设置为None,这意味着当脚本开始执行时,instance没有值。

另一件我们不习惯看到的事情是在__new__函数的定义中使用了cls参数而没有self参数。这是因为__new__是一个类方法。它不是将对象作为参数,而是将类作为参数接收,然后使用类定义来构造该类的新实例。

到目前为止,您已经看到了许多带有两个前导下划线和两个尾随下划线的方法。你需要知道的是,在你到目前为止遇到的情况下,这些都是用来表示这些是神奇的方法,是 Python 解释器使用的方法,不需要你显式调用它们。

__new__函数中,我们看到每当程序员试图实例化一个类型为SingletonObject的对象时,解释器会首先检查类变量instance,看看是否存在这样的实例。如果没有现有的实例,它会创建一个私有类的新实例,并将其赋给instance类变量。最后,__new__函数返回instance类变量中的类。

我们还修改了__getattr____setattr__函数来调用保存在instance类变量中的私有类的属性。这将调用传递给包含在instance变量中的对象,就好像外部对象具有属性一样。

设置self.val属性只是为了表明对象保持不变,即使脚本试图多次实例化它。

很好——现在您可以使用该类来验证单例实现是否如您所愿。

test_singleton.py

from singleton_object import SingletonObject

obj1 = SingletonObject()

obj1.val = "Object value 1"
print("print obj1: ", obj1)

print("-----")

obj2 = SingletonObject()
obj2.val = "Object value 2"
print("print obj1: ", obj1)
print("print obj2: ", obj2)

以下是输出:

print obj1:  <__main__.SingletonObject.__SingletonObject object at 0x7fda5524def0> Object value 1
-----
print obj1:  <__main__.SingletonObject.__SingletonObject object at 0x7fda5524def0> Object value 2
print obj2:  <__main__.SingletonObject.__SingletonObject object at 0x7fda5524def0> Object value 2

对单例模式的主要批评是,它只是获得全局状态的一种很好的方式,这是编写程序时要避免的事情之一。您希望避免全局状态的原因之一是,项目中某个部分的代码可能会改变全局状态,并在完全不相关的代码中导致意外结果。

也就是说,当项目的某些部分不影响代码的执行时,比如日志记录,使用全局状态是可以接受的。其他可能使用全局状态的地方包括缓存、负载平衡和路由映射。在所有这些情况下,信息都是单向流动的,单例实例本身是不可变的(它不会改变)。程序的任何部分都不会试图对单例进行更改,因此,由于共享状态,不存在项目的一部分干扰项目的另一部分的危险。

好了,你已经发现了你的第一个设计模式,叫做单例模式。恭喜你!

练习

  • 实现你自己的日志单例。
  • 如何使用单例模式创建日志记录器?

(鸣谢:singleton 模式代码模板的灵感来自于 http://python-3-patterns-idioms-test.readthedocs.io/en/latest/Singleton.html CC-BY-SA 上的 singleton 模式实现。)

三、原型模式

现实不在乎你是否相信它。——波巴·费特,星球大战扩展宇宙

问题

我仍然记得我第一次想写程序的时候。当时,DOS 一次不能处理超过 20 MB 的数据,因此我们巨大的 40 MB 硬盘必须分成两个驱动器。那是一辆米色的奥利维蒂 XT。当地图书馆有一个区,里面有计算机书籍。其中一个是非常薄的软皮封面,上面有一个游戏角色的图片。标题承诺教你编程你自己的游戏。在一个被虚假广告左右的年轻无知的经典案例中,我仔细检查了每一个例子,一个一个地打出字符(当时我看不懂英语,所以我真的不知道我在做什么)。大约第十次之后,我把所有的东西都正确地输入到了电脑自带的 GW Basic 界面中。我不知道如何保存和加载程序,所以每一个错误都意味着从头开始。我的巅峰时刻完全没有变化。我如此努力的“游戏”变成了一个简单的for循环和一个if语句。这个游戏的前提是你在一辆失控的汽车里,在汽车最终陷入水坝之前,你有三次机会猜测一个数字。就是这样——没有图形,没有声音,没有漂亮的颜色,只有三遍同样的文字问题,然后:“你死了。”做对了会产生一个简单的信息:“耶!你答对了。”

超越最初的步骤

尽管我的第一个程序完全令人失望,但当我第一次拿起这本书,并相信我可以编写一个游戏或任何我可以梦想的东西时,那神奇的时刻一直伴随着我。

我猜很多人最初对编程感兴趣是出于对游戏的兴趣和对编程游戏的渴望。可悲的是,游戏往往是庞大而复杂的系统,制作一个有趣而受欢迎的游戏是一项巨大的事业,没有任何成功的保证。最后,相对来说很少有程序员去追求自己对游戏编程的最初兴趣。在这一章中,我们将想象我们确实是这个被选择的程序员群体的一部分。

真实游戏的基础

假设我们想写一个类似星际争霸的 RTS(即时策略游戏的简称),你有一个玩家控制一群角色。玩家需要建造建筑,生成单位,并最终达到某种策略目标。让我们考虑一个单位,一个骑士。骑士是在一个叫做兵营的建筑里产生的。一个玩家可以在一个场景中拥有多个这样的建筑,以便更快地创建骑士单位。

看看这个单元的描述和它与建筑的交互,一个相当明显的解决方案是定义一个Barracks类,这个类有一个返回Knight对象的generate_knight函数,这个对象是Knight类的一个实例。我们将为Knight类实现以下基本属性:

  • 生活
  • 速度
  • 攻击力
  • 攻击范围
  • 武器

生成一个新的Knight实例只需要实例化一个来自Knight类的对象并设置值。

下面是实现这一点的代码:

rts_simple.py

class Knight(object):
  def __init__(
    self,
    life,
    speed,
    attack_power,
    attack_range,
    weapon
   ):
     self.life = life
     self.speed = speed
     self.attack_power = attack_power
     self.attack_range = attack_range
     self.weapon = weapon

  def __str__(self):
    return  "Life: {0}\n" \
            "Speed: {1}\n" \
            "Attack Power: {2}\n" \
            "Attack Range: {3}\n" \
            "Weapon: {4}".format(
     self.life,
     self.speed,
     self.attack_power,
     self.attack_range,
     self.weapon
    )

class Barracks(object):
  def generate_knight(self):
    return Knight(400, 5, 3, 1, "short sword")

if __name__ == "__main__":
  barracks = Barracks()
  knight1 = barracks.generate_knight()
  print("[knight1] {}".format(knight1))

从命令行运行这段代码将创建一个Barracks类的barracks实例,然后将使用该实例生成knight1,这只是一个Knight类的实例,其值是为我们在上一节中定义的属性设置的。运行代码将以下文本打印到终端,这样您就可以检查knight实例是否与您基于generate_knight函数中设置的值所期望的相匹配。

[knight1] Life: 400
Speed: 5
Attack Power: 3
Attack Range: 1
Weapon: short sword

即使我们没有任何绘制逻辑或交互代码,用一些默认值生成一个新的骑士也是相当容易的。在本章的其余部分,我们将关注这个生成代码,看看它教我们如何创建多个几乎完全相同的对象。

Note

如果你有兴趣学习更多关于游戏编程的知识,我挑战你看看PyGame包;有关更多信息,请参见本章的“练习”一节。

如果你以前从未玩过或看过 RTS,重要的是要知道这些游戏的很大一部分乐趣来自于你可以生成的许多不同的单位。每个单位都有自己的优势和劣势,您如何利用这些独特的特征决定了您最终采用的策略。你越善于理解权衡,你就越善于制定有效的策略。

下一步是增加一个可以由兵营生成的角色。例如,我打算添加一个Archer类,但是可以随意添加一些你自己的单元类型,赋予它们独特的优点和缺点。你可以创造你梦想中的演员阵容。当你这么做的时候,可以自由的思考你的单位需要增加游戏深度的其他属性。当你读完这一章时,仔细阅读你写的代码,并加入这些想法。这不仅会让你的 RTS 更有趣,还会帮助你更好地理解整章的论点。

添加了Archer类后,我们的rts_simple.py现在看起来像这样:

rts_simple.py

class Knight(object):
  def __init__(
    self,
    life,
    speed,
    attack_power,
    attack_range,
    weapon
   ):
     self.unit_type = "Knight"
     self.life = life
     self.speed = speed
     self.attack_power = attack_power
     self.attack_range = attack_range
     self.weapon = weapon

  def __str__(self):
    return  "Type: {0}\n" \
            "Life: {1}\n" \
            "Speed: {2}\n" \
            "Attack Power: {3}\n" \
            "Attack Range: {4}\n" \
            "Weapon: {5}".format(
       self.unit_type,
       self.life,
       self.speed,

       self.attack_power,
       self.attack_range,
       self.weapon
    )

class Archer(object):
  def __init__(
    self,
    life,
    speed,
    attack_power,
    attack_range,
    weapon
   ):
   self.unit_type = "Archer"
   self.life = life
   self.speed = speed
   self.attack_power = attack_power
   self.attack_range = attack_range
   self.weapon = weapon

  def __str__(self):
   return  "Type: {0}\n" \
           "Life: {1}\n" \
           "Speed: {2}\n" \
           "Attack Power: {3}\n" \
           "Attack Range: {4}\n" \
           "Weapon: {5}".format(
      self.unit_type,
      self.life,
      self.speed,

      self.attack_power,
      self.attack_range,
      self.weapon
   )

class Barracks(object):
  def generate_knight(self):
    return Knight(400, 5, 3, 1, "short sword")

  def generate_archer(self):
    return Archer(200, 7, 1, 5, "short bow")

if __name__ == "__main__":
  barracks = Barracks()  knight1 = barracks.generate_knight()  archer1 = barracks.generate_archer()
  print("[knight1] {}".format(knight1))
  print("[archer1] {}".format(archer1))

接下来,您将看到运行这个程序的结果。我们现在有一个骑士和一个弓箭手,每个都有自己独特的单位属性。在继续解释结果之前,通读代码并尝试理解每一行是做什么的。

[knight1] Type: Knight
Life: 400
Speed: 5

Attack Power: 3
Attack Range: 1
Weapon: short sword
[archer1] Type: Archer
Life: 200
Speed: 7
Attack Power: 1
Attack Range: 5
Weapon: short bow

目前,只有两个单位需要考虑,但是既然你已经有时间考虑你要添加到你的单位生成器中的所有其他单位,很明显,为你计划在特定建筑中生成的每种类型的单位设置单独的功能并不是一个好主意。为了强调这一点,想象一下如果你想升级一个兵营所能生产的单位会发生什么。举个例子,考虑升级Archer职业,这样它制造的武器不再是短弓而是长弓,它的攻击范围增加了 5 点,攻击力增加了 2 点。突然,你在Barracks类中增加了两倍的函数,你需要保存一些兵营和它能生成的单位的状态记录,以确保你生成了正确的单位等级。

警钟现在应该响了。

必须有一种更好的方法来实现单元生成,这种方法不仅知道您希望它生成的单元的类型,还知道所讨论的单元的级别。实现这一点的一种方法是用一种叫做generate_unit的方法来代替单独的generate_knightgenerate_archer方法。这个方法的参数是要生成的单元类型,以及您希望它生成的单元级别。single 方法将使用该信息来分割成要创建的单元。我们还应该扩展单个单元类,根据单元实例化时传递给构造函数的一个level参数,改变用于不同单元属性的参数。

升级后的单元生成代码将如下所示:

rts_multi_unit.py

class Knight(object):
  def __init__(self, level):
     self.unit_type = "Knight"
     if level == 1:
       self.life = 400
       self.speed = 5
       self.attack_power = 3
       self.attack_range = 1
       self.weapon = "short sword"
     elif level == 2:
       self.life = 400
       self.speed = 5
       self.attack_power = 6
       self.attack_range = 2
       self.weapon = "long sword"

  def __str__(self):
    return  "Type: {0}\n" \
            "Life: {1}\n" \
            "Speed: {2}\n" \
            "Attack Power: {3}\n" \
            "Attack Range: {4}\n" \
            "Weapon: {5}".format(
       self.unit_type,
       self.life,
       self.speed,
       self.attack_power,

       self.attack_range,
       self.weapon
    )

class Archer(object):
  def __init__(self, level):
    self.unit_type = "Archer"
    if level == 1:
      self.life = 200
      self.speed = 7
      self.attack_power = 1
      self.attack_range = 5
      self.weapon = "short bow"
    elif level == 2:
      self.life = 200
      self.speed = 7
      self.attack_power = 3
      self.attack_range = 10
      self.weapon = "long bow"

  def __str__(self):
   return  "Type: {0}\n" \
           "Life: {1}\n" \
           "Speed: {2}\n" \
           "Attack Power: {3}\n" \
           "Attack Range: {4}\n" \
           "Weapon: {5}".format(
      self.unit_type,
      self.life,
      self.speed,
      self.attack_power,
      self.attack_range,
      self.weapon
   )

class Barracks(object):
  def build_unit(self, unit_type, level):
    if unit_type == "knight":
      return Knight(level)
    elif unit_type == "archer":
      return Archer(level)

if __name__ == "__main__":
  barracks = Barracks()
  knight1 = barracks.build_unit("knight", 1)
  archer1 = barracks.build_unit("archer", 2)
  print("[knight1] {}".format(knight1))
  print("[archer1] {}".format(archer1))

在接下来的结果中,你会看到代码生成了一个 1 级骑士和一个 2 级弓箭手,他们的个人参数符合你对他们的期望,而不需要Barracks类来跟踪每个单位的每个等级以及与他们相关的相关参数。

[knight1] Type: Knight
Life: 400
Speed: 5
Attack Power: 3
Attack Range: 1
Weapon: short sword
[archer1] Type: Archer
Life: 200

Speed: 7
Attack Power: 3
Attack Range: 10
Weapon: long bow

与前一个实现相比,我更喜欢这个实现,因为我们减少了所需的方法,并在单元类中隔离了单元级参数,在那里拥有它们更有意义。

在现代 RTS 游戏中,平衡是一个大问题——游戏设计师面临的主要挑战之一。游戏平衡背后的想法是,有时用户会找到一种方法来利用一个特定单位的特性,以这种方式压倒游戏中的其他策略或情况。尽管这听起来像是你想要找到的策略类型,但这实际上保证了玩家会对你的游戏失去兴趣。或者,一些角色可能有弱点,这使得它在游戏中几乎没有用处。在这两种情况下,有问题的单位(或游戏整体)被认为是不平衡的。一个游戏设计者想要改变每个单位的参数(比如攻击力)来解决这些不平衡。

挖掘成千上万行代码来查找每个类的参数值并修改它们,特别是如果开发人员在整个开发生命周期中必须这样做数百次的话。想象一下,对于像 Eve Online 这样严重依赖 Python 作为游戏逻辑基础的游戏来说,在一行又一行的代码中挖掘会是一件多么混乱的事情。

我们可以将参数存储在一个单独的 JSON 文件或数据库中,以允许游戏设计者在一个地方改变单位参数。为游戏设计者创建一个漂亮的 GUI(图形用户界面)是很容易的,在这里他们可以快速方便地进行修改,甚至不需要在文本编辑器中修改文件。

当我们想要实例化一个单元时,我们加载相关的文件或条目,提取我们需要的值,然后像以前一样创建实例,就像这样:

knight_1.dat

400

5

3

1

short sword

archer_1.dat

200

7

3

10

Long bow

rts_file_based.py

class Knight(object):
  def __init__(self, level):
     self.unit_type = "Knight"

     filename = "{}_{}.dat".format(self.unit_type, level)

     with open(filename, 'r') as parameter_file:
       lines = parameter_file.read().split("\n")
       self.life = lines[0]
       self.speed = lines[1]
       self.attack_power = lines[2]
       self.attack_range = lines[3]
       self.weapon = lines[4]

  def __str__(self):
    return  "Type: {0}\n" \
            "Life: {1}\n" \
            "Speed: {2}\n" \
            "Attack Power: {3}\n" \
            "Attack Range: {4}\n" \
            "Weapon: {5}".format(
       self.unit_type,
       self.life,
       self.speed,

       self.attack_power,
       self.attack_range,
       self.weapon
    )

class Archer(object):

  def __init__(self, level):
    self.unit_type = "Archer"

    filename = "{}_{}.dat".format(self.unit_type, level)

    with open(filename, 'r') as parameter_file:
      lines = parameter_file.read().split("\n")
      self.life = lines[0]
      self.speed = lines[1]
      self.attack_power = lines[2]
      self.attack_range = lines[3]
      self.weapon = lines[4]

  def __str__(self):
   return  "Type: {0}\n" \
           "Life: {1}\n" \
           "Speed: {2}\n" \
           "Attack Power: {3}\n" \
           "Attack Range: {4}\n" \
           "Weapon: {5}".format(
      self.unit_type,
      self.life,
      self.speed,
      self.attack_power,
      self.attack_range,
      self.weapon
   )

class Barracks(object):
  def build_unit(self, unit_type, level):
    if unit_type == "knight":
      return Knight(level)
    elif unit_type == "archer":
      return Archer(level)

if __name__ == "__main__":
  baracks = Baracks()
  knight1 = barracks.build_unit("knight", 1)
  archer1 = barracks.build_unit("archer", 2)
  print("[knight1] {}".format(knight1))
  print("[archer1] {}".format(archer1))

因为单元数据文件以可预测的顺序存储数据,所以很容易从磁盘获取文件,然后读入构建相关单元所需的参数。代码仍然交付与以前相同的结果,但是现在我们准备平衡许多不同的单元类型和级别。在我们的例子中,从文件导入对于ArcherKnight类来说看起来是一样的,但是我们必须记住,我们将有一些单元必须从它们的文件中导入不同的参数,所以一个单独的导入文件在现实场景中是不切实际的。

您可以验证您的结果是否与这些结果匹配:

[knight1] Type: Knight
Life: 400
Speed: 5
Attack Power: 3
Attack Range: 1
Weapon: short sword
[archer1] Type: Archer
Life: 200
Speed: 7
Attack Power: 3
Attack Range: 10
Weapon: long bow

玩家将可以建造同一个建筑的多个版本,正如我们之前讨论过的,我们希望一个特定的建筑可以产生的单位的等级和类型可以根据建筑的等级而改变。一个 1 级兵营只能产生 1 级骑士,但是一旦兵营升级到 2 级,它就会释放弓箭手单位,作为额外的奖励,它现在可以产生 2 级骑士而不是以前的 1 级骑士。升级一个建筑只会影响它所能生产的单位,而不会影响玩家建造的所有同类建筑的能力。我们不能简单地跟踪一个单元的单个实例;现在每栋建筑都需要跟踪自己的单元版本。

每当一个建筑想要创建一个单元时,它需要查找它可以创建什么单元,然后发出命令来创建选定的单元。然后,单元类必须查询存储系统以找到相关的参数,然后在将这些参数传递给正在创建的实例的类构造函数之前,从存储中读取这些参数。这都是非常低效的。如果一栋建筑需要生成 500 个相同类型的单元,您必须对您选择的存储系统提出 499 个重复请求。用这个乘以建筑的数量,然后加上每栋建筑需要做的查找来决定它应该能够产生哪些单位。像 Eve Online 这样的大型游戏,或者任何其他现代即时策略游戏,如果每次需要生成一个单位或者建造一个建筑时都需要经过这个过程,那么它会在短时间内杀死你的系统。一些游戏更进了一步,允许在建筑上添加特定的附件,赋予建筑中的单位不同于普通单位的能力,这将使系统更加消耗资源。

我们在这里有一个非常真实的需求,即创建大量几乎相同的对象,通过一两个小的调整来区分它们。正如我们所看到的,每次从头开始加载这些对象并不是一个可伸缩的解决方案。

这就引出了。。。

实现原型模式

在原型模式中,我们更喜欢组合而不是继承。由部件组成的类允许您在运行时替换那些部件,这极大地影响了系统的可测试性和可维护性。要实例化的类是在运行时通过动态加载来指定的。原型模式的这一特征的结果是子分类显著减少。客户端看不到创建新实例的复杂性。所有这些都很棒,但是这种模式的主要好处是它迫使你去编程一个接口,这导致了更好的设计。

Note

只是要注意,带有循环引用的深度克隆类可能会导致问题。请参见下一节,了解有关浅层拷贝与深层拷贝的更多信息。

我们只想复制手头上的某个对象。要确保副本按其应有的方式设置,并将对象的功能与系统的其余部分隔离,您要复制的实例应该提供复制功能。实例上的clone()方法克隆对象,然后相应地修改它的值,这将是理想的。

原型模式需要的三个组件如下:

  • 客户端通过要求原型克隆自己来创建一个新对象
  • Prototype 声明了一个用于克隆自身的接口
  • 具体原型实现了克隆自身的操作

(原型模式的树形组件: http://radek.io/2011/08/03/design-pattern-prototype/ )

在 RTS 的例子中,每个建筑都应该保留一个原型列表,它可以用它来生成单位,就像一个相同级别的单位列表,其属性与建筑的当前状态相匹配。当建筑物升级时,该列表会更新以匹配建筑物的新功能。我们要从等式中去掉 499 个多余的电话。这也是原型设计模式偏离抽象工厂设计模式的地方,我们将在本书的后面讨论这一点。通过交换建筑在运行时可以使用的原型,我们允许建筑动态地切换到生成完全不同的单元,而不需要对正在讨论的建筑类进行任何形式的子类化。我们的建筑实际上成为了原型管理器。

原型模式的想法很棒,但是在我们可以在我们的建筑中实现该模式之前,我们需要再看一个概念。

浅层拷贝与深层拷贝

Python 处理变量的方式与您可能遇到的其他编程语言略有不同。除了像整数和字符串这样的基本类型,Python 中的变量更接近于标记或标签,而不是其他编程语言用来比喻的桶。Python 变量本质上是指向存储相关值的内存地址的指针。

让我们看看下面的例子:

a = range(1,6)
print("[a] {}".format(a))
b = a
print("[b] {}".format(b))
b.append(6)
print("[a] {}".format(a))
print("[b] {}".format(b))

第一条语句将变量a指向从 1 到 6(不包括 6)的数字列表。接下来,变量b被分配给a所指向的同一个数字列表。最后,数字 6 被添加到由b指向的列表的末尾。

您预计print陈述的结果会是什么?看看下面的输出。它符合你的期望吗?

[a] [1, 2, 3, 4, 5]
[b] [1, 2, 3, 4, 5]
[a] [1, 2, 3, 4, 5, 6]
[b] [1, 2, 3, 4, 5, 6]

大多数人感到惊讶的是,数字 6 被附加到列表a指向的和列表b指向的末尾。如果你记得事实上ab指向同一个列表,很明显,添加一个元素到列表的末尾应该显示添加到列表末尾的元素,不管你在看哪个变量。

浅拷贝

当我们想要制作一个列表的实际副本,并在过程结束时拥有两个独立的列表时,我们需要使用不同的策略。

就像之前一样,我们将把相同的数字加载到一个列表中,并将变量a指向该列表。这一次,我们将使用 slice 操作符来复制列表。

a = range(1,6)
print("[a] {}".format(a))
b = a[:]
print("[b] {}".format(b))
b.append(6)
print("[a] {}".format(a))
print("[b] {}".format(b))

slice 操作符是一个非常有用的工具,因为它允许我们以许多不同的方式分割一个列表(或字符串)。想从一个列表中获取所有的元素,而忽略前两个元素吗?没问题——只需用[2:]分割列表。除了最后两个元素之外的所有元素呢?切片做的:[:-2]。您甚至可以要求 slice 操作符只给你每隔一个元素。我向你挑战,看你是如何做到的。

当我们将b赋值给a[:]时,我们告诉b指向通过复制列表a中的元素创建的切片,从第一个到最后一个元素,有效地将列表a中的所有值复制到内存中的另一个位置,并将变量b指向该列表。

你看到的结果让你惊讶吗?

[a] [1, 2, 3, 4, 5]
[b] [1, 2, 3, 4, 5]
[a] [1, 2, 3, 4, 5]
[b] [1, 2, 3, 4, 5, 6]

slice 操作符在处理浅层列表(只包含实际值的列表,不包含对列表或字典等其他复杂对象的引用)时非常有效。

处理嵌套结构

看看下面的代码。你认为结果会是什么?

lst1 = ['a', 'b', ['ab', 'ba']]
lst2 = lst1[:]
lst2[0] = 'c'
print("[lst1] {}".format(lst1))
print("[lst2] {}".format(lst2))

在这里,您可以看到一个深度列表的示例。看一看结果——它们与你的预测相符吗?

[lst1] ['a', 'b', ['ab', 'ba']]
[lst2] ['c', 'b', ['ab', 'ba']]

正如您可能已经从浅层复制示例中预料到的,改变lst2的第一个元素并不会改变lst1的第一个元素,但是当我们改变列表中的一个元素时会发生什么呢?

lst1 = ['a', 'b', ['ab', 'ba']]
lst2 = lst1[:]
lst2[2][1] = 'd'
print("[lst1] {}".format(lst1))
print("[lst2] {}".format(lst2))

你能解释我们这次得到的结果吗?

[lst1] ['a', 'b', ['ab', 'd']]
[lst2] ['a', 'b', ['ab', 'd']]

你对发生的事情感到惊讶吗?

列表lst1包含三个元素,'a''b',以及一个指向另一个列表的指针,看起来像这样:['ab', 'ba']。当我们对lst1进行浅层复制来创建lst2指向的列表时,只有列表中某一层的元素被复制。未克隆包含在lst1中位置 2 的元素地址处的结构;只有指向内存中列表['ab', 'ba']位置的值。结果,lst1lst2都指向包含字符'a''b'的单独列表,后面是指向包含['ab', 'ba']的同一列表的指针,这将导致每当某个函数更改该列表中的元素时都会出现问题,因为它会影响另一个列表的内容。

深层拷贝

显然,在克隆对象时,我们需要另一种解决方案。我们如何才能强迫 Python 对列表及其子列表或对象中包含的所有内容进行完整的复制,即深度复制?幸运的是,我们将copy模块作为标准库的一部分。copy包含一个方法deep-copy,它允许一个任意列表的完整深度拷贝;即浅等列表。我们现在将使用深度拷贝来修改前面的示例,以便获得我们期望的输出。

from copy import deepcopy

lst1 = ['a', 'b', ['ab', 'ba']]
lst2 = deepcopy(lst1)
lst2[2][1] = 'd'
print("[lst1] {}".format(lst1))
print("[lst2] {}".format(lst2))

导致:

[lst1] ['a', 'b', ['ab', 'ba']]
[lst2] ['a', 'b', ['ab', 'd']]

那里。现在我们有了预期的结果,经过了相当长的一段弯路,我们准备看看这对 RTS 中的建筑意味着什么。

利用我们在项目中学到的知识

在本质上,原型模式只是一个clone()函数,它接受一个对象作为输入参数,并返回它的一个克隆。

原型模式实现的框架应该声明一个指定纯虚拟clone()方法的抽象基类。任何需要多态构造函数(该类根据实例化时收到的参数数量决定使用哪个构造函数)功能的类都从抽象基类中派生出来,并实现clone()方法。每个单元都需要从这个抽象基类中派生出来。客户机调用原型上的clone()方法,而不是编写调用硬编码类名上的new操作符的代码。

一般来说,原型模式应该是这样的:

prototype_1.py

from abc import ABCMeta, abstractmethod

class Prototype(metaclass=ABCMeta):
  @abstractmethod
  def clone(self):
    pass

concrete.py

from prototype_1 import Prototype
from copy import deepcopy

class Concrete(Prototype):
  def clone(self):
    return deepcopy(self)

最后,我们可以用原型模式实现我们的单元生成构建,使用相同的prototype_1.py文件。

rts_prototype.py

from prototype_1 import Prototype
from copy import deepcopy

class Knight(Prototype):
  def __init__(self, level):
     self.unit_type = "Knight"

     filename = "{}_{}.dat".format(self.unit_type, level)

     with open(filename, 'r') as parameter_file:
       lines = parameter_file.read().split("\n")
       self.life = lines[0]
       self.speed = lines[1]
       self.attack_power = lines[2]
       self.attack_range = lines[3]
       self.weapon = lines[4]

  def __str__(self):
    return  "Type: {0}\n" \
            "Life: {1}\n" \
            "Speed: {2}\n" \
            "Attack Power: {3}\n" \
            "Attack Range: {4}\n" \
            "Weapon: {5}".format(
       self.unit_type,
       self.life,
       self.speed,
       self.attack_power,
       self.attack_range,
       self.weapon
    )

  def clone(self):
    return deepcopy(self)

class Archer(Prototype):

  def __init__(self, level):
    self.unit_type = "Archer"

    filename = "{}_{}.dat".format(self.unit_type, level)

    with open(filename, 'r') as parameter_file:
      lines = parameter_file.read().split("\n")
      self.life = lines[0]
      self.speed = lines[1]
      self.attack_power = lines[2]
      self.attack_range = lines[3]
      self.weapon = lines[4]

  def __str__(self):
   return  "Type: {0}\n" \
           "Life: {1}\n" \
           "Speed: {2}\n" \
           "Attack Power: {3}\n" \
           "Attack Range: {4}\n" \
           "Weapon: {5}".format(
      self.unit_type,
      self.life,
      self.speed,
      self.attack_power,
      self.attack_range,
      self.weapon
   )

  def clone(self):
   return deepcopy(self)

class Barracks(object):
  def __init__(self):
    self.units = {
      "knight": {
        1: Knight(1),
        2: Knight(2)
      },
      "archer": {
        1: Archer(1),
        2: Archer(2)
      }
    }

  def build_unit(self, unit_type, level):
    return self.units[unit_type][level].clone()

if __name__ == "__main__":
  barracks = Baracks()
  knight1 = barracks.build_unit("knight", 1)
  archer1 = barracks.build_unit("archer", 2)
  print("[knight1] {}".format(knight1))
  print("[archer1] {}".format(archer1))

当我们在单位类中扩展抽象基类时,这迫使我们实现了clone方法,我们在让兵营生成新单位时使用了这个方法。我们做的另一件事是生成一个Barracks实例可以生成的所有选项,并将它保存在一个单元的数组中。现在,我们可以简单地用正确的级别克隆这个单元,而不需要打开一个文件或者从外部数据源加载任何数据。

[knight1] Type: Knight
Life: 400
Speed: 5
Attack Power: 3
Attack Range: 1
Weapon: short sword
[archer1] Type: Archer
Life: 200
Speed: 7
Attack Power: 3
Attack Range: 10
Weapon: long bow

干得好;你不仅迈出了创建你自己的 RTS 的第一步,而且你还深入挖掘了一个非常有用的设计模式。

练习

  • 以原型示例代码为例,扩展Archer类来处理它的升级。
  • 尝试在兵营建筑中增加更多的单位。
  • 添加第二种类型的建筑。
  • 看看 PyGame 包( http://pygame.org/hifi.html )。如何扩展你的Knight类,使它能在游戏循环中绘制自己?
  • 作为一个练习,您可以查看 PyGame 包并为Knight类实现一个draw()方法,以便能够在地图上绘制它。
  • 如果你有兴趣,试着用 PyGame 写一个自己的迷你 RTS 游戏,里面有一个兵营和两个单位。
  • 扩展每个单元的clone方法,生成一个随机名称,这样每个克隆的单元都会有一个不同的名称。

四、工厂模式

质量意味着在没人注意的时候把事情做好。—亨利·福特

在第三章中,你开始考虑编写自己的游戏。为了确保你不会被一个纯文本的“游戏”所欺骗,让我们花一点时间来看看在屏幕上画些什么。在这一章中,我们将接触到使用 Python 制作图形的基础知识。我们将使用PyGame套装作为我们的首选武器。我们将创建工厂类。工厂类定义了接受一组特定参数的对象,并使用这些参数来创建其他类的对象。我们还将定义抽象工厂类,作为构建这些工厂类的模板。

入门指南

在虚拟环境中,您可以使用以下命令使用 pip 安装 PyGame:

pip install pygame

这应该是相当无痛的。现在,要获得一个实际的窗口:

graphic_base.py

import pygame

pygame.init()
screen = pygame.display.set_mode((800, 600))

保存graphic_base.py并运行文件:

python graphic_base.py

一个 400 像素宽、300 像素高的空白窗口会在你的屏幕上弹出,然后立刻消失。恭喜你,你已经创建了你的第一个屏幕。显然,我们希望屏幕停留的时间长一点,所以我们只需添加一个sleep函数to graphic_base.py

graphic_base.py

import pygame
from time import sleep

pygame.init()
screen = pygame.display.set_mode((800, 600))

sleep(10)

time包(标准库的一部分)中,我们导入了sleep函数,该函数在给定的秒数内暂停执行。在脚本的末尾添加了一个十秒钟的睡眠,在脚本完成执行和窗口消失之前,窗口保持打开十秒钟。

当我第一次在屏幕上创建一个窗口时,我非常兴奋,但当我叫我的室友过来给他看我的窗口时,他完全不感兴趣。我建议你在展示你的新作品之前在窗户上加点东西。扩展graphic_base.py向窗口添加一个正方形。

graphic_base.py

import pygame
import time

pygame.init()
screen = pygame.display.set_mode((800,600))

pygame.draw.rect(screen, (255, 0, 34), pygame.Rect(42, 15, 40, 32))
pygame.display.flip()

time.sleep(10)

pygame.draw.rect函数为指向你的窗口的screen变量画一个矩形。第二个参数是包含用于填充形状的颜色的元组,最后,pygame 矩形与左上角和右下角的坐标一起传入。您在颜色元组中看到的三个值组成了所谓的 RGB 值(红绿蓝),每个分量都是 255 中的一个值,它指示最终颜色混合中分量颜色的强度。

如果省略pygame.display.flip(),则不会显示任何形状。这是因为 PyGame 在内存缓冲区中绘制屏幕,然后将整个图像翻转到活动屏幕上(这就是您所看到的)。每次你更新显示时,你必须调用pygame.display.flip()让变化显示在屏幕上。

尝试在屏幕上用多种颜色绘制不同的矩形。

游戏循环

游戏编程中最基本的概念叫做游戏循环。这个想法是这样工作的:游戏检查用户的一些输入,用户做一些计算来更新游戏的状态,然后给玩家一些反馈。在我们的例子中,我们只是更新玩家在屏幕上看到的东西,但是你可以包括声音或触觉反馈。这种情况反复发生,直到玩家退出。每次屏幕更新时,我们运行pygame.display.flip()函数向玩家显示更新后的显示。

游戏的基本结构如下:

  • 进行一些初始化,例如设置窗口和屏幕上元素的初始位置和颜色。
  • 当用户不退出游戏时,运行游戏循环。
  • 当用户退出时,终止窗口。

在代码中,它可能是这样的:

graphic_base.py

import pygame

window_dimensions = 800, 600
screen = pygame.display.set_mode(window_dimensions)

player_quits = False

while not player_quits:
  for event in pygame.event.get():
    if event.type == pygame.QUIT:
      player_quits = True

  pygame.display.flip()

此时,这段代码什么也不做,只是等待玩家点击窗口上的关闭按钮,然后终止执行。为了使它更具交互性,让我们添加一个小方块,并让它在用户按下其中一个箭头键时移动。

为此,我们需要在屏幕上的初始位置画一个正方形,准备好对箭头键事件做出反应。

shape_game.py(见 ch04_03.py)

import pygame

window_dimensions = 800, 600
screen = pygame.display.set_mode(window_dimensions)

x = 100
y = 100

player_quits = False

while not player_quits:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            player_quits = True

        pressed = pygame.key.get_pressed()
        if pressed[pygame.K_UP]: y -= 4
        if pressed[pygame.K_DOWN]: y += 4
        if pressed[pygame.K_LEFT]: x -= 4
        if pressed[pygame.K_RIGHT]: x += 4

        screen.fill((0, 0, 0))
        pygame.draw.rect(screen, (255, 255, 0), pygame.Rect(x, y, 20, 20))

    pygame.display.flip()

稍微试验一下代码,看看是否可以让块不移动到窗口的边界之外。

现在你的积木可以在屏幕上移动了,那么做一圈怎么样?然后形成一个三角形。现在是游戏角色图标。。。你明白了。突然,大量显示代码堵塞了游戏循环。如果我们使用面向对象的方法来解决这个问题,会怎么样呢?

shape_game.py

import pygame

class Shape(object):
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def draw(self):
        raise NotImplementedError()

    def move(self, direction):
        if direction == 'up':
            self.y -= 4
        elif direction == 'down':
            self.y += 4
        elif direction == 'left':
            self.x -= 4
        elif direction == 'right':
            self.x += 4

class Square(Shape):
    def draw(self):
        pygame.draw.rect(
            screen,
            (255, 255, 0),
            pygame.Rect(self.x, self.y, 20, 20)
        )

class Circle(Shape):
    def draw(self):
        pygame.draw.circle(
            screen,
            (0, 255, 255),
            (selfx, self.y),
            10
        )

if __name__ == '__main__':
    window_dimensions = 800, 600
    screen = pygame.display.set_mode(window_dimensions)

    square = Square(100, 100)
    obj = square

    player_quits = False

    while not player_quits:
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                player_quits = True

            pressed = pygame.key.get_pressed()
            if pressed[pygame.K_UP]: obj.move('up')
            if pressed[pygame.K_DOWN]: obj.move('down')
            if pressed[pygame.K_LEFT]: obj.move('left')
            if pressed[pygame.K_RIGHT]: obj.move('right')

            screen.fill((0, 0, 0))
            obj.draw()

        pygame.display.flip()

既然你现在已经有了圆形和方形的对象,考虑一下你将如何改变程序,以便当你按下“C”键时,屏幕上的对象变成一个圆形(如果它现在不是一个的话)。同样,当你按下“S”键时,形状会变成正方形。看看在游戏循环中使用对象比处理所有这些要容易得多。

Tip

看看 PyGame 的按键绑定以及本章中的代码。

仍然有一两个我们可以实现的改进,比如抽象出像movedraw这样每个类都必须发生的事情,这样我们就不必跟踪我们正在处理的是什么形状。我们希望能够引用一般的形状,只告诉它画自己,而不用担心它是什么形状(或者如果它是一个形状,开始,而不是一些图像或甚至一帧动画)。

显然,多态性不是一个完整的解决方案,因为每当我们创建一个新对象时,我们都必须不断更新代码,在一个大型游戏中,这种情况会发生在许多地方。问题是新类型的创建,而不是这些类型的使用。

既然你想写更好的代码,当你试图想出一个更好的方法来处理我们想要添加到变形人游戏中的扩展时,想想好代码的以下特征。

好的代码是

  • 易于维护,
  • 易于更新,
  • 易于扩展,并且
  • 很清楚它想要完成什么。

好的代码应该让你几周前写的东西变得尽可能的简单。你最不希望的就是在你害怕工作的代码中创建这些区域。

我们希望能够通过一个公共接口来创建对象,而不是将创建代码分散到整个系统中。这将本地化在您更新可以创建的形状类型时需要更改的代码。由于添加新类型是您最有可能对系统进行的添加,因此这是您在代码改进方面必须首先关注的领域之一是有意义的。

创建集中式对象创建系统的一种方法是使用工厂模式。这种模式有两种不同的方法,我们将同时介绍这两种方法,从更简单的工厂方法开始,然后转到抽象工厂实现。我们也将看看如何根据我们的游戏框架来实现这些。

在我们进入工厂模式之前,我希望你注意到原型模式和工厂模式之间有一个主要的区别。原型模式不需要子类化,但是需要一个initialize操作,而工厂模式需要子类化,但是不需要初始化。每一种都有自己的优势和你应该选择的地方,通过本章的学习,这种区别会变得更加清晰。

工厂方法

当我们想调用一个方法,传入一个字符串并获得一个新对象的返回值时,我们实际上是在调用一个工厂方法。对象的类型由传递给方法的字符串确定。

这使得通过允许您向软件添加功能来扩展您编写的代码变得容易,这是通过添加新的类并扩展工厂方法以接受新的字符串并返回您创建的类来完成的。

让我们看一下工厂方法的一个简单实现。

shape_ factory.py

import pygame

class Shape(object):
  def __init__(self, x, y):
      self.x = x
      self.y = y

  def draw(self):
      raise NotImplementedError()

  def move(self, direction):
      if direction == 'up':
          self.y -= 4
      elif direction == 'down':
          self.y += 4
      elif direction == 'left':
          self.x -= 4
      elif direction == 'right':
          self.x += 4

  @staticmethod  
  def factory(type):
    if type == "Circle":
      return Circle(100, 100)
    if type == "Square":
      return Square(100, 100)

    assert 0, "Bad shape requested: " + type

class Square(Shape):
    def draw(self):
        pygame.draw.rect(
            screen,
            (255, 255, 0),
            pygame.Rect(self.x, self.y, 20, 20)
        )

class Circle(Shape):
    def draw(self):
        pygame.draw.circle(
            screen,
            (0, 255, 255),
            (selfx, self.y),
            10
        )

if __name__ == '__main__':
    window_dimensions = 800, 600
    screen = pygame.display.set_mode(window_dimensions)

    obj = Shape.factory("square")

    player_quits = False

    while not player_quits:
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                player_quits = True

            pressed = pygame.key.get_pressed()
            if pressed[pygame.K_UP]: obj.move('up')
            if pressed[pygame.K_DOWN]: obj.move('down')
            if pressed[pygame.K_LEFT]: obj.move('left')
            if pressed[pygame.K_RIGHT]: obj.move('right')

            screen.fill((0, 0, 0))
            obj.draw()

        pygame.display.flip()

修改上面的这段代码,使其从正方形变成圆形,或者变成您想要的任何其他形状或图像,会容易多少呢?

工厂方法的一些拥护者建议所有的构造函数都应该是私有的或受保护的,因为无论是创建一个新对象还是回收一个旧对象,对该类的用户来说都无关紧要。其思想是将对象的请求与其创建分离开来。

这个想法不应该作为教条来遵循,而是在你自己的项目中尝试一下,看看你从中获得了什么好处。一旦你习惯了使用工厂方法,可能还有工厂模式,你就可以自由地使用你自己的判断力来判断模式在你手头的项目中的有用性。

每当你在游戏中添加一个需要在屏幕上绘制的新类时,你只需要改变factory()方法。

当我们需要不同种类的工厂时会发生什么?也许你想加入音效工厂或环境元素而不是玩家角色工厂?您希望能够从同一个基本工厂创建不同类型的工厂子类。

抽象工厂

当您想要创建一个单一的接口来访问整个工厂集合时,您可以放心地使用抽象工厂。集合中的每个抽象工厂都需要实现一个预定义的接口,该接口上的每个函数都根据工厂方法模式返回另一个抽象类型。

import abc

class AbstractFactory(object):
  __metaclass__ = abc.ABCMeta

  @abc.abstractmethod
  def make_object(self):
    return

class CircleFactory(AbstractFactory):
  def make_object(self):
    # do something

    return Circle()

class SquareFactory(AbstractFactory):
  def make_object(self):
    # do something

    return Square()

这里,我们使用了内置的abc模块,它允许您定义一个抽象基类。在这个例子中,抽象工厂定义了定义具体工厂的蓝图,然后具体工厂创建圆形和方形。

Python 是动态类型的,所以不需要定义公共基类。如果我们想让代码更 pythonic 化,我们会看到这样的内容:

class CircleFactory(AbstractFactory):
  def make_object(self):
    # do something

    return Circle()

class SquareFactory(AbstractFactory):
  def make_object(self):
    # do something

    return Square()

def draw_function(factory):
  drawable = factory.make_object()
  drawable.draw()

def prepare_client():
  squareFactory = SquareFactory()
  draw_function(squareFactory)

  circleFactory = CircleFactory()
  draw_function(circleFactory)

在我们到目前为止设置的准系统游戏中,您可以想象工厂生成包含一个play()方法的对象,您可以在游戏循环中运行该方法来播放声音、计算移动或在屏幕上绘制形状和图像。抽象工厂的另一个常见用途是为不同操作系统的 GUI 元素创建工厂。它们都使用相同的基本功能,但是所创建的工厂是根据程序运行的操作系统来选择的。

恭喜你!你刚刚提升了你写伟大代码的能力。使用抽象工厂,您可以编写更容易修改和测试的代码。

摘要

当开发软件时,你想要防止一种令人烦恼的感觉,即你应该构建一些能够迎合每一个可能的未来事件的东西。尽管考虑你的软件的未来是好的,但是试图构建一个如此通用的软件来解决每一个可能的未来需求通常是徒劳的。最简单的原因是没有办法预见你的软件将走向何方。我并不是说你应该太天真,不考虑你的代码的未来,但是作为一名软件开发人员,能够改进你的代码以包含新的功能是你将学到的最有价值的技能之一。总是有一种诱惑,要扔掉你以前写的所有旧代码,从头开始“把它做好”这是一个梦想,一旦你“做对了”,你就会学到一些新的东西,让你的代码看起来不那么漂亮,所以你将不得不重做一切,永远不会完成任何事情。

总的原则是 YAGNI;在您的软件开发职业生涯中,您可能会遇到这个缩写。这是什么意思?你不需要它!原则是您应该编写能很好地解决当前问题的代码,并且只在需要时修改它来解决后续问题。

这就是为什么许多软件设计开始使用更简单的工厂方法,只有当开发人员发现哪里需要更多的灵活性时,他才会发展程序来使用抽象工厂、原型或构建器模式。

练习

  • 添加通过按键在圆形、三角形和正方形之间切换的功能。
  • 尝试实现一个图像对象,而不仅仅是一个形状。
  • 增强您的图像对象类,以便在分别向上、向下、向左和向右移动时,在要绘制的独立图像之间进行切换。
  • 作为一个挑战,尝试在移动时给图像添加动画。

五、构建器模式

他能修好它吗?是的,他可以!——《建筑者鲍勃》主题曲

如果你从事软件工作一段时间,你将不得不处理来自用户的输入——从让玩家在游戏中输入他们的角色名字到在电子表格的单元格中添加数据。表单通常是应用的核心。实际上,大多数应用只是信息的输入、转换和反馈流的特例。有许多输入小部件和界面,但最常见的仍然是以下这些:

  • 正文框
  • 下拉和选择列表
  • 复选框
  • 单选按钮
  • 文件上传字段
  • 小跟班

这些可以以许多不同的方式混合和匹配,以从用户那里获取不同类型的数据作为输入。本章我们感兴趣的是如何编写一个脚本来简化生成这些表单的工作。对于我们的用例,我们将生成 HTML webforms,但是同样的技术也可以用来生成移动应用接口、表单的 JSON 或 XML 表示,或者任何你能想到的东西。让我们从编写一个简单的函数开始,它将为我们生成一个小表单。

basic_form_generator.py

def generate_webform(field_list):
    generated_fields = "\n".join(
        map(
            lambda x: '{0}:<br><input type="text" name="{0}"><br>'.format(x),
            field_list
          )
    )
    return "<form>{fields}</form>".format(fields=generated_fields)

if __name__ == "__main__":
    fields = ["name", "age", "email", "telephone"]
    print(generate_webform(fields))

在这个简单的例子中,代码假设字段列表只包含文本字段。字段名作为字符串包含在列表中。列表中的字符串也是返回的表单中使用的标签。对于列表中的每个元素,都会向 webform 添加一个字段。然后,webform 作为包含生成的 webform 的 HTML 代码的字符串返回。

如果我们更进一步,我们可以让一个脚本获取生成的响应,并从它构建一个常规的 HTML 文件,一个可以在 web 浏览器中打开的文件。

html_ 表单 _ 生成器. py

def generate_webform(field_list):
    generated_fields = "\n".join(
        map(
            lambda x: '{0}:<br><input type="text" name="{0}"><br>'.format(x),
            field_list
          )
    )
    return "<form>{fields}</form>".format(fields=generated_fields)

def build_html_form(fields):
    with open("form_file.html", w) as f:
      f.write(
        "<html><body>{}</body></html>".format(generate_webform(fields))
      )

if __name__ == "__main__":
    fields = ["name", "age", "email", "telephone"]
    build_html_form(fields)

正如我在本章开始时提到的,webforms(以及一般的表单)可以有比简单的文本字段更多的字段类型。我们可以使用命名参数向表单添加更多的字段类型。请看下面的代码,我们将在其中添加复选框。

html_ 表单 _ 生成器. py

def generate_webform(text_field_list=[], checkbox_field_list=[]):
  generated_fields = "\n".join(
    map(
      lambda x: '{0}:<br><input type="text" name="{0}"><br>'.format(x),
      text_field_list
    )
  )

  generated_fields += "\n".join(
    map(
      lambda x: '<label><input type="checkbox" id="{0}" value="{0}"> {0}<br>'.format(x),
      checkbox_field_list
    )
  )

  return "<form>{fields}</form>".format(fields=generated_fields)

def build_html_form(text_field_list=[], checkbox_field_list=[]):
  with open("form_file.html", 'w') as f:
    f.write(
      "<html><body>{}</body></html>".format(
        generate_webform(
          text_field_list=text_field_list,
          checkbox_field_list=checkbox_field_list
        )
    )
  )

if __name__ == "__main__":
  text_fields = ["name", "age", "email", "telephone"]
  checkbox_fields = ["awesome", "bad"]
  build_html_form(text_field_list=text_fields, checkbox_field_list=checkbox_fields)

这种方法有明显的问题,第一个问题是,我们不能处理具有不同默认值或选项的字段,或者事实上除了简单的标签或字段名之外的任何信息。我们甚至不能考虑表单中使用的字段名和标签之间的差异。我们现在要扩展表单生成器函数的功能,这样我们就可以满足大量的字段类型,每种类型都有自己的设置。我们还有一个问题,那就是没有办法交错不同类型的字段。为了向您展示这是如何工作的,我们去掉了命名参数,并用一个字典列表替换它们。该函数将在列表中的每个字典中查找,然后使用包含的信息生成字典中定义的字段。

html _ 字典 _ 表单 _ 生成器. py

def generate_webform(field_dict_list):
    generated_field_list = []

    for field_dict in field_dict_list:
        if field_dict["type"] == "text_field":
            generated_field_list.append(
                '{0}:<br><input type="text" name="{1}"><br>'.format(
                    field_dict["label"],
                    field_dict["name"]
                )
            )
        elif field_dict["type"] == "checkbox":
            generated_field_list.append(
                '<label><input type="checkbox" id="{0}" value="{1}"> {2}<br>'.format(
                    field_dict["id"],
                    field_dict["value"],
                    field_dict["label"]
                )
         )

    generated_fields = "\n".join(generated_field_list)

    return "<form>{fields}</form>".format(fields=generated_fields)

def build_html_form(field_list):
    with open("form_file.html", 'w') as f:
        f.write(
            "<html><body>{}</body></html>".format(
                generate_webform(field_list)
            )
        )

if __name__ == "__main__":
    field_list = [
        {
        "type": "text_field",
        "label": "Best text you have ever written",
        "name": "best_text"
        },
        {
        "type": "checkbox",
        "id": "check_it",
        "value": "1",
        "label": "Check for one",
        },
        {
        "type": "text_field",
        "label": "Another Text field",
        "name": "text_field2"
        }
    ]

    build_html_form(field_list)

字典包含一个type字段,用于选择要添加的字段类型。然后,字典中有一些可选元素,如标签、名称和选项(在选择列表的情况下)。您可以以此为基础创建一个表单生成器,它可以生成您能想到的任何表单。现在你应该闻到一股臭味了。将循环和条件语句堆叠在一起会很快变得不可读和不可维护。让我们稍微清理一下代码。

我们将提取每个字段的生成代码,并将其放入一个单独的函数中,该函数获取字典并返回表示该字段的 HTML 代码片段。这一步的关键是在不改变主函数的任何功能、输入或输出的情况下清理代码。

cleaned _ html _ dictionary _ form _ generator . py

def generate_webform(field_dict_list):
    generated_field_list = []

    for field_dict in field_dict_list:
        if field_dict["type"] == "text_field":
            field_html = generate_text_field(field_dict)
        elif field_dict["type"] == "checkbox":
            field_html = generate_checkbox(field_dict)

        generated_field_list.append(field_html)

    generated_fields = "\n".join(generated_field_list)

    return "<form>{fields}</form>".format(fields=generated_fields)

def generate_text_field(text_field_dict):
  return '{0}:<br><input type="text" name="{1}"><br>'.format(
      text_field_dict["label"],
      text_field_dict["name"]
  )

def generate_checkbox(checbox_dict):
  return '<label><input type="checkbox" id="{0}" value="{1}"> {2}<br>'.format(
      checkbox_dict["id"],
      checkbox_dict["value"],
      checkbox_dict["label"]
  )

def build_html_form(field_list):
    with open("form_file.html", 'w') as f:
        f.write(
            "<html><body>{}</body></html>".format(
                generate_webform(field_list)
            )
        )

if __name__ == "__main__":
    field_list = [
        {
        "type": "text_field",
        "label": "Best text you have ever written",
        "name": "best_text"
        },
        {
        "type": "checkbox",
        "id": "check_it",
        "value": "1",
        "label": "Check for one",
        },
        {
        "type": "text_field",
        "label": "Another Text field",
        "name": "text_field2"
        }
    ]

    build_html_form(field_list)

语句还在,但至少现在代码更干净了。随着我们向表单生成器添加更多的字段类型,这些杂乱无章的 if 语句将会越来越多。让我们通过使用面向对象的方法来改善这种情况。我们可以使用多态性来处理一些特定的字段以及在生成这些字段时遇到的问题。

oop_html_ form_ generator.py

class HtmlField(object):
    def __init__(self, **kwargs):
        self.html = ""

        if kwargs['field_type'] == "text_field":
            self.html = self.construct_text_field(kwargs["label"], kwargs["field_name"])

        elif kwargs['field_type'] == "checkbox":
            self.html = self.construct_checkbox(kwargs["field_id"], kwargs["value"], kwargs["label"])

    def construct_text_field(self, label, field_name):
        return '{0}:<br><input type="text" name="{1}"><br>'.format(
              label,
              field_name
        )

    def construct_checkbox(self, field_id, value, label):
         return '<label><input type="checkbox" id="{0}" value="{1}"> {2}<br>'.format(
            field_id,
            value,
            label
            )

    def __str__(self):
        return self.html

def generate_webform(field_dict_list):
    generated_field_list = []
    for field in field_dict_list:
        try:
            generated_field_list.append(str(HtmlField(**field)))
        except Exception as e:
            print("error: {}".format(e))

    generated_fields = "\n".join(generated_field_list)

    return "<form>{fields}</form>".format(fields=generated_fields)

def build_html_form(field_list):
    with open("form_file.html", 'w') as f:
        f.write(
            "<html><body>{}</body></html>".format(
                generate_webform(field_list)
            )
        )

if __name__ == "__main__":
    field_list = [
        {
            "field_type": "text_field",
            "label": "Best text you have ever written",
            "field_name": "Field One"
        },
        {
            "field_type": "checkbox",
            "field_id": "check_it",
            "value": "1",
            "label": "Check for on",
        },
        {
            "field_type": "text_field",
            "label": "Another Text field",
            "field_name": "Field One"
        }
    ]

    build_html_form(field_list)

对于每一组可选的参数,我们都需要另一个构造函数,所以我们的代码很快就会崩溃,变成一堆构造函数,这就是通常所说的伸缩构造函数反模式。像设计模式一样,由于现实世界中软件设计和开发过程的性质,反模式是您经常会看到的错误。对于一些语言,比如 Java,您可以根据接受的参数和随后构造的内容,用许多不同的选项来重载构造函数。另外,请注意,构造函数__init__()中的if条件可以定义得更清楚。

我相信你对这个实现有很多反对意见,你应该反对。在我们进一步清理之前,我们将快速查看一下反模式。

反模式

顾名思义,反模式是软件模式的对外观。也是软件开发过程中定期出现的东西。反模式通常是解决特定类型问题的通用方法,但也是解决问题的错误方法。这并不意味着设计模式总是正确的。这意味着我们的目标是编写易于调试、易于更新和易于扩展的干净代码。反模式会导致与这些既定目标完全相反的代码。它们会产生糟糕的代码。

伸缩构造函数反模式的问题是,它会产生几个构造函数,每个构造函数都有特定数量的参数,然后所有这些构造函数都委托给一个默认的构造函数(如果该类编写正确的话)。

构建器模式不使用大量的构造函数;它使用一个生成器对象。这个 builder 对象逐步接收每个初始化参数,然后将生成的构造对象作为单个结果返回。在 webform 示例中,我们希望添加不同的字段,并获得生成的 webform 作为结果。构建器模式的另一个好处是,它将对象的构造与对象的表示方式分开,因此我们可以改变对象的表示方式,而不改变构造它的过程。

在构建器模式中,两个主要参与者是BuilderDirector

Builder是一个抽象类,它知道如何构建最终对象的所有组件。在我们的例子中,Builder类将知道如何构建每个字段类型。它可以将各个部分组装成一个完整的表单对象。

Director控制建造的过程。有一个Builder的实例(或多个实例)供Director用来构建 webform。Director的输出是一个完全初始化的对象——在我们的例子中是 webform。Director实现了从它包含的字段建立一个 webform 的指令集。这组指令独立于传递给控制器的各个字段的类型。

Python 中构建器模式的一般实现如下所示:

form_builder.py

from abc import ABCMeta, abstractmethod

class Director(object, metaclass=ABCMeta):

    def __init__(self):
        self._builder = None

    @abstractmethod
    def construct(self):
        pass

    def get_constructed_object(self):
        return self._builder.constructed_object

class Builder(object, metaclass=ABCMeta):

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

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

  def __repr__(self):
    pass

class ConcreteBuilder(Builder):
  pass

class ConcreteDirector(Director):
  pass

你看我们有一个抽象类,Builder,它形成了创建对象(产品)的接口。然后,ConcreteBuilderBuilder提供了一个实现。产生的对象能够构造其他对象。

使用 builder 模式,我们现在可以重做 webform 生成器。

from abc import ABCMeta, abstractmethod

class Director(object, metaclass=ABCMeta):

    def __init__(self):
        self._builder = None

    def set_builder(self, builder):
        self._builder = builder

    @abstractmethod
    def construct(self, field_list):
        pass

    def get_constructed_object(self):
        return self._builder.constructed_object

class AbstractFormBuilder(object, metaclass=ABCMeta):

    def __init__(self):
        self.constructed_object = None

    @abstractmethod
    def add_text_field(self, field_dict):
        pass

    @abstractmethod
    def add_checkbox(self, checkbox_dict):
        pass

    @abstractmethod
    def add_button(self, button_dict):
        pass

class HtmlForm(object):
  def __init__(self):
    self.field_list = []

  def __repr__(self):
    return "<form>{}</form>".format("".join(self.field_list))

class HtmlFormBuilder(AbstractFormBuilder):

  def __init__(self):
    self.constructed_object = HtmlForm()

  def add_text_field(self, field_dict):
    self.constructed_object.field_list.append(
        '{0}:<br><input type="text" name="{1}"><br>'.format(
          field_dict['label'],
          field_dict['field_name']
        )
    )

  def add_checkbox(self, checkbox_dict):
    self.constructed_object.field_list.append(
        '<label><input type="checkbox" id="{0}" value="{1}"> {2}<br>'.format(
          checkbox_dict['field_id'],
          checkbox_dict['value'],
          checkbox_dict['label']
        )
    )

  def add_button(self, button_dict):
    self.constructed_object.field_list.append(
        '<button type="button">{}</button>'.format(
            button_dict['text']
        )
    )

class FormDirector(Director):

  def __init__(self):
      Director.__init__(self)

  def construct(self, field_list):
      for field in field_list:
          if field["field_type"] == "text_field":
              self._builder.add_text_field(field)
          elif field["field_type"] == "checkbox":
              self._builder.add_checkbox(field)
          elif field["field_type"] == "button":
              self._builder.add_button(field)

if __name__ == "__main__":
    director = FormDirector()
    html_form_builder = HtmlFormBuilder()
    director.set_builder(html_form_builder)

    field_list = [
        {
            "field_type": "text_field",
            "label": "Best text you have ever written",
            "field_name": "Field One"
        },
        {
            "field_type": "checkbox",
            "field_id": "check_it",
            "value": "1",
            "label": "Check for on",
        },
        {
            "field_type": "text_field",
            "label": "Another Text field",
            "field_name": "Field One"
        },
        {
            "field_type": "button",
            "text": "DONE"
        }
    ]

    director.construct(field_list)

    with open("form_file.html", 'w') as f:
        f.write(
            "<html><body>{0!r}</body></html>".format(
                director.get_constructed_object()
            )
        )

我们的新脚本只能构建带有文本字段、复选框和基本按钮的表单。作为练习,您可以添加更多字段类型。

其中一个ConcreteBuilder类具有创建您想要的 webform 所需的逻辑。Director调用了create()方法。这样,创建不同种类产品的逻辑就被抽象出来了。注意构建器模式是如何一步一步地构建表单的。这与抽象工厂形成了鲜明的对比,在抽象工厂中,可以使用多态性创建并立即返回一系列产品。

由于构建器模式将复杂对象的构造与其表示分离,因此您现在可以使用相同的构造过程来创建不同表示形式的表单,从本机应用接口代码到简单的文本字段。

这种分离的另一个好处是减少了对象的大小,从而使代码更加简洁。我们对构造过程有更好的控制,模块化的本质允许我们容易地对对象的内部表示进行修改。

最大的缺点是,这种模式要求我们为您想要创建的每种类型的产品创建ConcreteBuilder构建器。

那么,什么时候不应该使用构建器模式呢?当您构造可变对象时,这些对象提供了一种在构造完对象后修改配置值的方法。如果构造函数很少,构造函数很简单,或者任何构造函数参数都没有合理的默认值,并且所有这些参数都必须显式指定,那么也应该避免这种模式。

关于抽象的一个注记

当我们谈论抽象时,最基本的术语是指将名称与事物联系起来的过程。在 C 语言中,机器代码是使用机器指令的标签来抽象的,这些指令是一组十六进制指令,而十六进制指令又是机器中实际二进制数的抽象。使用 Python,我们在对象和函数方面有了更高层次的抽象(尽管 C 也有函数和库)。

练习

  • 实现单选按钮组生成功能,作为表单生成器的一部分。
  • 扩展表单构建器以处理提交后的数据,并将收到的信息保存在字典中。
  • 实现表单生成器的一个版本,该版本使用数据库表模式来构建到数据库表的表单接口。
  • 编写一个管理界面生成器,自动生成表单来处理数据库中的信息。您可以将一个模式作为字符串传递给表单生成器,然后让它返回表单的 HTML 以及一些提交 URL。
  • 扩展表单构建器,这样就有了两个新对象,一个创建表单的 XML 版本,另一个生成包含表单信息的 JSON 对象。

六、适配器模式

幸存下来的既不是最强壮的物种,也不是最聪明的物种。它是最能适应变化的。—查尔斯·达尔文

有时您没有想要连接的接口。这可能有许多原因,但在这一章中,我们将关心我们能做些什么来改变我们被给予的东西,使之更接近我们想要的东西。

假设你想开发自己的营销软件。首先,编写一些发送电子邮件的代码。在更大的系统中,您可以这样调用:

send_email.py

import smtplib
from email.mime.text import MIMEText

def send_email(sender, recipients, subject, message):    
    msg = MIMEText(message)
    msg['Subject'] = subject
    msg['From'] = sender
    msg['To'] = ",".join(recipients)

    mail_sender = smtplib.SMTP('localhost')
    mail_sender.send_message(msg)
    mail_sender.quit()

if __name__ == "__main__":
    response = send_email(
        'me@example.com',
        ["peter@example.com", "paul@example.com", "john@example.com"],
        "This is your message", "Have a good day"
    )

接下来,你实现一个 email sender 类,因为到现在为止,你怀疑如果你要构建一个更大的系统,你将需要以面向对象的方式工作。EmailSender类处理向用户发送消息的细节。用户存储在逗号分隔值(CSV)文件中,您可以根据需要加载该文件。

users.csv

name, surname, email
peter, potter, peter@example.comma
...

下面是您的电子邮件发送类第一次运行时的样子:

mail_sender.py

import csv
import smtplib
from email.mime.text import MIMEText

class Mailer(object):
  def send(sender, recipients, subject, message):    
      msg = MIMEText(message)
      msg['Subject'] = subject
      msg['From'] = sender
      msg['To'] = [recipients]

      s = smtplib.SMTP('localhost')
      s.send_message(recipient)
      s.quit()

if __name__ == "__main__":
  with open('users.csv', 'r') as csv_file:
    reader = csv.DictReader(csv_file)
    users = [x for row in reader]

  mailer = Mailer()
  mailer.send(
  'me@example.com',
  [x["email"] for x in users],
  "This is your message", "Have a good day"
  )

为此,您可能需要在您的操作系统上设置某种电子邮件服务。如果你需要的话,Postfix 是一个不错的免费选择。在我们继续这个例子之前,我想介绍两个你会在本书中多次看到的设计原则。这些原则为编写更好的代码提供了清晰的指导。

不要重复自己(干)

像 DRY 这样的设计原则是你在任何软件项目中需要克服的问题和挑战的森林中的指路明灯。它们是你大脑深处的细小声音,指引你走向一个干净、清晰、可维护的解决方案。你会忍不住跳过它们,但这样做会让你感到不舒服。当你的代码中有没有人喜欢去的黑暗角落时,很可能是因为最初的开发人员没有遵循这些准则。我不会对你撒谎;开始的时候,很难。用正确的方法做事情往往会多花一点时间,但从长远来看,这是值得的。

在“四人帮”的书(关于面向对象设计模式的原始大部头)中,作者提出的解决常见问题的两个主要原则如下:

  • 编程到接口,而不是实现
  • 优先选择对象组合而不是继承

记住第一个原则,我们想改变获取用户数据的方式,这样我们就不再处理在EmailSender类中获取数据的实现。我们希望将这部分代码移到一个单独的类中,这样我们就不再关心如何检索数据了。这将允许我们将文件系统替换为某种数据库,甚至是远程微服务,如果将来需要的话。

关注点分离

对接口编程允许我们在获取用户详细信息的代码和发送消息的代码之间创建更好的分离。你越清晰地将你的系统分成不同的单元,每个单元都有自己的关注点或任务,这些部分彼此越独立,维护和扩展系统就越容易。只要你保持界面不变,你就可以切换到更新更好的做事方式,而不需要对你现有的系统进行全面的改造。在现实世界中,这可能意味着一个被 COBOL 库和磁带卡住的系统和一个与时俱进的系统之间的差别。它还有一个额外的好处,那就是如果你发现你早期做出的一个或多个设计决策是次优的,你可以更容易地进行修正。

这个设计原则被称为关注点分离(SoC ),迟早你会非常感激你遵守了它,或者非常遗憾你没有遵守。你已经被警告了。

让我们按照自己的建议,将用户信息的检索从EmailSender类中分离出来。

user_ fetcher.py

class UserFetcher(object):
  def __init__(source):
    self.source = source

  def fetch_users():
    with open(self.source, 'r') as csv_file:
      reader = csv.DictReader(csv_file)
      users = [x for row in reader]

    return rows

mail_sender.py

import csv
import smtplib
from email.mime.text import MIMEText

class Mailer(object):
  def send(sender, recipients, subject, message):    
      msg = MIMEText(message)
      msg['Subject'] = subject
      msg['From'] = sender
      msg['To'] = [recipients]

      s = smtplib.SMTP('localhost')
      s.send_message(recipient)
      s.quit()

if __name__ == "__main__":
  user_fetcher = UserFetcher('users.csv')
  mailer = Mailer()

  mailer.send(  
  'me@example.com',
  [x["email"] for x in user_fetcher.fetch_users()],
  "This is your message", "Have a good day"
  )

你能看出要做好这件事可能需要一点努力,但是当我们想在不改变实际发送电子邮件的代码的情况下加入 Postgres、Redis 或一些外部 API 时,会容易得多吗?

现在您已经有了这段向用户发送电子邮件的漂亮代码,您可能会收到一个请求,要求您的代码能够向脸书或 Twitter 这样的社交媒体平台发送状态更新。您不想更改您的工作邮件发送代码,并且您在网上找到了一个可以为您处理集成的另一方面的包或程序。您如何创建某种消息传递接口,使您能够向许多不同的平台发送消息,而无需更改发送代码来适应每个目标,或者重新发明轮子来适应您的需求?

我们要解决的更普遍的问题是:我有的界面不是我想要的界面!

对于大多数常见的软件问题,都有一个设计模式。让我们先处理一个示例问题,这样在我们为手头的问题实现它之前,我们可以对解决方案有一点直觉。

让我们从把问题过于简单化开始。我们想将一段代码插入另一段代码,但插头与插座不匹配。当你去另一个国家旅行时,想要给你的笔记本电脑充电,你需要找一个适配器来把你的充电器插头插到墙上的插座上。在我们的日常生活中,我们有用于在两叉插头和三叉插座之间切换的适配器,或者用于使用带有 HDMI 输出的 VGA 屏幕或其他物理设备。这些都是物理解决问题的接口,我有不是我想要的接口!

当你从头开始构建一切的时候,你将不会面临这个问题,因为你要决定你将在你的系统中使用的抽象层次,以及系统的不同部分将如何相互作用。在现实世界中,你很少能从头开始构建一切。你必须使用可用的插件包和工具。你需要以一种你从未考虑过的方式来扩展你一年前写的代码,没有足够的时间回去重做你以前做过的一切。每隔几周或几个月,你会意识到你不再是以前的程序员了——你变得更好了——但你仍然需要支持你以前做过的项目(和错误)。你将需要你没有的接口。你需要适配器。

样本问题

我们说我们想要一个适配器,但实际上我们想要一个我们没有的特定接口。

class WhatIHave(object):
  def provided_function_1(self): pass

  def provided_function_2(self): pass

class WhatIWant(object):
  def required_function(): pass

我想到的第一个解决方案就是简单地改变我们想要的东西,使之适合我们现有的东西。当您只需要与单个对象进行交互时,您能够做到这一点,但是让我们想象我们还想使用第二个服务,现在我们有了第二个接口。我们可以从使用一个if语句将执行指向正确的接口开始:

class Client(object)
  def __init__(some_object):
    self.some_object = some_object

  def do_something():
    if self.some_object.__class__ == WhatIHave:
      self.some_object.provided_function_2()
      self.some_object.provided_function_1()
    else if self.some_object.__class__ == WhatIWant:
      self.some_object.required_function()
    else:
      print("Class of self.some_object not recognized")

在一个只有两个接口的系统中,这是一个不错的解决方案,但是到现在为止,你知道它很少会只存在于两个系统中。同样容易发生的是,程序的不同部分可能会使用相同的服务。突然之间,您需要修改多个文件来适应您引入的每个新界面。在前一章中,您意识到通过到处创建多个、通常是可伸缩的if语句来添加到一个系统中很少是可行的方法。

您希望创建一段代码,它位于您拥有的库或接口之间,并使它看起来像您想要的接口。添加另一个类似的服务只需要向该服务添加另一个接口,这样您的调用对象就与所提供的服务分离了。

类别适配器

解决这个问题的一个方法是创建几个接口,这些接口使用多态来继承预期的和提供的接口。因此,目标接口可以创建为一个纯接口类。

在下面的例子中,我们导入了一个第三方库,其中包含一个名为WhatIHave的类。这个类包含两个方法,provided_function_1provided_function_ 2,我们用它们来构建客户端对象所需的函数。

from third_party import WhatIHave

class WhatIWant:
    def required_function(self): pass

class MyAdapter(WhatIHave, WhatIWant):
  def __init__(self, what_i_have):
    self.what_i_have = what_i_have

  def provided_function_1(self):
    self.what_i_have.provided_function_1

  def provided_function_2(self):
    self.what_i_have.profided_function_2

  def required_function(self):
    self.provided_function_1()
    self.provided_function_2()

class ClientObject():

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

  def do_something():
    self.what_i_want.required_function()

if __name__ == "__main__":
  adaptee = WhatIHave()
  adapter = MyAdapter(adaptee)
  client = ClientObject(adapter)

  client.do_something()

适配器存在于各种复杂程度。在 Python 中,适配器的概念超越了类及其实例。通常,可调用程序是通过 decorators、闭包和 functools 来适应的。

还有另一种方法可以使一个接口适应另一个接口,这种方法的核心甚至更加 Pythonic 化。

对象适配器模式

我们可以利用组合来代替使用继承来包装一个类。然后,适配器可以包含它包装的类,并调用包装对象的实例。这种方法进一步降低了实现的复杂性。

class ObjectAdapter(InterfaceSuperClass):
  def __init__(self, what_i_have):
    self.what_i_have = what_i_have

  def required_function(self):
    return self.what_i_have.provided_function_1()

  def __getattr__(self, attr):
    # Everything else is handled by the wrapped object

    return getattr(self.what_i_have, attr)

我们的适配器现在只从InterfaceSuperClass继承,并在其构造函数中接受一个实例作为参数。然后实例被存储在self.what_i_have中。它实现了一个required_function方法,该方法返回调用其包装的WhatIHave对象的provided_function_1()方法的结果。对该类的所有其他调用都通过___ getattr__()方法传递给它的what_i_have实例。

您仍然只需要实现您正在适应的接口。ObjectAdapter不需要继承其余的WhatIHave接口;相反,它提供了__getattr__()来提供与WhatIHave类相同的接口,除了它自己实现的方法。

__getattr__()方法很像__init__()方法,是 Python 中的一个特殊方法。通常称为魔术方法,这类方法由前导和尾随双下划线表示,有时也称为双下划线或 dunder。当 Python 解释器对对象进行属性查找但找不到它时,调用__getattr__()方法,并将属性查找传递给self.what_i_have对象。

由于 Python 使用了一个叫做 duck typing 的概念,我们可以进一步简化我们的实现。

鸭子打字

鸭子打字说,如果一个动物走路像鸭子,叫起来像鸭子,那它就是鸭子。

用我们能理解的术语来说,在 Python 中,我们只关心一个对象是否提供了我们需要的接口元素。如果是的话,我们可以像使用接口实例一样使用它,而不需要声明一个公共父类。InterfaceSuperClass只是失去了它的用处,可以去掉。

现在,考虑一下ObjectAdapter,因为它充分利用了 duck 类型来摆脱InterfaceSuperClass继承。

from third_party import WhatIHave

class ObjectAdapter(object):
  def __init__(self, what_i_have):
    self.what_i_have = what_i_have

  def required_function(self):
    return self.what_i_have.provided_function_1()

  def __getattr__(self, attr):
    # Everything else is handeled by the wrapped object

    return getattr(self.what_i_have, attr)

代码看起来没有任何不同,除了用默认对象代替InterfaceSuperClass作为ObjectAdapter类的父类。这意味着我们永远不必定义一些超类来使一个接口适应另一个接口。我们遵循了适配器模式的意图和精神,没有不太动态的语言所带来的所有规避。

记住,你不是我们领域经典文本中事物定义方式的奴隶。随着技术的发展和变化,我们这些技术的实现者也必须如此。

我们新版本的适配器模式使用组合而不是继承来交付这种结构设计模式的更 Pythonic 化的版本。因此,我们不需要访问接口类的源代码。这反过来允许我们不违反 Eiffel 编程语言的创造者 Bertrand Meyer 提出的开放/封闭原则,以及契约式设计的思想。

开放/封闭原则规定:软件实体(类、模块、函数等。)应该对扩展开放,但对修改关闭。

您希望在不修改源代码的情况下扩展实体的行为。

当你想学习新的东西时,特别是当它不是琐碎的信息时,你需要在深层次上理解这些信息。加深理解的一个有效策略是将大图解构为小部分,然后将它们简化为可消化的块。然后你用这些简单的组块来发展对问题及其解决方案的直觉。一旦你对自己的能力有了信心,你就加入了更多的复杂性。一旦你对部分有了深刻的理解,你继续把它们串起来,你会发现你对整体有了清晰得多的感觉。每当你想开始探索一个新的想法时,你可以使用我们刚刚遵循的过程。

在现实世界中实现适配器模式

既然您现在已经对适配器模式的清晰的 pythonic 式实现有了感觉,那么让我们将这种直觉应用到我们在本章开始时看到的例子中。我们将创建一个适配器,它将允许我们向命令行写入文本,以便向您展示适配器模式的实际效果。作为练习的一部分,您将被要求为 Python 创建一个社会网络库的适配器,并将其与下面的代码集成。

import csv
import smtplib
from email.mime.text import MIMEText

class Mailer(object):
    def send(sender, recipients, subject, message):    
        msg = MIMEText(message)
        msg['Subject'] = subject
        msg['From'] = sender
        msg['To'] = [recipients]

        s = smtplib.SMTP('localhost')
        s.send_message(recipient)
        s.quit()

class Logger(object):
    def output(message):
        print("[Logger]".format(message))

class LoggerAdapter(object):
    def __init__(self, what_i_have):
        self.what_i_have = what_i_have

  def send(sender, recipients, subject, message):
      log_message = "From: {}\nTo: {}\nSubject: {}\nMessage: {}".format(
          sender,
          recipients,
          subject,
          message
      )
      self.what_i_have.output(log_message)

  def __getattr__(self, attr):
      return getattr(self.what_i_have, attr)

if __name__ == "__main__":
      user_fetcher = UserFetcher('users.csv')
      mailer = Mailer()      mailer.send(  
          'me@example.com',
          [x["email"] for x in user_fetcher.fetch_users()],
          "This is your message", "Have a good day"
      )

你也可以使用你在第一章开发的Logger类。在前面的例子中,Logger类只是一个例子。

对于您想要适应的每个新接口,您需要编写一个新的AdapterObject类型类。其中每一个都在其构造函数中用两个参数构造,其中一个是 adaptee 的实例。每个适配器还需要用所需的参数实现send_message(self, topic, message_body)方法,这样就可以从代码中的任何地方使用所需的接口调用它。

如果我们传递给每个provided_function的参数与传递给required_function的参数保持不变,我们就可以创建一个更通用的适配器,它不再需要知道关于适配器的任何事情;我们只需给它提供一个对象和provided_function,就大功告成了。

这是这个想法的一般实现:

class ObjectAdapter(object):
   def __init__(self, what_i_have, provided_function):
       self.what_i_have = what_i_have
       self.required_function = provided_function

   def __getattr__(self, attr):
       return getattr(self.what_i_have, attr)

以这种方式实现消息发送适配器也是本章末尾的练习之一。到现在为止,您可能已经意识到您必须自己完成越来越多的实现工作。这是故意的。目的是在学习过程中指导你。您应该实现这些代码,以便对它们所涉及的概念有更多的直觉。一旦你有了一个工作版本,摆弄它,破坏它,然后改进它。

临别赠言

您已经看到,当您需要在设计好适配器之后让它们工作时,适配器是非常有用的。这是一种向被提供接口的主体提供不同于所提供接口的接口的方式。

适配器模式具有以下元素:

  • 目标——定义客户端使用的特定于域的接口
  • 客户端-使用符合目标接口的对象
  • adaptee——因为对象不符合目标而必须改变的接口
  • 适配器——将适配器中的内容更改为客户机中的内容的代码

要实现适配器模式,请遵循以下简单过程:

  1. 定义您想要容纳的组件。
  2. 确定客户需要的接口。
  3. 设计和实现适配器,以将客户机所需的接口映射到适配器提供的接口。

客户端从适配器解耦,并耦合到接口。这为您提供了可扩展性和可维护性。

练习

  • 扩展 user-fetcher 类,使其能够从 CSV 文件或 SQLite 数据库中获取数据。
  • 选择任何一个现代的社交网络,它提供一个 API 来与网络进行交互并发布某种消息。现在,为该社交网络安装 Python 库,并为该库创建一个类和一个对象适配器,这样你在本章中开发的代码就可以用来向该社交网络发送消息。一旦完成,你可以祝贺自己建立了许多提供预定社交媒体信息的大企业的基础。
  • 重写上一个练习中的代码,将通用适配器用于您实现的所有接口。

七、装饰器模式

对那些利用时间的人来说,时间停留得够久了。—莱昂纳多·达芬奇

随着您变得越来越有经验,您会发现自己已经超越了可以用最通用的编程结构轻松解决的问题类型。这时,你会意识到你需要一套不同的工具。本章将重点介绍一个这样的工具。

当您试图从您的机器中挤出最后一点性能时,您需要确切地知道一段特定的代码执行需要多长时间。在启动要分析的代码之前,必须节省系统时间,然后执行代码;一旦结束,您必须保存第二个系统时间戳。最后,从第二个时间中减去第一个时间,以获得执行过程中经过的时间。看看这个计算斐波纳契数的例子。

纤维。巴拉圭

import time
n = 77

start_time = time.time()
fibPrev = 1
fib = 1

for num in range(2, n):
    fibPrev, fib = fib, fib + fibPrev

end_time = time.time()

print("[Time elapsed for n = {}] {}".format(n, end_time - start_time))
print("Fibonacci number for n = {}: {}".format(n, fibIter(n)))

每个系统都是不同的,但是您应该会得到如下所示的结果:

[Time elapsed for n = 77] 8.344650268554688e-06
Fibonacci number for n = 77: 5527939700884757

我们现在扩展我们的 Fibonacci 代码,这样我们就可以请求我们想要检索的 Fibonacci 序列中的元素数。我们通过将斐波那契计算封装在一个函数中来实现这一点。

fib_ 函数. py

import time

def fib(n):
    start_time = time.time()
    if n < 2:
        return

    fibPrev = 1
    fib = 1

    for num in range(2, n):
        fibPrev, fib = fib, fib + fibPrev

    end_time = time.time()
    print("[Time elapsed for n = {}] {}".format(n, end_time - start_time))

    return fib

if __name__ == "__main__":
    n = 77
    print("Fibonacci number for n = {}: {}".format(n, fib(n)))

在前面的代码中,分析发生在函数本身之外。当您只想查看单个函数时,这是没问题的,但当您优化程序时,情况通常不是这样。幸运的是,函数是 Python 中的一等公民,因此我们可以将函数作为参数传递给另一个函数。因此,让我们创建一个函数来计时另一个函数的执行。

纤维 _ 功能 _ 轮廓 _ 我. py

import time

def fib(n):
    if n < 2:
        return

    fibPrev = 1
    fib = 1

    for num in range(2, n):
        fibPrev, fib = fib, fib + fibPrev

    return fib

def profile_me(f, n):
    start_time = time.time()
    result = f(n)
    end_time = time.time()
    print("[Time elapsed for n = {}] {}".format(n, end_time - start_time))

    return result

if __name__ == "__main__":
    n = 77
    print("Fibonacci number for n = {}: {}".format(n, profile_me(fib, n)))

每当我们想要获得执行一个函数所花费的时间,我们可以简单地将该函数传递给分析函数,然后像往常一样运行它。这种方法确实有一些限制,因为您必须预先定义要应用于定时函数的参数。Python 又一次帮助了我们,它允许我们返回函数。我们现在返回添加了概要分析的函数,而不是调用函数。

base_profiled_ fib.py

import time

def fib(n):
    if n < 2:
        return

    fibPrev = 1
    fib = 1

    for num in range(2, n):
        fibPrev, fib = fib, fib + fibPrev

    return fib

def profile_me(f, n):
    start_time = time.time()
    result = f(n)
    end_time = time.time()
    print("[Time elapsed for n = {}] {}".format(n, end_time - start_time))

    return result

def get_profiled_function(f):
    return lambda n: profile_me(f, n)

if __name__ == "__main__":
    n = 77
    fib_profiled = get_profiled_function(fib)
    print("Fibonacci number for n = {}: {}".format(n, fib_profiled(n)))

这种方法工作得更好,但是当试图分析几个函数时仍然需要一些努力,因为这会干扰执行函数的代码。一定有更好的方法。理想情况下,我们希望有一种方法将特定的函数标记为要分析的函数,然后不用担心启动函数调用的调用。我们将使用装饰器模式来实现这一点。

装饰器模式

要修饰一个函数,我们需要返回一个可以作为函数使用的对象。decorator 模式的经典实现利用了 Python 实现常规过程函数的方式,这些函数可以被视为具有某种执行方法的类。在 Python 中,一切都是对象;函数是具有特殊__call__()方法的对象。如果我们的 decorator 用一个__call__()方法返回一个对象,结果可以作为一个函数使用。

装饰器模式的经典实现并不使用 Python 中可用的装饰器。同样,我们将选择更 Python 化的 decorator 模式实现,因此我们将利用 Python 中 decorator 的内置语法,使用@符号。

class _ decorated _ profiled _ fib . py

import time

class ProfilingDecorator(object):

    def __init__(self, f):
        print("Profiling decorator initiated")
        self.f = f

    def __call__(self, *args):
        start_time = time.time()
        result = self.f(*args)
        end_time = time.time()
        print("[Time elapsed for n = {}] {}".format(n, end_time - start_time))

        return result

@ProfilingDecorator

def fib(n):
    print("Inside fib")
    if n < 2:
        return

    fibPrev = 1
    fib = 1

    for num in range(2, n):
        fibPrev, fib = fib, fib + fibPrev

    return fib

if __name__ == "__main__":
    n = 77
    print("Fibonacci number for n = {}: {}".format(n, fib(n)))

fib函数中的print语句只是用来显示执行相对于分析装饰的位置。一旦您看到它在运行,就删除它,以免影响计算所用的实际时间。

Profiling decorator initiated
Inside fib
[Time elapsed for n = 77] 1.1682510375976562e-05
Fibonacci number for n = 77: 5527939700884757

在类定义中,我们看到修饰函数在初始化过程中被保存为对象的一个属性。然后,在call函数中,被修饰的函数的实际运行被包装在时间请求中。就在返回之前,配置文件值被打印到控制台。当编译器遇到_@ProfilingDecorator_时,它会启动一个对象,并将被包装的函数作为参数传递给构造函数。返回的对象具有作为方法的__ call__()函数,因此鸭子类型将允许该对象作为函数使用。另外,注意我们如何在__call__()方法的参数中使用*args来传递参数。允许我们处理多个传入的参数。这被称为打包,因为所有传入的参数都被打包到args变量中。然后,当我们调用装饰对象的f属性中的存储函数时,我们再次使用*args。这被称为解包,它将args中集合的所有元素转化为传递给相关函数的独立参数。

装饰器是一个一元函数(接受单个参数的函数),它接受要装饰的函数作为其参数。正如您在前面看到的,它返回一个与原始函数相同的函数,只是增加了一些功能。这意味着所有与被修饰的函数交互的代码都可以保持与函数未被修饰时相同。这允许我们堆叠装饰器。因此,在一个愚蠢的例子中,我们可以分析我们的 Fibonacci 函数,然后将结果字符串输出为 HTML。

stacked_ fib.py

import time

class ProfilingDecorator(object):

    def __init__(self, f):
        print("Profiling decorator initiated")
        self.f = f

    def __call__(self, *args):
        print("ProfilingDecorator called")
        start_time = time.time()
        result = self.f(*args)
        end_time = time.time()
        print("[Time elapsed for n = {}] {}".format(n, end_time - start_time))

        return result

class ToHTMLDecorator(object):

    def __init__(self, f):
        print("HTML wrapper initiated")
        self.f = f

    def __call__(self, *args):
        print("ToHTMLDecorator called")
        return "<html><body>{}</body></html>".format(self.f(*args))

@ToHTMLDecorator
@ProfilingDecorator

def fib(n):
    print("Inside fib")
    if n < 2:
        return

    fibPrev = 1
    fib = 1

    for num in range(2, n):
        fibPrev, fib = fib, fib + fibPrev

    return fib

if __name__ == "__main__":
    n = 77
    print("Fibonacci number for n = {}: {}".format(n, fib(n)))

这将产生以下输出。当装饰一个函数时,要注意你包装的函数的输出类型是什么,以及装饰器的结果的输出类型是什么。通常,您希望保持类型一致,这样使用未修饰函数的函数就不需要更改。在本例中,我们将类型从数字更改为 HTML 字符串,但这不是您通常想要做的事情。

Profiling decorator initiated
HTML wrapper initiated
ToHTMLDecorator called
ProfilingDecorator called
Inside fib
[Time elapsed for n = 77] 1.52587890625e-05
Fibonacci number for n = 77: <html><body>5527939700884757</body></html>

请记住,我们能够使用一个类来修饰一个函数,因为在 Python 中,只要对象有一个__call__()方法,一切都是对象,所以它可以像函数一样使用。现在,让我们来探索如何反向使用 Python 的这个属性。与其用类来修饰函数,不如直接用函数来修饰我们的fib函数。

函数 _ 装饰 _ 纤维. py

import time

def profiling_decorator(f):
    def wrapped_f(n):
        start_time = time.time()
        result = f(n)
        end_time = time.time()
        print("[Time elapsed for n = {}] {}".format(n, end_time - start_time))

        return result

    return wrapped_f

@profiling_decorator

def fib(n):
    print("Inside fib")
    if n < 2:
        return

    fibPrev = 1
    fib = 1

    for num in range(2, n):
        fibPrev, fib = fib, fib + fibPrev

    return fib

if __name__ == "__main__":
    n = 77
    print("Fibonacci number for n = {}: {}".format(n, fib(n)))

准备好接受一点疯狂吧。decorator 函数必须返回一个在被修饰的函数被调用时使用的函数。返回的函数用于包装被修饰的函数。调用函数时会创建wrapped_f()函数,因此它可以访问传递给profiling_decorator()函数的所有参数。这种将一些数据(在本例中,传递给profiling_decorator()函数的函数)附加到代码上的技术在 Python 中被称为闭包。

关闭

让我们看看访问非局部变量fwrapped_f()函数,该变量作为参数传递给profiling_decorator()函数。当wrapped_f()函数被创建时,非局部变量f的值被存储并作为函数的一部分返回,即使最初的变量移出了作用域,并在程序退出profiling_decorator()函数时从名称空间中删除。

简而言之,要有一个闭包,你必须让一个函数返回嵌套在它自己内部的另一个函数,嵌套函数引用封闭函数范围内的一个变量。

保留函数 namedoc 属性

如前所述,理想情况下,您不希望使用 decorator 的任何函数以任何方式被更改,但是我们在上一节中实现包装函数的方式导致函数的__name____doc__属性更改为wrapped_f()函数的属性。查看以下脚本的输出,了解__name__属性是如何变化的。

func_attrs.py

def dummy_decorator(f):
    def wrap_f():
        print("Function to be decorated: ", f.__name__)
        print("Nested wrapping function: ", wrap_f.__name__)
        return f()

           return wrap_f

@dummy_decorator

def do_nothing():
    print("Inside do_nothing")

if __name__ == "__main__":
    print("Wrapped function: ",do_nothing.__name__)

    do_nothing()

检查以下结果;被包装的函数采用了wrap函数的名称。

Wrapped function:  wrap_f
Function to be decorated:  do_nothing
Nested wrapping function:  wrap_f
Inside do_nothing

为了保持被包装的函数的__name____doc__属性,我们必须在离开装饰函数的范围之前将它们设置为等于传入的函数。

def dummy_decorator(f):
    def wrap_f():
        print("Function to be decorated: ", f.__name__)
        print("Nested wrapping function: ", wrap_f.__name__)
        return f()

    wrap_f.__name__ = f.__name__
    wrap_f.__doc__ = wrap_f.__doc__
    return wrap_f

@dummy_decorator

def do_nothing():
    print("Inside do_nothing")

if __name__ == "__main__":
    print("Wrapped function: ",do_nothing.__name__)

    do_nothing()

现在,do_nothing()函数和修饰过的do_nothing()函数之间不再有区别。

Python 标准库包括一个模块,该模块允许保留__name____doc__属性,而无需我们自己进行设置。在本章的上下文中,更好的是模块通过对包装函数应用装饰器来实现这一点。

from functools import wraps

def dummy_decorator(f):

    @wraps(f)
    def wrap_f():
        print("Function to be decorated: ", f.__name__)
        print("Nested wrapping function: ", wrap_f.__name__)
        return f()

    return wrap_f

@dummy_decorator

def do_nothing():
    print("Inside do_nothing")

if __name__ == "__main__":
    print("Wrapped function: ",do_nothing.__name__)

    do_nothing()

如果我们想选择打印时间的单位,比如秒而不是毫秒,会怎么样呢?我们必须找到一种方法将参数传递给装饰器。为此,我们将使用一个装饰工厂。首先,我们创建一个装饰函数。然后,我们将它扩展成一个装饰工厂,修改或替换包装器。然后工厂返回更新后的装饰器。

让我们看看包含了functools包装器的斐波那契代码。

import time
from functools import wraps

def profiling_decorator(f):
    @wraps(f)
    def wrap_f(n):
        start_time = time.time()
        result = f(n)
        end_time = time.time()

        elapsed_time = (end_time - start_time)
        print("[Time elapsed for n = {}] {}".format(n, elapsed_time))

        return result

    return wrap_f

@profiling_decorator

def fib(n):
    print("Inside fib")
    if n < 2:
        return

    fibPrev = 1
    fib = 1

    for num in range(2, n):
        fibPrev, fib = fib, fib + fibPrev

    return fib

if __name__ == "__main__":
    n = 77
    print("Fibonacci number for n = {}: {}".format(n, fib(n)))

现在,让我们扩展代码,这样我们可以传入一个选项,以毫秒或秒为单位显示经过的时间。

import time
from functools import wraps

def profiling_decorator_with_unit(unit):
    def profiling_decorator(f):
        @wraps(f)
        def wrap_f(n):
            start_time = time.time()
            result = f(n)
            end_time = time.time()
            if unit == "seconds":
                elapsed_time = (end_time - start_time) / 1000
            else:
                elapsed_time = (end_time - start_time)

            print("[Time elapsed for n = {}] {}".format(n, elapsed_time))

            return result

        return wrap_f
    return profiling_decorator

@profiling_decorator_with_unit("seconds")

def fib(n):
    print("Inside fib")
    if n < 2:
        return

    fibPrev = 1
    fib = 1

    for num in range(2, n):
        fibPrev, fib = fib, fib + fibPrev

    return fib

if __name__ == "__main__":
    n = 77
    print("Fibonacci number for n = {}: {}".format(n, fib(n)))

和以前一样,当编译器到达\@profiling_decorator_with_unit时,它调用函数,函数返回装饰器,然后装饰器被应用到被装饰的函数。您还应该注意到,我们再次使用闭包概念来处理传递给装饰器的参数。

装饰课

如果我们想分析特定类中的每个方法调用,我们的代码应该是这样的:

class DoMathStuff(object):
    @profiling_decorator
    def fib(self):
        ...
    @profiling_decorator
    def factorial(self):
        ...

对类中的每个方法应用相同的方法会非常好,但是它违反了 DRY 原则(不要重复自己)。如果我们对性能感到满意,并且需要从大量杂乱的类中取出分析代码,会发生什么呢?如果我们在类中添加了方法,却忘了修饰这些新添加的方法,那该怎么办?一定有更好的方法,而且确实有。

我们希望能够做的是修饰类,并让 Python 知道将修饰应用于类中的每个方法。

我们想要的代码应该是这样的:

@profile_all_class_methods

class DoMathStuff(object):

    def fib(self):
        ...
    @profiling_decorator
    def factorial(self):
        ...

本质上,我们想要的是一个从外部看起来与DoMathStuff类一模一样的类,唯一的区别是每个方法调用都应该被分析。

我们在上一节中编写的分析代码应该仍然有用,所以让我们利用它,使包装函数更加通用,以便它可以为传递给它的任何函数工作。

import time

def profiling_wrapper(f):
    @wraps(f)
    def wrap_f(*args, **kwargs):
        start_time = time.time()
        result = f(*args, **kwargs)
        end_time = time.time()
        elapsed_time = end_time - start_time
        print("[Time elapsed for n = {}] {}".format(n, elapsed_time))

        return result

    return wrap_f

我们收到了两个打包的参数:一个常规参数列表和一个映射到关键字的参数列表。在它们之间,这两个参数将捕获可以传递给 Python 函数的任何形式的参数。当我们将它们传递给要包装的函数时,我们也对它们进行解包。

现在,我们想要创建一个类,它将包装一个类,并将装饰函数应用于该类中的每个方法,并返回一个看起来与它接收到的类没有什么不同的类。这是通过__ getattribute__()的魔法方法实现的。Python 使用这个方法来检索对象的方法和属性,通过覆盖这个方法,我们可以根据需要添加装饰器。由于__getattribute__()返回方法和值,我们还需要检查请求的属性是方法。

class_profiler.py

import time

def profiling_wrapper(f):
    @wraps(f)
    def wrap_f(*args, **kwargs):
        start_time = time.time()
        result = f(*args, **kwargs)
        end_time = time.time()
        elapsed_time = end_time - start_time
        print("[Time elapsed for n = {}] {}".format(n, elapsed_time))

        return result

    return wrap_f

def profile_all_class_methods(Cls):
    class ProfiledClass(object):
        def __init__(self, *args, **kwargs):
            self.inst = Cls(*args, **kwargs)

        def __getattribute__(self, s):
            try:
                x = super(ProfiledClass, self).__getattribute__(s)
            except AttributeError:
                pass

            else:

                x = self.inst.__getattribute__(s)
                if type(x) == type(self.__init__):
                    return profiling_wrapper(x)
                else:
                    return x

    return ProfiledClass

@profile_all_class_methods

class DoMathStuff(object):

    def fib(self):
        ...
    @profiling_decorator
    def factorial(self):
        ...

你有它!现在可以修饰类和函数了。如果您下载并阅读 Flask 源代码(从官方网站: http://flask.pocoo.org/ )会对您有所帮助,特别注意他们是如何利用 decorators 来处理路由的。另一个你可能会感兴趣的包是 Django Rest 框架(来自官方网站: http://www.django-rest-framework.org/ ),它使用 decorators 来改变返回值。

临别赠言

装饰器模式与适配器和外观模式的不同之处在于,接口不会改变,但会以某种方式添加新的功能。所以,如果你有一个特定的功能,你想把它附加到一些函数上,在不改变它们的接口的情况下改变这些函数,装饰器可能是个不错的选择。

练习

  • 扩展 Fibonacci 函数包装器,使其也返回以分钟和小时为单位的时间,打印为“hrs:minutes:seconds .毫秒”。
  • 列出你能想到的可以实现 decorators 的十种常见情况。
  • 阅读 Flask 源代码,写下你遇到的每一个有趣的 decorators 用法。

八、外观模式

歌剧魅影就在那里,在我的脑海里。—克里斯汀

在这一章中,我们将会看到另一种向系统用户隐藏系统复杂性的方法。对外界来说,所有移动的部分应该看起来像一个单一的实体。

所有系统都比乍看起来更复杂。以销售点(POS)系统为例。大多数人只是作为收银台另一边的客户与这样的系统进行交互。在一个简单的例子中,发生的每一笔交易都需要一些事情发生。需要记录销售情况,需要调整交易中每件商品的库存水平,甚至可能需要为老顾客申请某种忠诚度积分。这些系统中的大多数还允许考虑交易中的项目的特价,如买两个并免费获得最便宜的一个。

销售点示例

在这个交易过程中涉及到许多东西,比如忠诚度系统、股票系统、特价或促销系统、任何支付系统,以及使交易过程正常进行所需的任何东西。对于这些交互中的每一个,我们将有不同的类来提供所讨论的功能。

不难想象流程事务的实现会是这样的:

import datetime
import random

class Invoice(object):
  def __init__(self, customer):
    self.timestamp = datetime.datetime.now()
    self.number = self.generate_number()
    self.lines = []
    self.total = 0
    self.tax = 0
    self.customer = customer

  def save(self):
    # Save the invoice to some sort of persistent storage

    pass

  def send_to_printer(self):
    # Send invoice representation to external printer

    pass

  def add_line(self, invoice_line):
    self.lines.append(invoice_line)
    self.calculate()

  def remove_line(self, line_item):
    try:
      self.lines.remove(line_item)
    except ValueError as e:
      print("could not remove {} because there is no such item in the invoice".format(line_item))

  def calculate(self):
    self.total = sum(x.total * x.amount for x in self.lines)
    self.tax = sum(x.total * x.tax_rate for x in self.lines)

  def generate_number(self):
    rand = random.randint(1, 1000)
    return "{}{}".fomat(self.timestamp, rand)

class InvoiceLine(object):
  def __init__(self, line_item):
    # turn line item into an invoice line containing the current price etc

    pass

  def save(self):
    # save invoice line to persistent storage

    pass

class Receipt(object):
  def __init__(self, invoice, payment_type):
    self.invoice = invoice
    self.customer = invoice.customer
    self.payment_type = payment_type
    pass

  def save(self):
    # Save receipt to persistent storage

    pass

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

  @classmethod
  def fetch(cls, item_barcode):
    # fetch item from persistent storage

    pass

  def save(self):
    # Save item to persistent storage

    pass

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

  @classmethod
  def fetch(cls, customer_code):
    # fetch customer from persistent storage

    pass

  def save(self):
    # save customer to persistent storage

    pass

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

  @classmethod
  def fetch(cls, customer):
    # fetch loyaltyAccount from persistent storage

    pass

  def calculate(self, invoice):
    # Calculate the loyalty points received for this purchase

    pass

  def save(self):
    # save loyalty account back to persistent storage

    pass

如果您想要处理一笔销售,您必须与所有这些类进行交互。

def complex_sales_processor(customer_code, item_dict_list, payment_type):
  customer = Customer.fetch_customer(customer_code)
  invoice = Invoice()

  for item_dict in item_dict_list:
    item = Item.fetch(item_dict["barcode"])
    item.amount_in_stock - item_dict["amount_purchased"]
    item.save()
    invoice_line = InvoiceLine(item)
    invoice.add_line(invoice_line)

  invoice.calculate()
  invoice.save()

  loyalt_account = LoyaltyAccount.fetch(customer)
  loyalty_account.calculate(invoice)
  loyalty_account.save()

  receipt = Receipt(invoice, payment_type)
  receipt.save()

如您所见,无论哪个系统负责处理销售,都需要与特定于销售点子系统的大量类进行交互。

系统进化

系统会进化——这是生活的现实——随着系统的成长,它们会变得非常复杂。为了保持整个系统的简单,我们希望对客户端隐藏所有的功能。我知道你一看到前面的片段就闻到了。一切都是紧密耦合的,我们有大量的类要与之交互。当我们想要用一些新的技术或功能来扩展我们的 POS 时,我们将需要深入系统的内部。我们可能会忘记更新一些隐藏的小代码,这样我们的整个系统就会崩溃。我们希望在不损失任何系统功能的情况下简化该系统。

理想情况下,我们希望我们的事务处理器看起来像这样:

def nice_sales_processor(customer_code, item_dict_list, payment_type):
  invoice = SomeInvoiceClass(customer_code)

  for item_dict in item_dict_list:
    invoice.add_item(item_dict["barcode"], item_dict_list["amount_purchased"])

  invoice.finalize()
  invoice.generate_receipt(payment_type)

这使得代码更加清晰易读。面向对象编程中一个简单的经验法则是,每当你遇到一个丑陋或混乱的系统,把它藏在一个对象中。

您可以想象,复杂或丑陋系统的问题是程序员生活中经常遇到的问题,因此您可以肯定,对于如何最好地解决这个问题,有几种观点。一个这样的解决方案是使用一种叫做外观模式的设计模式。这种模式专门用于创建子系统或子系统集合的复杂性限制接口。请看下面这段代码,以便更好地理解这种通过 facade 模式的抽象应该如何发生。

simple_ facade.py

class Invoice:
  def __init__(self, customer): pass

class Customer:
  # Altered customer class to try to fetch a customer on init or creates a new one

  def __init__(self, customer_id): pass

class Item:
  # Altered item class to try to fetch an item on init or creates a new one

  def __init__(self, item_barcode): pass

class Facade:
  @staticmethod
  def make_invoice(customer): return Invoice(customer)

  @staticmethod
  def make_customer(customer_id): return Customer(customer_id)

  @staticmethod
  def make_item(item_barcode): return Item(item_barcode)

  # ...

在继续阅读之前,请尝试自己实现 POS 系统的外观模式。

是什么让门面模式与众不同

facade 模式的一个关键区别是,facade 不仅用于封装单个对象,还用于提供一个包装器,该包装器向一组复杂的子系统提供一个简化的接口,该接口易于使用,没有不必要的功能或复杂性。只要有可能,您希望限制向系统外的世界暴露的知识量和/或复杂性。您越是允许访问一个系统并与之交互,这个系统就变得越紧密。允许更深入地访问您的系统的最终结果是一个可以像黑匣子一样使用的系统,具有一组清晰的预期输入和定义良好的输出。facade 应该决定使用哪个内部类和表示。也是在 facade 中,当时机成熟时,您将对分配给外部或更新的内部系统的功能进行更改。

facade 经常在软件库中使用,因为 facade 提供的方便的方法使库更容易理解和使用。因为与库的交互是通过外观进行的,所以使用外观也减少了库外部对其内部工作的依赖性。对于任何其他系统,这允许在开发库时有更大的灵活性。

并非所有的 API 都是生来平等的,您将不得不处理设计糟糕的 API 集合。然而,使用 facade 模式,您可以将这些 API 包装在一个设计良好的 API 中,这是一种乐趣。

你可以养成的最有用的习惯之一就是为自己的问题创造解决方案。如果你有一个不太理想的系统,为它建立一个门面,让它在你的环境中工作。如果您的 IDE 缺少您喜欢的工具,编写您自己的扩展来提供这个功能。不要等待其他人来提供解决方案。每天,当你采取行动来改变你的世界以适应你的需求时,你会感到更有力量。

让我们将 POS 后端实现为这样一个门面。因为我们不想构建整个系统,所以定义的几个函数将是存根,如果您愿意,可以对其进行扩展。

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

  @staticmethod
  def make_invoice(customer_id):
    return Invoice(Customer.fetch(customer_id))

  @staticmethod
  def make_customer():
    return Customer()

  @staticmethod
  def make_item(item_barcode):
    return Item(item_barcode)

  @staticmethod
  def make_invoice_line(item):
    return InvoiceLine(item)

  @staticmethod
  def make_receipt(invoice, payment_type):
    return Receipt(invoice, payment_type)

  @staticmethod
  def make_loyalty_account(customer):
    return LoyaltyAccount(customer)

  @staticmethod
  def fetch_invoice(invoice_id):
    return Invoice(customer)

  @staticmethod
  def fetch_customer(customer_id):
    return Customer(customer_id)

  @staticmethod
  def fetch_item(item_barcode):
    return Item(item_barcode)

  @staticmethod
  def fetch_invoice_line(line_item_id):
    return InvoiceLine(item)

  @staticmethod
  def fetch_receipts(invoice_id):
    return Receipt(invoice, payment_type)

  @staticmethod
  def fetch_loyalty_account(customer_id):
    return LoyaltyAccount(customer)  

  @staticmethod
  def add_item(invoice, item_barcode, amount_purchased):
    item = Item.fetch(item_barcode)
    item.amount_in_stock - amount_purchased
    item.save()
    invoice_line = InvoiceLine.make(item)
    invoice.add_line(invoice_line)   

  @staticmethod
  def finalize(invoice):
    invoice.calculate()
    invoice.save()

    loyalt_account = LoyaltyAccount.fetch(invoice.customer)
    loyalty_account.calculate(invoice)
    loyalty_account.save()

  @staticmethod
  def generate_receipt(invoice, payment_type):
    receipt = Receipt(invoice, payment_type)
    receipt.save()

使用这个新的Sales类,它作为系统其余部分的门面,我们前面看到的理想函数现在看起来像这样:

def nice_sales_processor(customer_id, item_dict_list, payment_type):
  invoice = Sale.make_invoice(customer_id)

  for item_dict in item_dict_list:
    Sale.add_item(invoice, item_dict["barcode"], item_dict_list["amount_purchased"])

  Sale.finalize(invoice)
  Sale.generate_receipt(invoice, payment_type)

如你所见,没什么变化。

Sales的代码导致了更复杂的业务系统的单一入口点。我们现在有一组有限的函数可以通过 facade 调用,它允许我们与需要访问的系统部分进行交互,而不会让我们在系统的所有内部事务上负担过重。我们还有一个单独的类可以交互,所以我们不再需要在整个类图中寻找正确的地方来连接系统。

在子系统中,第三方股票管理系统或忠诚度程序可以取代内部系统,而无需使用 facade 类的客户修改一行代码。因此,我们的代码更加松散耦合,也更容易扩展。POS 客户不关心我们是否决定将库存和会计工作交给第三方提供商,或者构建和运行一个内部系统。简化的界面仍然幸福地不知道引擎盖下的复杂性,而不降低子系统固有的功率。

临别赠言

门面格局没什么。一开始,你会对一个子系统或一组子系统没有按照你想要的方式工作感到沮丧,或者你发现自己不得不在许多课程中查找交互。到目前为止,您已经知道有些事情不太对劲,所以您决定清理这些乱七八糟的东西,或者至少将它们隐藏在一个单一的、优雅的界面后面。您设计您的包装器类来将丑陋的子系统变成一个黑盒,为您处理交互和复杂性。您的代码的其余部分,以及其他潜在的客户端代码,只处理这个外观。因为你的努力,这个世界现在变得更好了一点点——干得好。

练习

  • 当外观不需要跟踪状态时,它们通常被实现为单件。在最后一段代码中修改 facade 实现,使其使用第二章中的单例模式。
  • 扩展本章概述的销售点系统,直到你有一个可以在现实世界中使用的系统。
  • 选择你最喜欢的社交媒体平台,创建你自己的门面,让你与他们的 API 互动。

九、代理模式

先生,你对我们咬拇指吗?—亚伯兰、罗密欧与朱丽叶

随着程序的成长,你经常会发现有一些你经常调用的函数。当这些计算繁重或缓慢时,您的整个程序都会受到影响。

请考虑以下计算数字 n 的斐波那契数列的示例:

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

  return fib(n - 2) + fib(n - 1)

这个相当简单的递归函数有一个严重的缺陷,特别是当 n 变得非常大的时候。你能找出问题可能是什么吗?

如果你认为当你想计算一个大于 2 的 n 的斐波那契数时,某个f(x)的值必须被计算多次,那你就对了。

有很多方法可以解决这个问题,但是我想用一个非常特别的方法,叫做记忆化。

记忆化

每当我们有一个函数被多次调用,其值被重复时,存储计算的响应将是有用的,这样我们就不必再次经历计算值的过程;我们宁愿只获取已经计算过的值并返回。这种保存函数调用结果以备后用的行为叫做记忆化。

现在,让我们看看记忆化对我们的简单例子会有什么影响。

import time

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

    return fib(n - 2) + fib(n - 1)

if __name__ == "__main__":
    start_time = time.time()
    fib_sequence = [fib(x) for x in range(1,80)]
    end_time = time.time()

    print(
        "Calculating the list of {} Fibonacci numbers took {} seconds".format(
            len(fib_sequence),
            end_time - start_time
        )
    )

让这个运行一段时间;查看计算从 0 到 40 的斐波纳契数所花费的时间。

在我的电脑上,我得到了以下结果:

Calculating the list of 80 Fibonacci numbers took 64.29540348052979 seconds

一些新系统嘲笑仅仅 80 个斐波那契数。如果你在那种情况下,你可以试着增加数量级——80,800,8000——直到你偶然发现一个显示负载的数字,而不用花很长时间来完成。在本章的其余部分使用这个数字。

如果我们缓存每次调用的结果,会对计算产生什么影响?

让我们实现和以前一样的函数,但是这次我们将利用一个额外的字典来跟踪我们以前已经计算过的请求。

import time

def fib_cached1(n, cache):
    if n < 2:
        return 1

    if n in cache:
        return cache[n]

    cache[n] = fib_cached1(n - 2, cache) + fib_cached1(n - 1, cache)
    return cache[n]

if __name__ == "__main__":
    cache = {}
    start_time = time.time()
    fib_sequence = [fib_cached1(x, cache) for x in range(0, 80)]
    end_time = time.time()

    print(
        "Calculating the list of {} Fibonacci numbers took {} seconds".format(
            len(fib_sequence),
            end_time - start_time
        )
    )

运行这段代码。在我的机器上,同样的一系列计算现在给出了以下输出:

Calculating the list of 80 Fibonacci numbers took 4.7206878662109375e-05 seconds

这是一个非常好的结果,以至于我们想要创建一个calculator对象来进行一些数学级数计算,包括计算斐波那契数列。看这里:

class Calculator(object):

    def fib_cached(self, n, cache):
        if n < 2:
            return 1

        try:
            result = cache[n]
        except:
            cache[n] = fib_cached(n - 2, cache) + fib_cached(n - 1, cache)
            result = cache[n]

        return result

你知道你的对象的大多数用户不知道他们应该如何处理cache变量,或者它意味着什么。在calculator方法的方法定义中包含这个变量可能会导致混乱。相反,您想要的是一个方法定义,它看起来像我们看到的第一段代码,但是具有缓存的性能优势。

在理想的情况下,我们应该有一个类作为calculator类的接口。客户端不应该知道这个类,因为客户端只向原始类的接口编码,代理提供与原始类相同的功能和结果。

代理模式

代理提供与原始对象相同的接口,但它控制对原始对象的访问。作为该控件的一部分,它可以在访问原始对象之前和之后执行其他任务。当我们想实现像记忆化这样的东西,而又不想让客户端承担任何理解缓存的责任时,这一点尤其有用。通过屏蔽客户端对fib方法的调用,代理允许我们返回计算结果。

Duck typing 允许我们通过复制对象接口,然后使用代理类而不是原始类来创建这样的代理。

import time

class RawCalculator(object):

    def fib(self, n):
        if n < 2:
            return 1

        return self.fib(n - 2) + self.fib(n - 1)

def memoize(fn):
    __cache = {}

    def memoized(*args):
        key = (fn.__name__, args)
        if key in __cache:
            return __cache[key]

        __cache[key] = fn(*args)
        return __cache[key]

    return memoized

class CalculatorProxy(object):

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

        fib = getattr(self.target, 'fib')
        setattr(self.target, 'fib', memoize(fib))

    def __getattr__(self, name):
        return getattr(self.target, name)

if __name__ == "__main__":
    calculator = CalculatorProxy(RawCalculator())

    start_time = time.time()
    fib_sequence = [calculator.fib(x) for x in range(0, 80)]
    end_time = time.time()

    print(
        "Calculating the list of {} Fibonacci numbers took {} seconds".format(
            len(fib_sequence),
            end_time - start_time
        )
    )

我必须承认,这不是一段无足轻重的代码,但是让我们一步一步来看看发生了什么。

首先,我们有RawCalculator类,这是我们想象的计算对象。目前,它只包含 Fibonacci 计算,但您可以想象它包含许多其他递归定义的系列和序列。和以前一样,这个方法使用递归调用。

接下来,我们定义一个封装函数调用的闭包,但是让我们把它留到以后。

最后,我们有一个CalculatorProxy类,它在初始化时将目标对象作为参数,在代理对象上设置一个属性,然后用fib方法的记忆版本覆盖目标对象的fib方法。每当目标对象调用它的fib()方法时,内存化的版本就会被调用。

现在,memoize函数将一个函数作为参数。接下来,它初始化一个空字典。然后我们定义一个函数,它接受一个参数列表,获取传递给它的函数名,并创建一个包含函数名和接收到的参数的元组。然后这个元组形成了__cache字典的键。该值表示函数调用返回的值。

memoized 函数首先检查该键是否已经在缓存字典中。如果是,则不需要重新计算该值,并返回该值。如果找不到键,则调用原始函数,并在返回之前将值添加到字典中。

memoize函数将一个常规的旧函数作为参数,然后记录调用接收到的函数的结果。如果需要,它调用函数并接收新计算的值,然后返回一个新函数,如果将来需要该值,则保存结果。

最终结果如下所示:

Calculating the list of 80 Fibonacci numbers took 8.20159912109375e-05 seconds

我非常喜欢这个事实,即memoize函数可以与传递给它的任何函数一起使用。拥有一段这样的通用代码是有帮助的,因为它允许您在不修改代码的情况下记忆许多不同的功能。这是面向对象编程的主要目标之一。随着经验的积累,您还应该建立一个有趣且有用的代码库,以便重用。理想情况下,您希望将这些打包到您自己的库中,这样您就可以在更短的时间内做更多的事情。这也有助于你不断进步。

现在您已经理解了代理模式,让我们来看看可供我们使用的不同类型的代理。我们已经详细了解了缓存代理,所以剩下的内容如下:

  • 远程代理
  • 虚拟代理
  • 保护代理

远程代理

当我们想要抽象一个对象的位置时,我们可以使用远程代理。远程对象对客户端来说似乎是本地资源。当您调用代理上的方法时,它会将调用转移到远程对象。一旦返回结果,代理就使这个结果对本地对象可用。

虚拟代理

有时创建对象的成本可能很高,并且可能直到程序执行的后期才需要它们。当延迟对象创建有帮助时,可以使用代理。只有在需要时,才能创建目标对象。

保护代理

许多程序有不同的用户角色和不同的访问级别。通过在目标对象和客户机对象之间放置代理,可以限制对目标对象上的信息和方法的访问。

临别赠言

您看到了代理可以采取多种形式,从我们在本章中讨论的缓存代理到网络连接。每当我们希望以一种对客户端透明的方式控制对象或资源的访问时,就应该考虑代理模式。在本章中,您看到了代理模式如何包装其他对象,并以这些函数的用户不可见的方式改变它们的执行方式,因为它们仍然将相同的参数作为输入,并将相同的结果作为输出返回。

代理模式通常有三个部分:

  • 需要访问某个对象的客户端
  • 客户端想要访问的对象
  • 控制对对象的访问的代理

客户端实例化代理并调用代理,就好像它是对象本身一样。

因为 Python 是动态类型的,所以我们不关心定义一些公共接口或抽象类,因为我们只需要提供与目标对象相同的属性,以便客户端代码像对待目标对象一样对待代理。

用我们已经熟悉的术语来思考代理是有帮助的,比如 web 代理。一旦连接到代理,你,客户端,不知道代理;您可以像没有代理一样访问互联网和其他网络资源。这是代理模式和适配器模式之间的关键区别。使用代理模式,您希望界面保持不变,一些操作在后台发生。相反,适配器模式的目标是改变接口。

练习

  • RawCalculator类添加更多的函数,这样它就可以创建其他序列。
  • 扩展CalculatorProxy类来处理您添加的新系列生成函数。他们也使用开箱即用的memoize功能吗?
  • 修改__init__()函数来迭代目标对象的属性,并自动记忆可调用的属性。
  • 计算每个RawCalculator方法在一个程序中被调用的次数;使用它来计算对fib()方法进行了多少次调用。
  • 查看 Python 标准库,了解记忆函数的其他方法。

十、责任链模式

那不是我。—沙吉,《不是我》

网络应用是复杂的野兽。他们需要解释传入的请求,并决定应该如何处理它们。当您考虑创建一个框架来帮助您更快地构建 web 应用时,您很有可能会淹没在这样一个项目所涉及的复杂性和选项中。Python 是许多 web 框架的家园,因为这种语言在云应用的开发人员中很受欢迎。在这一章中,我们将探索 web 框架的一个组件,叫做中间件层。

中间件位于发出应用请求的客户端和实际应用之间。来自客户端的请求和来自主要应用功能的响应都需要通过这一层。在中间件中,您可以做一些事情,比如记录传入的请求以进行故障排除。您还可以添加一些代码来捕获和保存应用返回给用户的响应。通常,您会找到代码来检查用户的访问,以及是否应该允许他们执行他们在系统上发出的请求。您可能希望将传入的请求体转换成易于处理的数据格式,或者对所有输入值进行转义,以防止黑客将代码注入应用。您也可以将应用的路由机制视为中间件的一部分。路由机制确定应该使用什么代码来响应指向特定端点的请求。

大多数 web 框架创建一个在整个系统中使用的请求对象。一个简单的Request对象可能如下所示:

class Request(object):
  def __init__(self, headers, url, body, GET, POST):
    self.headers
    self.url
    self.body
    self.GET
    self.POST

目前,您还没有与请求相关联的额外功能,但是这个有限的对象应该是一个良好的开端,让您对中间件在处理客户端对 web 应用的请求时的使用方式有所了解。

Web 框架需要向客户端返回某种响应。让我们定义一个基本的Response对象:

class Response(object):
  def __init__(self, headers, status_code, body):
    self.headers
    self.status_code
    self.body

如果你看一下其中一个比较流行的框架的文档,比如 Django 或者 Flask,你会发现这些对象比刚刚定义的简单对象要复杂得多,但是这些都符合我们的目的。

我们希望我们的中间件做的事情之一是检查Request对象头中的令牌,然后将User对象添加到该对象中。这将允许主应用访问当前登录的用户,根据需要改变界面和显示的信息。

让我们编写一个基本函数,它将获取用户令牌并从数据库中加载一个User对象。然后,该函数将返回一个可以附加到请求对象的User对象。

import User

def get_user_object(username, password):
  return User.find_by_token(username, password)

您可以轻松地创建一个存储在数据库中的User类。作为一个练习,查看 SQLAlchemy 项目( https://www.sqlalchemy.org/ )并创建一个 SQLite 数据库的接口,在该数据库中存储基本的用户数据。确保User类有一个基于user token字段查找用户的class函数,这是唯一的。

设置 WSGI 服务器

让我们把这个变得更实际一点。

因为您的系统上已经运行了pip,所以在您的虚拟环境中安装 uWSGI:

pip install uwsgi

Windows 用户可能会收到一条错误消息,AttributeError:对于本章,模块“os”没有属性“uname”,在第十二章中,您需要安装 Cygwin。确保在 Cygwin 安装过程中从选项中选择 python3 解释器、pip3、gcc-core、python3-devel(检查您是否选择了 Windows 版本— 32 位/64 位)、libcrypt-devel 和 wget。可以在 http://cygwin.com/install.html 获得。Cygwin 是一个类似 Linux 的 Windows 终端。

您可以使用默认的 Linux 命令进行导航,比如pip3 install uwsgi命令。您也可以按照第一章中的 virtualenv 设置说明,在 Cygwin 中设置一个虚拟环境(activate命令位于YOURENVNAME/Scripts)。

如果这一切看起来太费力,你最好安装 virtualbox 并在机器上运行 Ubuntu。然后,您可以完全遵循 Linux 的道路,而无需对您的日常环境进行任何更改。

让我们测试一下服务器是否正常工作,是否可以在您的机器上提供网站服务。以下脚本在正文中返回一个非常简单的 status 200 success 和 Hello World 消息(来自位于 http://uwsgi-docs.readthedocs.io/en/latest/WSGIquickstart.html 的 uWSGI 文档)。

hello_world_server.py

def application(env, start_response):
    start_response('200 OK', [('Content-Type','text/html')])
    return [b"Hello World"]

在命令行中,在虚拟环境处于活动状态的情况下,启动服务器,将hello_world_server.py设置为处理请求的应用。

uwsgi --http :9090 --wsgi-file hello_world_server.py

如果您将浏览器指向http://localhost:9090,您应该会在窗口中看到消息Hello World

恭喜你!你有自己的网络服务器在运行;你甚至可以把它部署在一个服务器上,指向一个 URL,让世界向它发送请求。我们接下来要做的是,通过从头开始构建中间件,看看中间件如何适应从请求到响应的流程。

由于服务器已经启动并运行,让我们添加一个函数来检查用户令牌是否设置在头中。我们将遵循你可能在网上找到的许多更大的 API 所使用的惯例。

认证标题

其思想是客户端包含一个Authorization头,头的值设置为单词Basic,后跟一个空格,再后跟一个 base64 编码的字符串。被编码的字符串具有下面的形式,USERNAME:PASSWORD,它将用于识别和验证系统上的用户。

在我们继续构建功能之前,重要的是要注意,通过在调用应用时包含Authentication头,并让应用检查Authorization头,应用就不再需要跟踪哪个用户正在进行调用,而是在请求发生时将用户附加到请求上。

测试 URL 端点最简单的方法是在您的系统上安装一个类似 postman 的工具。它允许你从应用请求网址并解释结果。免费的邮递员 app 可以在这里下载: https://www.getpostman.com/

一旦邮差安装结束,打开应用。

首先,在 postman 地址栏中输入您在浏览器中输入的 URL,然后单击 Send 或按 enter。您应该会在屏幕底部的框中看到Hello World。这告诉你,postman 在工作,你的 app 还在运行。

在地址栏的正下方,你会看到一个带有几个标签的框。其中一个选项卡是授权。单击它,并从下拉框中选择基本身份验证。现在,用户名和密码字段被添加到界面中。Postman 允许您在这些框中输入用户名和密码,然后它会为您编码字符串。

要查看从 uWSGI 接收到的Request对象是什么样子,需要修改用于向浏览器返回Hello World消息的脚本。

hello_world_request_display.py

import pprint

pp = pprint.PrettyPrinter(indent=4)

def application(env, start_response):
    pp.pprint(env)

    start_response('200 OK', [('Content-Type','text/html')])
    return [b"Hello World "]

Python 标准库中包含的pprint模块打印字典和列表等数据类型,是一种易于阅读的格式。当您调试复杂的数据类型时,例如从 web 服务器收到的请求,这是一个很有价值的工具。

当您重启 uWSGI 服务器并用 postman 发送包含Authorization头的请求时,您将看到控制台中打印出一个字典。字典中的一个键是HTTP_AUTHORIZATION,它是包含单词Basic的字段,后跟一个空格,再跟一个 base64 编码的字符串。

要从数据库中检索User对象,需要对这个字符串进行解码,然后分别拆分成用户名和密码。我们可以通过更新服务器应用来完成所有这些工作。我们将从获取HTTP_AUTHORIZATION头的值开始。然后,我们将在一个空格上分割这个值,得到单词Basic和 base64 编码的字符串。然后我们将继续对字符串进行解码,并在:上进行分割,这将产生一个数组,用户名在索引 0 处,密码在索引 1 处。然后,我们将把它发送给一个简单的函数,该函数返回一个User对象,为了这个例子,我们只是内联地构造了这个对象,但是欢迎您插入从本地 SQLite 数据库获取用户的函数。

user_aware_server.py

import base64

class User(object):
    def __init__(self, username, password, name, email):
        self.username = username
        self.password = password
        self.name = name
        self.email = email

    @classmethod
    def get_verified_user(cls, username, password):
        return User(
            username,
            password,
            username,
            "{}@demo.com".format(username)
        )

def application(env, start_response):
    authorization_header = env["HTTP_AUTHORIZATION"]
    header_array = authorization_header.split()
    encoded_string = header_array[1]
    decoded_string = base64.b64decode(encoded_string).decode('utf-8')
    username, password = decoded_string.split(":")

    user = User.get_verified_user(username, password)

    start_response('200 OK', [('Content-Type', 'text/html')])
    response = "Hello {}!".format(user.name)
    return [response.encode('utf-8')]

在我们的例子中,我们根据请求创建了一个用户。这显然是不正确的,但是我们将它分离到自己的类中的事实允许我们获取代码并更改get_verified_user方法以基于用户名获取用户。然后,我们验证该用户的密码是否正确。然后,您可能希望添加一些代码来处理数据库中不存在用户的情况。

此时,我们已经看到了许多只与在主服务器应用中检查用户凭证的过程相关的代码。这感觉不对。让我们通过添加最基本的路由代码来强调它是多么的错误。我们的路由器将寻找一个报头,它告诉我们客户端请求的是哪个端点。基于这些信息,我们将为所有情况打印一条 hello 消息,除非路径包含子串goodbye,在这种情况下,服务器将返回一条 goodbye 消息。

当我们打印出请求内容时,我们看到路径信息被捕获在PATH_INFO头中。标题以“/”开头,后面是路径的其余部分。

可以在“/”上拆分路径,然后检查路径是否包含单词goodbye

import base64

from pprint import PrettyPrinter
pp = PrettyPrinter(indent=4)

class User(object):
    def __init__(self, username, password, name, email):
        self.username = username
        self.password = password
        self.name = name
        self.email = email

    @classmethod
    def get_verified_user(cls, username, password):
        return User(
            username,
            password,
            username,
            "{}@demo.com".format(username)
        )

def application(env, start_response):
    authorization_header = env["HTTP_AUTHORIZATION"]
    header_array = authorization_header.split()
    encoded_string = header_array[1]
    decoded_string = base64.b64decode(encoded_string).decode('utf-8')
    username, password = decoded_string.split(":")

    user = User.get_verified_user(username, password)

    start_response('200 OK', [('Content-Type', 'text/html')])

    path = env["PATH_INFO"].split("/")
    if "goodbye" in path:
        response = "Goodbye {}!".format(user.name)
    else:
        response = "Hello {}!".format(user.name)

    return [response.encode('utf-8')]

我相信你的代码意识现在正在制造很多噪音。application函数现在显然在做三件不同的事情。它开始获取User对象。然后,它决定客户端请求什么端点,并基于该路径编写相关消息。最后,它将编码后的消息返回给发出请求的客户机。

每当用户的初始请求和实际响应之间需要更多的功能时,application函数就会变得更加复杂。简而言之,如果你继续这样下去,你的代码一定会变得一团糟。

理想情况下,我们希望每个函数只做一件事。

责任链模式

每段代码做一件事情并且只做一件事情的想法被称为单一责任原则,这是当你致力于编写更好的代码时的另一个方便的指导方针。

如果我们想遵守前面示例中的单一责任原则,我们需要一段代码来处理用户检索,另一段代码来验证用户是否与密码匹配。还有一段代码会根据请求的路径生成一条消息,最后,某段代码会将响应返回给浏览器。

在我们尝试为服务器编写代码之前,我们将使用一个示例应用建立一些关于解决方案的直觉。

在我们的示例应用中,我们有四个函数,每个函数都简单地打印出一条消息。

def function_1():
  print("function_1")

def function_2():
  print("function_2")

def function_3():
  print("function_3")

def function_4():
  print("function_4")

def main_function():
  function_1()
  function_2()
  function_3()
  function_4()

if __name__ == "__main__":
  main_function()

从本节开始的讨论中,我们知道让main_function按顺序调用每个函数并不理想,因为在现实世界中,这会导致我们在上一节中遇到的混乱代码。

我们想要的是创建某种方式,让我们进行一次调用,然后动态调用函数。

class CatchAll(object):
    def __init__(self):
        self.next_to_execute = None

    def execute(self):
        print("end reached.")

class Function1Class(object):
    def __init__(self):
        self.next_to_execute = CatchAll()

    def execute(self):
        print("function_1")
        self.next_to_execute.execute()

class Function2Class(object):
    def __init__(self):
        self.next_to_execute = CatchAll()

    def execute(self):
        print("function_2")
        self.next_to_execute.execute()

class Function3Class(object):
    def __init__(self):
        self.next_to_execute = CatchAll()

    def execute(self):
        print("function_3")
        self.next_to_execute.execute()

class Function4Class(object):
    def __init__(self):
        self.next_to_execute = CatchAll()

    def execute(self):
        print("function_4")
        self.next_to_execute.execute()   

def main_function(head):
  head.execute()

if __name__ == "__main__":
    hd = Function1Class()

    current = hd
    current.next_to_execute = Function2Class()

    current = current.next_to_execute
    current.next_to_execute = Function3Class()

    current = current.next_to_execute
    current.next_to_execute = Function4Class()

    main_function(hd)

尽管编写的代码量比第一个实例多得多,但您应该开始看到这个新程序是如何清晰地将每个函数的执行与其他函数分开的。

为了进一步说明这一点,我们现在扩展主 execute 函数以包含一个请求字符串。该字符串是一系列数字,如果数字字符串中包含函数名称中的数字,则该函数会将其名称与请求字符串一起打印出来,然后从请求字符串中删除该数字的所有实例,并将请求传递给代码的其余部分进行处理。

在我们最初的上下文中,我们可以按如下方式解决这个新需求:

def function_1(in_string):
    print(in_string)
    return "".join([x for x in in_string if x != '1'])

def function_2(in_string):
    print(in_string)
    return "".join([x for x in in_string if x != '2'])

def function_3(in_string):
    print(in_string)
    return "".join([x for x in in_string if x != '3'])

def function_4(in_string):
    print(in_string)
    return "".join([x for x in in_string if x != '4'])

def main_function(input_string):
    if '1' in input_string:
        input_string = function_1(input_string)
    if '2' in input_string:
        input_string = function_2(input_string)
    if '3' in input_string:
        input_string = function_3(input_string)
    if '4' in input_string:
        input_string = function_4(input_string)

    print(input_string)

if __name__ == "__main__":
    main_function("1221345439")

是的,你是对的——所有的函数都做完全相同的事情,但这只是因为示例问题的设置方式。如果你参考最初的问题,你会发现情况并不总是这样。

还应该感觉到的是一系列的if语句,随着更多的函数被添加到执行队列中,这些语句的长度只会增加。

最后,您可能已经注意到了main_function与正在执行的每个函数之间的紧密耦合。

在我们的项目中实施责任链

现在,让我们用之前开始构建的想法来解决同一个问题。

class CatchAll(object):
    def __init__(self):
        self.next_to_execute = None

    def execute(self, request):
        print(request)

class Function1Class(object):
    def __init__(self):
        self.next_to_execute = CatchAll()

    def execute(self, request):
        print(request)
        request = "".join([x for x in request if x != '1'])
        self.next_to_execute.execute(request)

class Function2Class(object):
    def __init__(self):
        self.next_to_execute = CatchAll()

    def execute(self, request):
        print(request)
        request = "".join([x for x in request if x != '2'])
        self.next_to_execute.execute(request)

class Function3Class(object):
    def __init__(self):
        self.next_to_execute = CatchAll()

    def execute(self, request):
        print(request)
        request = "".join([x for x in request if x != '3'])
        self.next_to_execute.execute(request)

class Function4Class(object):
    def __init__(self):
        self.next_to_execute = CatchAll()

    def execute(self, request):
        print(request)
        request = "".join([x for x in request if x != '4'])
        self.next_to_execute.execute(request)

def main_function(head, request):
  head.execute(request)

if __name__ == "__main__":
    hd = Function1Class()

    current = hd
    current.next_to_execute = Function2Class()

    current = current.next_to_execute
    current.next_to_execute = Function3Class()

    current = current.next_to_execute
    current.next_to_execute = Function4Class()

    main_function(hd, "1221345439")

很明显,我们手头已经有了一个更清洁的解决方案。我们不需要对每个类与下一个类的连接方式做任何改变,除了传递请求之外,我们不需要对单个执行函数或主函数做任何改变来适应每个类执行请求的方式。我们已经清楚地将每个函数分成它自己的代码单元,这些代码单元可以插入到处理请求的类链中,或者从处理请求的类链中删除。

每个处理程序只关心自己的执行,而忽略另一个处理程序由于查询而执行时会发生什么。处理程序根据收到的请求立即决定是否应该执行任何操作,这增加了确定哪些处理程序应该作为查询结果执行的灵活性。这个实现最好的部分是可以在运行时添加和删除处理程序,当顺序对于执行队列不重要时,您甚至可以打乱处理程序的顺序。这再次表明,松耦合代码比紧耦合代码更灵活。

为了进一步说明这种灵活性,我们扩展了前面的程序,这样每个类只有在看到与类名相关的数字在请求字符串中时才会做一些事情。

class CatchAll(object):
    def __init__(self):
        self.next_to_execute = None

    def execute(self, request):
        print(request)

class Function1Class(object):
    def __init__(self):
        self.next_to_execute = CatchAll()

    def execute(self, request):
        if '1' in request:
            print("Executing Type Function1Class with request [{}]".format(request))
            request = "".join([x for x in request if x != '1'])

        self.next_to_execute.execute(request)

class Function2Class(object):
    def __init__(self):
        self.next_to_execute = CatchAll()

    def execute(self, request):
        if '2' in request:
            print("Executing Type Function2Class with request [{}]".format(request))
            request = "".join([x for x in request if x != '2'])

        self.next_to_execute.execute(request)

class Function3Class(object):
    def __init__(self):
        self.next_to_execute = CatchAll()

    def execute(self, request):
        if '3' in request:
            print("Executing Type Function3Class with request [{}]".format(request))
            request = "".join([x for x in request if x != '3'])

        self.next_to_execute.execute(request)

class Function4Class(object):
    def __init__(self):
        self.next_to_execute = CatchAll()

    def execute(self, request):
        if '4' in request:
            print("Executing Type Function4Class with request [{}]".format(request))
            request = "".join([x for x in request if x != '4'])

        self.next_to_execute.execute(request)

def main_function(head, request):
  head.execute(request)

if __name__ == "__main__":
    hd = Function1Class()

    current = hd
    current.next_to_execute = Function2Class()

    current = current.next_to_execute
    current.next_to_execute = Function3Class()

    current = current.next_to_execute
    current.next_to_execute = Function4Class()

    main_function(hd, "12214549")

注意,这次我们在请求中没有任何3数字,因此在请求被传递给Function4Class的实例之前,Function3Class实例的代码没有被执行。此外,请注意,我们在设置执行链或main_function的代码上没有任何变化。

这个处理者链是责任链模式的本质。

更 Pythonic 化的实现

在该模式的经典实现中,您将定义某种通用处理程序类型,该类型将定义next_to_execute对象和execute函数的概念。正如我们之前看到的,Python 中的 duck-typing 系统使我们不必增加额外的开销。只要我们为链中要使用的每个处理程序定义了相关部分,我们就不需要从某个共享库继承。这不仅节省了大量编写样板代码的时间,而且使您(开发人员)能够根据应用或系统的需要,轻松地发展这些类型的想法。

在更受约束的语言中,您没有在运行中发展设计的奢侈。像这样改变方向通常需要大量的重新设计,很多代码需要重写。为了避免重新设计整个系统或重建大部分应用的痛苦,工程师和开发人员试图迎合他们能想到的每一种可能性。这种方法在两个关键方面存在缺陷。首先,人类对未来的知识是不完善的,所以即使你要为今天设计完美的系统(几乎不可能),也不可能考虑到客户可能使用你的系统的每一种方式,或者市场可能在哪里驱动你的应用。第二,在大多数情况下,你不会需要它(YAGNI),或者,换句话说,在这样一个系统的设计中涉及的大多数潜在的未来将永远不会被需要,因此这是浪费时间和精力。Python 再一次提供了从简单问题的简单解决方案开始,然后随着用例的变化和发展来发展解决方案的能力。Python 与您一起发展,随着您的发展而变化和成长。就像围棋一样,Python 很容易上手,但是你可以用一生的时间来掌握它的多种可能性。

下面是责任链模式的一个简单实现,去掉了所有多余的东西。剩下的就是我们将在 web 应用上实现的一般结构。

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

    def handle_request(self, request):
        pass

class Handler1(object):
    def __init__(self):
        self.next_handler = EndHandler()

    def handle_request(self, request):

        self.next_handler.handle_request(request)

def main(request):
    concrete_handler = Handler1()

    concrete_handler.handle_request(request)

if __name__ == "__main__":
    # from the command line we can define some request

    main(request)

从本章开始,我们将使用这个非常简单的框架来显著改进我们的 web 应用。新的解决方案将更加严格地遵守单一责任原则。您还会注意到,您可以向流程中添加更多的功能和扩展。

import base64

class User(object):
    def __init__(self, username, password, name, email):
        self.username = username
        self.password = password
        self.name = name
        self.email = email

    @classmethod
    def get_verified_user(cls, username, password):
        return User(
            username,
            password,
            username,
            "{}@demo.com".format(username)
        )

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

    def handle_request(self, request, response=None):
        return response.encode('utf-8')

class AuthorizationHandler(object):
    def __init__(self):
        self.next_handler = EndHandler()

    def handle_request(self, request, response=None):
        authorization_header = request["HTTP_AUTHORIZATION"]
        header_array = authorization_header.split()
        encoded_string = header_array[1]
        decoded_string = base64.b64decode(encoded_string).decode('utf-8')
        username, password = decoded_string.split(":")
        request['username'] = username
        request['password'] = password

        return self.next_handler.handle_request(request, response)

class UserHandler(object):
    def __init__(self):
        self.next_handler = EndHandler()

    def handle_request(self, request, response=None):
        user = User.get_verified_user(request['username'], request['password'])
        request['user'] = user

        return self.next_handler.handle_request(request, response)

class PathHandler(object):
    def __init__(self):
        self.next_handler = EndHandler()

    def handle_request(self, request, response=None):
        path = request["PATH_INFO"].split("/")
        if "goodbye" in path:
            response = "Goodbye {}!".format(request['user'].name)
        else:
            response = "Hello {}!".format(request['user'].name)

        return self.next_handler.handle_request(request, response)

def application(env, start_response):
    head = AuthorizationHandler()

    current = head
    current.next_handler = UserHandler()

    current = current.next_handler
    current.next_handler = PathHandler()

    start_response('200 OK', [('Content-Type', 'text/html')])    
    return [head.handle_request(env)]

责任链模式的另一个可选实现是实例化一个主对象,以便在实例化时从传递给调度程序的列表中调度处理程序。

class Dispatcher(object):

  def __init__(self, handlers=[]):
    self.handlers = handlers

    def handle_request(self, request):
      for handler in self.handlers:
        request = handler(request)

      return request

Dispatcher类利用了 Python 中一切都是对象的事实,包括函数。由于函数是一等公民(这意味着它们可以传递给函数,也可以从函数返回),我们可以实例化 dispatcher 并传入一个函数列表,该列表可以像其他列表一样保存。当需要执行函数时,我们遍历列表并依次执行每个函数。

使用Dispatcher实现我们在本节中使用的示例代码如下所示:

class Dispatcher(object):

    def __init__(self, handlers=[]):
        self.handlers = handlers

    def handle_request(self, request):
        for handle in self.handlers:
            request = handle(request)

        return request

def function_1(in_string):
    print(in_string)
    return "".join([x for x in in_string if x != '1'])

def function_2(in_string):
    print(in_string)
    return "".join([x for x in in_string if x != '2'])

def function_3(in_string):
    print(in_string)
    return "".join([x for x in in_string if x != '3'])

def function_4(in_string):
    print(in_string)
    return "".join([x for x in in_string if x != '4'])

def main(request):
    dispatcher = Dispatcher([
        function_1,
        function_2,
        function_3,
        function_4
    ])

    dispatcher.handle_request(request)

if __name__ == "__main__":
    main("1221345439")

作为一个练习,我建议您使用这个结构来实现 web 应用的流程。

在我们结束这一章之前,我想让你思考一下我们开始的关于中间件的讨论。您能看到如何使用责任链模式通过向队列添加处理程序来为您的 web 应用实现中间件吗?

临别赠言

正如我们在本章中所看到的,请求是构建模式的中心对象,因此当请求对象从一个处理程序传递到下一个处理程序时,在构建和修改请求对象时应该小心。

由于责任链模式为我们提供了调整和改变运行时使用的处理程序的机会,我们可以将一些增强性能的想法应用到责任链中。我们可以在每次使用处理程序时将它移动到链的前面,导致更经常使用的处理程序占据链中的早期位置,然后是不经常使用的处理程序(仅当处理程序的执行顺序无关紧要时才使用)。当处理程序的顺序有某种意义时,使用前一章的思想并缓存某些处理的请求的结果,可以提高性能。

我们从来不想完全忽略一个请求,所以您必须确保有某种 end 处理程序作为链的总括,至少提醒您链中的任何处理程序都没有处理该请求。

在本章的例子中,我们让链一直执行,直到所有的处理程序都查看了请求。情况并不一定如此;你可以在任何时候断开这个链并返回一个结果。

责任链模式可以用来将元素的处理封装到管道中(就像数据管道一样)。

最后,一个好的经验法则是,当一个请求有不止一个潜在的处理程序时,使用责任链模式,这样就不知道哪个或哪些处理程序最适合处理您将收到的请求。

练习

  • 查看 SQLAlchemy 并创建一个到 SQLite 数据库的接口,在该数据库中存储基本的用户数据。
  • 扩展检索用户的User类函数,以便它使用 SQLite 数据库,就像在前面的练习中一样。
  • 将您在第二章中构建的日志记录器添加到您的 web 应用的责任链中,这样您就可以记录传入的请求和发送回客户端的响应。
  • 使用责任链的Dispatcher实现你的 web 应用。

十一、命令模式

我会回来的。—终结符

机器人来了。

在这一章中,我们将看看如何使用 Python 代码来控制机器人。出于我们的目的,我们将使用 Python 标准库中包含的turtle模块。这将使我们能够处理前进、左转和右转等命令,而不需要构建一个实际的机器人,尽管您应该能够使用本章中开发的代码,结合 Raspberry Pi 等板和一些执行器,来实际构建一个您可以用代码控制的机器人。

控制海龟

让我们从做一些简单的事情开始,比如用 turtle 模块在屏幕上画一个正方形。

turtle_basic.py

import turtle

turtle.color('blue', 'red')

turtle.begin_fill()

for _ in range(4):
    turtle.forward(100)
    turtle.left(90)

turtle.end_fill()
turtle.done()

海龟是一个简单的线条和填充工具,有助于可视化机器人正在做什么。在前面的代码中,我们导入了turtle库。接下来,我们将线条颜色设置为蓝色,背景颜色设置为红色。然后,我们告诉海龟将当前点记录为稍后将被填充的多边形的起点。然后,循环绘制正方形的四条边。最后,end _fill()方法用红色填充多边形。done()方法防止程序在进程完成后终止和关闭窗口。

如果你使用的是 Ubuntu 系统,并且得到一个关于python3-tk,的错误信息,你需要安装这个包。

sudo apt-get install python3-tk

运行代码,你应该看到一个小三角形(乌龟)在屏幕上移动。

当我们试图在屏幕上绘制形状和线条时,这种方法是可行的,但是因为乌龟只是我们机器人的一个隐喻,所以绘制漂亮的形状并没有太大的用处。我们将扩展我们的机器人控制脚本以接受四个命令(开始、停止、向左转 10 度和向右转 10 度),我们将把它们映射到键盘按键。

turtle_control.py

import turtle

turtle.setup(400, 400)

screen = turtle.Screen()
screen.title("Keyboard drawing!")

t = turtle.Turtle()
distance = 10

def advance():
   t.forward(distance)

def turn_left():
    t.left(10)

def turn_right():
    t.right(10)

def retreat():
    t.backward(10)

def quit():
    screen.bye()

screen.onkey(advance, "w")
screen.onkey(turn_left, "a")
screen.onkey(turn_right, "d")
screen.onkey(retreat, "s")
screen.onkey(quit, "Escape")

screen.listen()
screen.mainloop()

你本质上做的是创建一个界面来控制海龟。在现实世界中,这将类似于一辆遥控汽车,它接受一个信号表示左转,另一个信号表示右转,第三个信号表示加速,第四个信号表示减速。如果您要构建一个 RC 发送器,它将通常来自遥控器的信号转换为现在使用一些电子设备和 USB 端口从键盘输入的信号,您将能够使用类似于我们这里的代码来控制遥控车。同样,你可以通过遥控来控制机器人。

本质上,我们是从系统的一部分向另一部分发送命令。

命令模式

每当您想将一条指令或一组指令从一个对象发送到另一个对象,同时保持这些对象松散耦合时,明智的做法是将执行指令所需的一切都封装在某种数据结构中。

发起执行的客户端不必知道指令将被执行的方式;它所需要做的就是设置所有需要的信息,并传递系统下一步需要发生的任何事情。

因为我们正在处理一个面向对象的范例,用于封装指令的数据结构将是一个对象。用于封装其他方法执行所需信息的对象类称为命令。客户机对象实例化一个命令,该命令包含它希望执行的方法、执行该方法所需的所有参数以及拥有该方法的某个目标对象。

这个目标对象称为接收器。接收者是一个类的实例,它可以在给定封装信息的情况下执行该方法。

所有这些都依赖于一个名为 invoker 的对象,它决定接收方的方法何时执行。认识到方法的调用类似于类的实例可能会有所帮助。

在没有任何实际功能的情况下,命令模式可以这样实现:

class Command:
    def __init__(self, receiver, text):
        self.receiver = receiver
        self.text = text

    def execute(self):
        self.receiver.print_message(self.text)

class Receiver(object):
    def print_message(self, text):
      print("Message received: {}".format(text))

class Invoker(object):
    def __init__(self):
        self.commands = []

    def add_command(self, command):
        self.commands.append(command)

    def run(self):
        for command in self.commands:
            command.execute()

if __name__ == "__main__":
    receiver = Receiver()

    command1 = Command(receiver, "Execute Command 1")
    command2 = Command(receiver, "Execute Command 2")

    invoker = Invoker()
    invoker.add_command(command1)
    invoker.add_command(command2)
    invoker.run()

现在,即使接收者正忙于执行另一个命令,您也可以对要执行的命令进行排队。在分布式系统中,这是一个非常有用的概念,在分布式系统中,需要捕获和处理传入的命令,但是系统可能正忙于另一个动作。将一个对象中的所有信息放在队列中允许系统处理所有传入的命令,而不会在执行其他命令时丢失重要的命令。实际上,您可以在运行时动态地创建新的行为。当您希望一次设置一个方法的执行,然后稍后再执行它时,这个过程特别有用。

想想看,如果操作人员只能发送一个要执行的命令,然后必须等待该命令执行完毕才能发送下一个命令,这对像火星漫游车这样的机器人意味着什么。这将使得在火星上做任何事情都极其耗时,几乎是不可能的。取而代之的是向漫游者发送一系列动作。然后,漫游者可以依次调用每个命令,允许它在单位时间内完成更多的工作,并可能消除漫游者和地球之间传输和接收命令之间的时间延迟的影响。这一串命令称为宏。

在运行自动化测试时,当您想要模拟用户与系统的交互,或者当您想要自动化您重复执行的某些任务时,宏也很有用。您可以为流程中的每一步创建一个命令,然后在让某个调用程序处理执行之前将它们串起来。解耦允许你通过键盘将直接的人工干预换成生成的计算机输入。

一旦我们开始将命令串在一起,我们还可以考虑撤销已经执行的命令。这是命令模式特别有用的另一个领域。通过为每个被调用的命令将撤销命令压入堆栈,我们可以免费获得多级撤销,就像你在从现代文字处理器到视频编辑软件的所有东西中看到的一样。

为了说明如何使用命令模式构建多级撤销堆栈,我们将构建一个非常简单的计算器,它可以做加法、减法、乘法和除法。为了保持例子的简单,我们不考虑与零相乘的情况。

class AddCommand(object):
    def __init__(self, receiver, value):
        self.receiver = receiver
        self.value = value

    def execute(self):
        self.receiver.add(self.value)

    def undo(self):
        self.receiver.subtract(self.value)

class SubtractCommand(object):
    def __init__(self, receiver, value):
        self.receiver = receiver
        self.value = value

    def execute(self):
        self.receiver.subtract(self.value)

    def undo(self):
        self.receiver.add(self.value)

class MultiplyCommand(object):
    def __init__(self, receiver, value):
        self.receiver = receiver
        self.value = value

    def execute(self):
        self.receiver.multiply(self.value)

    def undo(self):
        self.receiver.divide(self.value)

class DivideCommand(object):
    def __init__(self, receiver, value):
        self.receiver = receiver
        self.value = value

    def execute(self):
        self.receiver.divide(self.value)

    def undo(self):
        self.receiver.multiply(self.value)

class CalculationInvoker(object):
    def __init__(self):
        self.commands = []
        self.undo_stack = []

    def add_new_command(self, command):
        self.commands.append(command)

    def run(self):
        for command in self.commands:
            command.execute()
            self.undo_stack.append(command)

    def undo(self):
        undo_command = self.undo_stack.pop()
        undo_command.undo()

class Accumulator(object):
    def __init__(self, value):
        self._value = value

    def add(self, value):
        self._value += value

    def subtract(self, value):
        self._value -= value

    def multiply(self, value):
        self._value *= value

    def divide(self, value):
        self._value /= value

    def __str__(self):
        return "Current Value: {}".format(self._value)

if __name__ == "__main__":
    acc = Accumulator(10.0)

    invoker = CalculationInvoker()
    invoker.add_new_command(AddCommand(acc, 11))
    invoker.add_new_command(SubtractCommand(acc, 12))
    invoker.add_new_command(MultiplyCommand(acc, 13))
    invoker.add_new_command(DivideCommand(acc, 14))

    invoker.run()

    print(acc)

    invoker.undo()
    invoker.undo()
    invoker.undo()
    invoker.undo()

    print(acc)

请注意,当 Python 在打印时将accumulator对象转换为字符串时,使用了__ str()__方法。

正如我们已经多次看到的,Python 中的 duck 类型允许我们跳过定义一个总体基类来继承。只要我们的命令有execute()undo()方法,并接受一个接收器和值作为构造的参数,我们就可以使用任何我们喜欢的命令。

在前面的代码中,我们将调用者执行的每个命令都推送到一个堆栈上。然后,当我们想要撤销一个命令时,从堆栈中弹出最后一个命令,并执行该命令的undo()方法。在计算器的情况下,执行反向计算以将累加器的值返回到命令执行前的值。

命令模式中的Command类的所有实现都有一个方法,比如由调用者调用来执行命令的execute()Command类的实例保存了对接收方的引用、要执行的方法的名称以及在执行中用作参数的值。接收者是唯一知道如何执行命令中保存的方法的对象,它独立于系统中的其他对象管理自己的内部状态。客户端不知道命令的实现细节,命令和调用者也不知道。明确地说,command对象不执行任何东西;它只是一个容器。通过实现这种模式,我们在要执行的动作和调用该动作的对象之间添加了一个抽象层。增加的抽象导致系统中不同对象之间更好的交互,以及它们之间更松散的耦合,使得系统更容易维护和更新。

这种设计模式的核心是将方法调用转换成数据,这些数据可以保存在变量中,作为参数传递给方法或函数,并作为结果从函数返回。应用这种模式的结果是函数或方法成为一等公民。当函数是一级公民时,变量可以指向函数,函数可以作为参数传递给其他函数,它们可以作为执行函数的结果返回。

command 模式的有趣之处,特别是这种将行为转变为一等公民的能力,在于我们可以使用这种模式来实现一种带有惰性求值的函数式编程风格,这意味着函数可以传递给其他函数,也可以由函数返回。所有函数都传递了执行所需的所有信息,没有全局状态,只有在需要返回实际结果时才执行函数。这是对函数式编程的一个相当肤浅的描述,但其含义是深刻的。

我们故事中有趣的转折是,在 Python 中一切都是函数,因此我们也可以去掉封装函数调用的类,只需将函数和相关参数传递给调用者。

def text_writer(string1, string2):
    print("Writing {} - {}".format(string1, string2))

class Invoker(object):
    def __init__(self):
        self.commands = []

    def add_command(self, command):
        self.commands.append(command)

    def run(self):
        for command in self.commands:
            command"function"

if __name__ == "__main__":
    invoker = Invoker()
    invoker.add_command({
        "function": text_writer,
        "params": ("Command 1", "String 1")
    })
    invoker.add_command({
        "function": text_writer,
        "params": ("Command 2", "String 2")
    })
    invoker.run()

对于这样一个简单的例子,你可以只定义一个 lambda 函数,甚至不需要写函数定义。

class Invoker(object):
    def __init__(self):
        self.commands = []

    def add_command(self, command):
        self.commands.append(command)

    def run(self):
        for command in self.commands:
            command"function"

if __name__ == "__main__":
    f = lambda string1, string2: print("Writing {} - {}".format(string1, string2))

    invoker = Invoker()
    invoker.add_command({
        "function": f,
        "params": ("Command 1", "String 1")
    })
    invoker.add_command({
        "function": f,
        "params": ("Command 2", "String 2")
    })
    invoker.run()

一旦你想执行的函数变得更复杂,除了我们在前一个程序中使用的方法之外,你还有两个选择。您可以将命令的execute()函数包装在 lambda 中,从而避免创建类。尽管这是一个巧妙的技巧,但是如果你只是为复杂的情况创建一个类,你的代码可能会更清晰,这是第二种选择。所以,一个好的经验法则是,当你需要为比一段代码更复杂的东西创建命令时,你可以用一行 lambda 函数来包装,为它创建一个类。

临别赠言

无论是建造自动驾驶汽车还是向外太空发送消息,将执行请求与实际执行解耦,不仅可以缓冲需要执行的动作,还可以交换输入的模式。

重要的是,命令模式将调用方与接收方隔离开来。它还将执行的建立时间与处理时间分开。

练习

  • 使用 command 模式创建一个命令行单词替换脚本,该脚本可以用其他单词替换给定文本文件中给定单词的每个实例。然后,添加一个命令来撤消上一次替换操作。

十二、解释器模式

西尔维亚·布鲁姆:当你睡不着的时候你会做什么?托宾·凯勒:我保持清醒。—解释器

有时,不使用 Python 来描述问题或此类问题的解决方案是有意义的。当我们需要一种面向特定问题领域的语言时,我们求助于一种叫做特定领域语言的东西。

特定领域语言

有些语言,如 Python,是为解决任何类型的问题而创建的。Python 属于被称为通用语言的整个语言家族,旨在用于解决任何问题。在极少数情况下,创建一种只做一件事,但做得非常好的语言更有意义。这些语言属于特定领域语言(DSL)家族,对那些在某个特定领域是专家但不是程序员的人很有帮助。

在您的编程生涯中,您可能已经遇到过 DSL。如果你曾经修修补补过一个网站,你知道 CSS,它是一个 DSL,用于定义 HTML 应该如何在浏览器中呈现。

style.css

body {

  color: #00ff00;

}

这一小段 CSS 代码告诉浏览器将使用该 CSS 文件的网页的 body 标记内的所有文本呈现为绿色。浏览器完成这项工作所需的代码比这段简单的代码要复杂得多,但 CSS 对大多数人来说足够简单,几个小时就能学会。HTML 本身是一个 DSL,由浏览器解释,并显示为一个具有相当复杂布局的格式化文档。

或者,您可能在行为驱动开发中使用过 Aloe,在行为驱动开发中,您用人类可读的句子定义了项目的验收标准。

Install the aloe framework using pip
$ pip install aloe

现在,我们可以写一些测试。


zero.feature

Feature: Compute factorial
  In order to play with aloe
  As beginners
  We'll implement factorial

  Scenario: Factorial of 0
    Given I have the number 0
    When I compute its factorial
    Then I see the number 1

接下来,您将这些步骤映射到一些 Python 代码,以便 Aloe 框架能够理解所描述的行为。


steps.py

from aloe import *

@step(r'I have the number (\d+)')

def have_the_number(step, number):
  world.number = int(number)

@step('I compute its factorial')

def compute_its_factorial(step):
  world.number = factorial(world.number)

@step(r'I see the number (\d+)')

def check_number(step, expected):
  expected = int(expected)
  assert world.number == expected, \
    "Got %d" % world.number

def factorial(number):
  return -1

此代码允许您用非技术人员可以理解的术语定义程序的结果,然后将这些术语映射到实际代码,这确保它们按照特性文件中的描述执行。.feature文件中的描述是 DSL,steps.py文件中的 Python 代码用于将 DSL 中的单词和短语转换成计算机可以理解的东西。

当您决定创建一个 DSL(实践领域工程)时,您就能够在领域专家和开发人员之间进行清晰的交流。不仅专家能够更清楚地表达自己,而且开发人员会明白他们需要什么,计算机也是如此。您通常会发现,领域专家会从他们的领域向开发人员描述问题或问题的解决方案,然后开发人员会立即用特定于领域的语言写下描述。领域专家能够理解开发人员对问题或解决方案的描述,并且能够对描述提出修改或扩展的建议。因此,在会议过程中,或者最多几天的时间,描述可以转化为有效的解决方案,从而大大提高每个参与者的工作效率。

让我们来做点实际的。

你已经签约创建一个系统,餐馆可以用它来定义他们的特色菜。这些特价商品有时有简单的规则,如买一送一,但其他时候它们更复杂,如如果你在周二买了三个披萨,最便宜的一个是免费的。挑战在于这些规则经常变化,因此为每次变化重新部署代码是没有意义的。如果您不在乎当您的客户决定更改特价规则时,每周被迫更改代码,那么这将导致的if语句的潜在幅度应该足以吓退您。

既然我们已经讨论了 DSL,您可能会怀疑为餐馆创建 DSL 是解决方案。然后,对特殊标准的任何更改或添加都可以独立于主应用进行存储,并且只在调用时进行解释。

在一次内容丰富的演讲中,ICE 的技术主管 Neil Green 建议采用以下流程来开发 DSL:

  1. 了解你的领域。
  2. 为你的领域建模。
  3. 实现您的域。

如果我们按照尼尔的指导,我们会去和餐馆老板谈谈。我们会尝试学习他或她如何表达特色菜的规则,然后尝试用一种对我们和店主都有意义的语言来捕捉这些规则。然后我们会根据我们的谈话画出一些图表。如果所有者同意我们抓住了他或她所说的本质,我们就可以编写一些代码来实现我们的模型。

DSL 有两种类型:内部和外部。外部 DSL 将代码写在外部文件中或作为字符串。该字符串或文件在使用前由应用读取和解析。CSS 是外部 DSL 的一个例子,其中浏览器的任务是读取、解析和解释表示 HTML 元素应该如何显示的文本数据。另一方面,内部 DSL 使用语言的特性——在这种情况下是 Python——使人们能够编写类似领域语法的代码,一个例子是 numpy 如何使用 Python 来允许数学家描述线性代数问题。

解释器模式特别适合内部 DSL。

这个想法是创建一种语法,它远不如通用语言全面,但却更具表现力,特别是当它与所讨论的领域相关时。

看看下面的代码,它定义了我们想象中的餐馆经营的特色菜的规则,并用 Python 写成了条件语句。

pizzas = [item for tab.items if item.type == "pizza"]

if len(pizzas) > 2 and day_of_week == 4:
    cheapest_pizza_price = min([pizza.price for pizza in pizzas])
    tab.add_discount(cheapest_pizza_price)

drinks = [item for tab.items if item.type == "drink"]

if 17 < hour_now < 19:
    for item in tab.items:
        if item.type == "drink":
            item.price = item.price * 0.90   

if tab.customer.is_member():
    for item in tab.items:
        item.price = item.price * 0.85

现在,看看前面的代码片段和在一个简单的用于定义特价商品的 DSL 中相同规则的定义之间的鲜明对比。

If tab contains 2 pizzas on Wednesdays cheapest one is free
Every day from 17:00 to 19:00 drinks are less 10%
All items are less 15% for members

我们现在还不看解释,它将在稍后出现。我想让你注意的是,DSL 更清楚地传达了如何应用特价规则。它以一种对企业主有意义的方式书写,并允许他们确认它确实符合他们的意图,或者是犯了一个错误,需要更正。模糊性减少了,添加新规则或特色菜变得更加简单——这是两方面的双赢。

DSL 的优势

对于 DSL 所创建的领域的理解和交流水平大大提高了。领域专家可以在没有软件工程背景的情况下对问题和解决方案的表达进行推理。商业信息系统的开发因此可以从软件开发人员转移到领域专家。这导致更丰富、更准确的系统,用系统帮助的专家可以理解的术语来表达。

DSL 的缺点

在您开始为您能想到的每个问题领域创建 DSL 之前,请记住学习 DSL 是有成本的,即使它对领域专家来说是有意义的。新来的人可能理解你的 DSL 在表达什么,但是他们仍然需要学习语言实现以便能够使用它。你的 DSL 越深入和丰富,领域专家操作该语言所需的知识就越多,这可能会影响到能够使用和维护该系统的人数。请记住,DSL 的使用应该有助于业务,而不是阻碍业务,否则从长远来看,如果一开始就采用这种语言,它将会抛弃这种语言。

我们已经考虑了使用 DSL 的利弊,并决定使用 DSL 来指定餐馆特色菜的规则。

在宏观层面上,您希望完成两项任务。首先,你要定义语言。具体来说,您希望定义语言的语义(含义)和语法(结构)。接下来,你要编写代码,能够把语言作为输入,并把它翻译成机器可以理解和行动的形式。

根据您与客户的对话,提取流程中涉及的内容以及每个相关实体采取的行动。在此之后,分离出在所考虑的领域中具有特定含义的常用单词或短语。使用这三个元素构建一个语法,在实现之前,您将与领域专家讨论这个语法。

在我们看一个关于餐馆特色菜的例子之前,我想让你把自己想象成一个工具制造者。每当你遇到挫折或问题时,你应该开始思考你将如何着手解决这个问题,以及你将编写什么工具来使这个过程十倍地更容易或更快。这是将最好的软件开发人员和工程师与该领域其他人区分开来的心态。

在我们的示例中,我们首先与餐馆老板(领域专家)进行了一系列对话,并确定了以下特色菜规则:

Members always get 15% off their total tab
During happy hour, which happens from 17:00 to 19:00 weekdays, all drinks are less 10%
Mondays are buy one get one free burger nights
Thursday are 'eat all you can' ribs
Sunday nights after 18:00 buy three pizzas and get the cheapest one free when you buy 6 you get the two cheapest ones free and so on for every three additional pizzas

然后,我们确定了被提及的内容:

members
tabs
happy hour
drinks
weekdays
Mondays
burgers
Thursday
ribs
Sunday
pizzas

我们还确定了可以采取的行动:

get 15% discount
get 10% discount
get one free
eat all you can

最后,我们认识到了与餐厅特色菜相关的关键理念:

If a customer is of a certain type they always get a fixed % off their total tab
At certain times all items of a specific type gets discounted with a fixed percentage
At certain times you get a second item of a certain type free if you were to buy one item of that type

概括关键理念中的元素会产生以下特价规则:

If certain conditions are met certain items get a % discount

一种正式表达语法的方式是用扩展的巴科斯诺尔形式(EBNF 关于 EBNF 的更多信息参见: https://www.cs.umd.edu/class/fall2002/cmsc214/Tutorial/ebnf.html )。我们在 ENBF 中的餐馆示例如下所示:

rule: "If ", conditions, " then ", item_type, " get ", discount
conditions: condition | conditions " and " conditions | conditions " or " conditions
condition: time_condition | item_condition | customer_condition
discount: number, "% discount" | "cheapest ", number, item_type " free"

time_condition: "today is ", day_of_week | "time is between ", time, " and ", time | "today is a week day" | "today not a week day"
day_of_week: "Monday" | "Tuesday" | "Wednesday" | "Thursday" | "Friday" | "Saturday" | "Sunday"
time: hours, ":", minutes
hours: hour_tens, hour_ones
hour_tens: "0" | "1"
hour_ones: digit
minutes: minute_tens, minute_ones
minute_tens: "0" | "1" | "2" | "3" | "4" | "5"
minute_ones: digit

item_condition: "item is a ", item_type | "there are ", number, " of ", item_type
item_type: "pizza" | "burger" | "drink" | "chips"
number: {digit}
digit: "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9"

customer_condition: "customer is a ", customer_type
customer_type: "member" | "non-member"

您有一组不能用任何模式替换的终结符,以及一组非终结符产生规则,这些规则可用于用一系列终结符或其他非终结符模式的某种配方替换非终结符(占位符)。非终结符的不同替换方式由管道字符“|”分隔。

当您创建一个外部 DSL 时,可以使用 PyParsing 之类的包来把用刚才看到的语法编写的字符串翻译成 Python 代码可以处理的数据类型。

现在,您已经看到了一个示例,说明了如何通过与领域专家的对话来理解领域,识别领域的关键元素,将这种理解整理成某种可以被领域专家验证的领域模型,最后将 DSL 实现为过程的最终产品,从而完成领域建模的过程。

从现在开始,我们将处理内部 DSL。

如果我们要将特殊规则作为内部 DSL 来实现,我们基本上需要为每个符号创建一个类。

我们首先根据前面定义的语法创建存根:

class Tab(object):
    pass

class Item(object):
    pass

class Customer(object):
    pass

class Discount(object):
    pass

class Rule(object):
    pass

class CustomerType(object):
    pass

class ItemType(object):
    pass

class Conditions(object):
    pass

class Condition(object):
    pass

class TimeCondition(object):
    pass

class DayOfWeek(object):
    pass

class Time(object):
    pass

class Hours(object):
    pass

class HourTens(object):
    pass

class HourOnes(object):
    pass

class Minutes(object):
    pass

class MinuteTens(object):
    pass

class MinuteOnes(object):
    pass

class ItemCondition(object):
    pass

class Number(object):
    pass

class Digit(object):
    pass

class CustomerCondition(object):
    pass

在最终的设计中并不需要所有这些类,因此你应该注意 YAGNI 原则;基本上,这个想法是你不应该建造你不需要的东西。在这种情况下,我把它们都写了出来,所以你可以看到我们在定义语法的过程中提到的每一件事,并为每一件事创建了一个类。

在我们减少类并创建最终的内部 DSL 之前,我们将涉及复合模式,这在我们开始构建解释器时会很有用。

复合模式

当一个容器中的元素本身也可以是容器时,就很适合使用复合模式。看看我们为餐馆开发的 EBNF 版本的语法;项目可以是包含项目的组合。

复合模式定义了复合(即非终端)类和叶(即终端)类,它们可用于构建复合组件,如特殊规则。

class Leaf(object):
  def __init__(self, *args, **kwargs):
    pass

  def component_function(self):
    print("Leaf")

class Composite(object):
  def __init__(self, *args, **kwargs):
    self.children = []

  def component_function(self):
    for child in children:
      child.component_function()

  def add(self, child):
    self.children.append(child)

  def remove(self, child):
    self.children.remove(child)

在动态性较低的语言中,您还必须定义一个由CompositeLeaf类继承的超类,但是由于 Python 使用了 duck 类型,我们再次避免了创建不必要的样板代码。

使用复合模式的内部 DSL 实现

让我们考虑一下本章开始时餐馆折扣的第一个规则的实现,作为一些基本的内部 DSL。

class Tab(object):
    def __init__(self, customer):
        self.items = []
        self.discounts = []
        self.customer = customer

    def calculate_cost(self):
        return sum(x.cost for x in self.items)

    def calculate_discount(self):
        return sum(x for x in self.discounts)

class Item(object):
    def __init__(self, name, item_type, cost):
        self.name = name
        self.item_type = item_type
        self.cost = cost

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

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

    def is_a(self, customer_type):
        return self.customer_type == customer_type

class Discount(object):
    def __init__(self, amount):
        self.amount = amount

class CustomerType(object):
    def __init__(self, customer_type):
        self.customer_type = customer_type

class Rule(object):
    def __init__(self, tab):
        self.tab = tab
        self.conditions = []
        self.discounts = []

    def add_condition(self, test_value):
        self.conditions.append(test_value)

    def add_percentage_discount(self, item_type, percent):
        if item_type == "any item":
            f = lambda x: True
        else:
            f = lambda x: x.item_type == item_type

        items_to_discount = [item for item in self.tab.items if f(item)]
        for item in items_to_discount:
            discount = Discount(item.cost * (percent/100.0))
            self.discounts.append(discount)

    def apply(self):
        if all(self.conditions):
            return sum(x.amount for x in self.discounts)

        return 0

if __name__ == "__main__":
    member = CustomerType("Member")
    member_customer = Customer(member, "John")
    tab = Tab(member_customer)

    pizza = ItemType("pizza")
    burger = ItemType("Burger")
    drink = ItemType("Drink")

    tab.items.append(Item("Margarita", pizza, 15))
    tab.items.append(Item("Cheddar Melt", burger, 6))
    tab.items.append(Item("Latte", drink, 4))

    rule = Rule(tab)
    rule.add_condition(tab.customer.is_a(member))
    rule.add_percentage_discount("any item", 15)

    tab.discounts.append(
        rule.apply()
    )

    print(
        "Calculated cost: {}\nDiscount applied: {}\n{}% Discount applied".format(
            tab.calculate_cost(),
            tab.calculate_discount(),
            100 * tab.calculate_discount() / tab.calculate_cost()
        )
    )

现在我们有了一个工作的规则,使用产生某种形式可读代码的对象,让我们回到使用复合模式的 DSL 实现。条件可以是一组合取条件、一组析取条件或单个布尔表达式。

class AndConditions(object):
    def __init__(self):
        self.conditions = []

    def evaluate(self, tab):
        return all(x.evaluate(tab) for x in self.conditions)

    def add(self, condition):
        self.conditions.append(condition)

    def remove(self, condition):
        self.conditions.remove(condition)

class OrConditions(object):
    def __init__(self):
        self.conditions = []

    def evaluate(self, tab):
        return any(x.evaluate(tab) for x in self.conditions)

    def add(self, condition):
        self.conditions.append(condition)

    def remove(self, condition):
        self.conditions.remove(condition)

class Condition(object):
    def __init__(self, condition_function):
        self.test = condition_function

    def evaluate(self, tab):
        return self.test(tab)

class Discounts(object):
    def __init__(self):
        self.children = []

    def calculate(self, tab):
        return sum(x.calculate(tab) for x in self.children)

    def add(self, child):
        self.children.append(child)

    def remove(self, child):
        self.children.remove(child)

class Discount(object):
    def __init__(self, test_function, discount_function):
        self.test = test_function
        self.discount = discount_function

    def calculate(self, tab):
        return sum(self.discount(item) for item in tab.items if self.test(item))

class Rule(object):
    def __init__(self, tab):
        self.tab = tab
        self.conditions = AndConditions()
        self.discounts = Discounts()

    def add_conditions(self, conditions):
        self.conditions.add(conditions)

    def add_discount(self, test_function, discount_function):
        discount = Discount(test_function, discount_function)
        self.discounts.add(discount)

    def apply(self):
        if self.conditions.evaluate(self.tab):
            return self.discounts.calculate(self.tab)

        return 0

实现解释器模式

两种类型的人使用软件:一种是对现成的产品感到满意的人,另一种是愿意修改和调整软件以更好地满足他们需求的人。解释器模式只对第二组人感兴趣,因为他们愿意花时间学习如何使用 DSL 来使软件满足他们的需求。

回到餐馆,一个老板会对一些特价菜的基本模板感到满意,比如买一送一的优惠,而另一个餐馆老板会希望调整和扩大他的优惠。

我们现在可以概括我们以前所做的 DSL 的基本实现,以创建一种将来解决这些问题的方法。

每个表达式类型都有一个类(和以前一样),每个类都有一个interpret方法。还需要一个类和一个对象来存储全局上下文。这个上下文被传递给解释流中下一个对象的interpret函数。假设解析已经发生。

解释器递归地遍历容器对象,直到找到问题的答案。

class NonTerminal(object):
    def __init__(self, expression):
        self.expression = expression

    def interpret(self):
        self.expression.interpret()

class Terminal(object):
    def interpret(self):
        pass

最后,解释器模式可以用来决定某个选项卡是否符合任何特殊条件。首先,定义 tab 和 item 类,然后是语法所需的类。然后,使用测试标签实现并测试语法中的一些句子。注意,我们对这个例子中的类型进行了硬编码,这样代码就可以运行了;通常,这些是你想存储在一些文件或数据库中的东西。

import datetime

class Rule(object):
    def __init__(self, conditions, discounts):
        self.conditions = conditions
        self.discounts = discounts

    def evaluate(self, tab):
        if self.conditions.evaluate(tab):
            return self.discounts.calculate(tab)

        return 0

class Conditions(object):
    def __init__(self, expression):
        self.expression = expression

    def evaluate(self, tab):
        return self.expression.evaluate(tab)

class And(object):
    def __init__(self, expression1, expression2):
        self.expression1 = expression1
        self.expression2 = expression2

    def evaluate(self, tab):
        return self.expression1.evaluate(tab) and self.expression2.evaluate(tab)

class Or(object):
    def __init__(self, expression1, expression2):
        self.expression1 = expression1
        self.expression2 = expression2

    def evaluate(self, tab):
        return self.expression1.evaluate(tab) or self.expression2.evaluate(tab)

class PercentageDiscount(object):
    def __init__(self, item_type, percentage):
        self.item_type = item_type
        self.percentage = percentage

    def calculate(self, tab):
        return (sum([x.cost for x in tab.items if x.item_type == self.item_type]) * self.percentage) / 100

class CheapestFree(object):
    def __init__(self, item_type):
        self.item_type = item_type

    def calculate(self, tab):
        try:
            return min([x.cost for x in tab.items if x.item_type == self.item_type])
        except:
            return 0

class TodayIs(object):
    def __init__(self, day_of_week):
        self.day_of_week = day_of_week

    def evaluate(self, tab):
        return datetime.datetime.today().weekday() == self.day_of_week.name

class TimeIsBetween(object):
    def __init__(self, from_time, to_time):
        self.from_time = from_time
        self.to_time = to_time

    def evaluate(self, tab):
        hour_now = datetime.datetime.today().hour
        minute_now = datetime.datetime.today().minute

        from_hour, from_minute = [int(x) for x in self.from_time.split(":")]
        to_hour, to_minute = [int(x) for x in self.to_time.split(":")]

        hour_in_range = from_hour <= hour_now < to_hour
        begin_edge = hour_now == from_hour and minute_now > from_minute
        end_edge = hour_now == to_hour and minute_now < to_minute

        return any(hour_in_range, begin_edge, end_edge)

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

    def evaluate(self, tab):
        week_days = [
            "Monday",
            "Tuesday",
            "Wednesday",
            "Thursday",
            "Friday",
        ]
        return datetime.datetime.today().weekday() in week_days

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

    def evaluate(self, tab):
        weekend_days = [
            "Saturday",
            "Sunday",
        ]
        return datetime.datetime.today().weekday() in weekend_days

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

class ItemIsA(object):
    def __init__(self, item_type):
        self.item_type = item_type

    def evaluate(self, item):
        return self.item_type == item.item_type

class NumberOfItemsOfType(object):
    def __init__(self, number_of_items, item_type):
        self.number = number_of_items
        self.item_type = item_type

    def evaluate(self, tab):
        return len([x for x in tab.items if x.item_type == self.item_type]) == self.number

class CustomerIsA(object):
    def __init__(self, customer_type):
        self.customer_type = customer_type

    def evaluate(self, tab):
        return tab.customer.customer_type == self.customer_type

class Tab(object):
    def __init__(self, customer):
        self.items = []
        self.discounts = []
        self.customer = customer

    def calculate_cost(self):
        return sum(x.cost for x in self.items)

    def calculate_discount(self):
        return sum(x for x in self.discounts)

class Item(object):
    def __init__(self, name, item_type, cost):
        self.name = name
        self.item_type = item_type
        self.cost = cost

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

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

class CustomerType(object):
    def __init__(self, customer_type):
        self.customer_type = customer_type

member = CustomerType("Member")
pizza = ItemType("pizza")
burger = ItemType("Burger")
drink = ItemType("Drink")

monday = DayOfTheWeek("Monday")

def setup_demo_tab():
    member_customer = Customer(member, "John")
    tab = Tab(member_customer)

    tab.items.append(Item("Margarita", pizza, 15))
    tab.items.append(Item("Cheddar Melt", burger, 6))
    tab.items.append(Item("Hawaian", pizza, 12))
    tab.items.append(Item("Latte", drink, 4))
    tab.items.append(Item("Club", pizza, 17))

    return tab

if __name__ == "__main__":
    tab = setup_demo_tab()

    rules = []

    # Members always get 15% off their total tab

    rules.append(
        Rule(
            CustomerIsA(member),
            PercentageDiscount("any_item", 15)
        )
    )

    # During happy hour, which happens from 17:00 to 19:00 weekdays, all drinks are less 10%

    rules.append(
        Rule(
            And(TimeIsBetween("17:00", "19:00"), TodayIsAWeekDay()),
            PercentageDiscount(drink, 10)
        )
    )

    # Mondays are buy one get one free burger nights

    rules.append(
        Rule(
            And(TodayIs(monday), NumberOfItemsOfType(burger, 2)),
            CheapestFree(burger)
        )
    )

    for rule in rules:
        tab.discounts.append(rule.evaluate(tab))

    print(
        "Calculated cost: {}\nDiscount applied: {}\n".format(
            tab.calculate_cost(),
            tab.calculate_discount()
        )
    )

在这一章中,你看到了解释语法和内部 DSL 的两种方法。我们查看了复合模式,然后用它来解释一家餐馆的特色菜规则。然后,我们基于这个想法,结合通用解释器模式,开发了一个更完整的解释器来测试条件并计算适用的折扣。本质上,您经历了从获取业务需求到根据 DSL 定义问题,然后用代码实现 DSL 的整个过程。

临别赠言

无论你在你的编程之旅的哪个地方,现在就决定你将努力工作以变得更好。你通过从事编程项目和进行编程挑战来做到这一点。

Jeff Bay 的一个这样的挑战可以在务实程序员出版的思想作品选集中找到。

尝试一个不平凡的项目(需要 1000 多行代码来解决的项目),遵循以下规则:

  1. 每个方法只允许一级缩进,因此循环或嵌套中没有if语句。
  2. 不允许使用关键字else
  3. 所有基本类型和字符串都必须包装在对象中——专门用于它们的用途。
  4. 集合是第一类,因此需要它们自己的对象。
  5. 不要缩写名字。如果名字太长,你可能在一个方法或类中做了不止一件事——不要这样做。
  6. 每行只允许一个对象操作符,所以object.method()可以,但是object.attribute.method()不行。
  7. 保持你的实体小(包小于 15 个对象,类小于 50 行,方法小于 5 行)。
  8. 任何类都不能有两个以上的实例变量。
  9. 不允许使用 getters、setters 或直接访问属性。

练习

  • 按照我们在为餐馆定义 DSL 时使用的相同的思维过程来定义 DSL,您可以在自己的 to-do list 应用中实现它。这个 DSL 应该允许你的程序获取像“每周三”这样的提示。。."并将它们转化为周期性的任务。还要加上日期意识和其他一些相对于其他日期或时间的时间指示。
  • 用 Python 实现前面练习中的 DSL,这样它就可以解释待办事项并提取必要的特征。
  • 使用流行的 Python web 框架之一构建一个基本的待办事项列表应用,并让它使用 DSL 解释器来解释待办事项。
  • 找到一些有趣的改进或扩展,使待办事项列表应用成为你可以每天使用的东西。

十三、迭代器模式

公共汽车的轮子转啊转,转啊转,转啊转

数据结构和算法是软件设计和开发过程中不可或缺的一部分。通常,不同的数据结构有不同的用途,对于特定的问题使用正确的数据结构可能意味着更少的工作和更高的效率。选择正确的数据结构,并将其与相关算法相匹配,将会改进您的解决方案。

由于数据结构有不同的实现,我们倾向于实现对它们进行不同操作的算法。例如,如果我们考虑一个看起来像这样的二叉树:

class Node(object):
    def __init__(self, data):
        self.data = data
        self.left = None
        self.right = None

遍历树的元素以确保您访问了每一个元素,可能如下所示:

class Node(object):
    def __init__(self, data):
        self.data = data
        self.left = None
        self.right = None

def tree_traverse(node):
    if node.left is not None:
        tree_traverse(node.left)

    print(node.data)

    if node.right is not None:
        tree_traverse(node.right)

if __name__ == "__main__":
    root = Node("i am the root")

    root.left = Node("first left child")
    root.right = Node("first right child")

    root.left.right = Node("Right child of first left child of root")

    tree_traverse(root)

相比之下,当您想要遍历一个简单的列表时:

lst = [1, 2, 3, 4, 5]

for i in range(len(lst)):
    print(lst[i])

我知道我以一种不被认为是 pythonic 的方式使用了for循环,但是如果你来自 C/C++或 Java 背景,这看起来会很熟悉。我这样做是因为我想说明如何遍历链表和二叉树,以及它们是如何产生相似结果的截然不同的过程。

作为泛型编程的倡导者和 C++标准模板库的主要设计者和实现者,亚历山大·斯捷潘诺夫花了很多时间思考泛型编程的技术,在泛型编程中,代码是根据算法编写的,这些算法对将来要定义的数据类型进行操作。他将纯数学和代数的思想与计算机科学结合起来,并得出结论,大多数算法都可以用一种称为容器的代数数据类型来定义。

通过将算法与容器的特定类型和实现分离,您可以自由地描述算法,而无需关注特定类型容器的实际实现细节。这种分离的结果是更通用、更可重用的代码。

我们希望能够遍历一个集合,而不用担心我们是在处理一个列表、一个二叉树还是其他集合。

为此,我们希望创建一个集合数据类型可以继承的接口,这将允许它一般化遍历集合内容的动作。我们通过使用以下组件来实现这一点。

首先,我们定义了一个接口,该接口定义了一个获取集合中下一个项目的函数,以及另一个提醒外部函数集合中没有元素可返回的函数。

第二,我们定义了某种可以使用接口来遍历集合的对象。这个对象叫做迭代器。

按照传统的方法,我们可以如下实现这个想法:


classic_iter.py

import abc

class Iterator(metaclass=abc.ABCMeta):

    @abc.abstractmethod
    def has_next(self): pass

    @abc.abstractmethod
    def next(self): pass

class Container(metaclass=abc.ABCMeta):

    @abc.abstractmethod
    def getIterator(self): pass

class MyListIterator(Iterator):
        def __init__(self, my_list):
            self.index = 0
            self.list = my_list.list

        def has_next(self):
            return self.index < len(self.list)

        def next(self):
            self.index += 1
            return self.list[self.index - 1]

class MyList(Container):    
    def __init__(self, *args):
        self.list = list(args)

    def getIterator(self):
        return MyListIterator(self)

if __name__ == "__main__":
    my_list = MyList(1, 2, 3, 4, 5, 6)
    my_iterator = my_list.getIterator()

    while my_iterator.has_next():
        print(my_iterator.next())

这会打印出以下结果:

1
2
3
4
5
6

这种遍历集合的想法如此普遍,以至于它有了一个名字——迭代器模式。

迭代器模式的 Python 内部实现

您可能已经猜到了,Python 使得迭代器模式的实现变得极其简单。事实上,你已经在本书中使用了迭代器。Python 中实现for循环的方式使用迭代器模式。

以这个for循环为例:

for i in range(1, 7):
  print(i)

这类似于我们在上一节中看到的迭代器模式实现。

Python 中的这种便利是通过定义迭代器协议来实现的,迭代器协议用于创建可迭代的对象,然后返回一些知道如何迭代这些可迭代对象的对象。

Python 使用两个特定的方法调用和一个引发的异常来提供整个语言的迭代器功能。

iterable 的第一个方法是__ iter__()方法,它返回一个 iterator 对象。接下来,迭代器对象必须提供一个__next__()方法;它用于返回 iterable 中的下一个元素。最后,当迭代完所有元素后,迭代器会引发一个StopIteration异常。

我们经常看到 Python 中的 iterable 集合返回自身的一个实例,因为这个类也实现了__ next__()方法并引发了StopIteration异常。类似于我们之前看到的经典实现的迭代器可能看起来像这样:

class MyList(object):    
    def __init__(self, *args):
        self.list = list(args)
        self.index = 0

    def __iter__(self):
        return self

    def __next__(self):
        try:
            self.index += 1
            return self.list[self.index - 1]
        except IndexError:
            raise StopIteration()

if __name__ == "__main__":
    my_list = MyList(1, 2, 3, 4, 5, 6)

    for i in my_list:
        print(i)

这一次,您会注意到我们使用 Python for循环来迭代列表中的元素,这是唯一可能的,因为我们的MyList类具有 Python 迭代器协议所需的函数。

如果我们要将本章开头看到的二叉树实现为 Python iterable,那么它将如下实现:


bin_tree_iterator.py

class Node(object):
    def __init__(self, data):
        self.data = data
        self.left = None
        self.right = None

class MyTree(object):
    def __init__(self, root):
        self.root = root

    def add_node(self, node):
        current = self.root

        while True:
            if node.data <= current.data:
                if current.left is None:
                    current.left = node
                    return

                else:
                    current = current.left
            else:
                if current.right is None:
                    current.right = node
                    return

                else:
                    current = current.right

    def __iter__(self):
        if self.root is None:
            self.stack = []
        else:
            self.stack = [self.root]
            current = self.root
            while current.left is not None:
                current = current.left
                self.stack.append(current)

        return self

    def __next__(self):
        if len(self.stack) <= 0:
            raise StopIteration

        while len(self.stack) > 0:
            current = self.stack.pop()
            data = current.data

            if current.right is not None:
                current = current.right
                self.stack.append(current)

                while current.left is not None:
                    current = current.left
                    self.stack.append(current)

            return data

        raise StopIteration

if __name__ == "__main__":
    tree = MyTree(Node(16))
    tree.add_node(Node(8))
    tree.add_node(Node(1))
    tree.add_node(Node(17))
    tree.add_node(Node(13))
    tree.add_node(Node(14))
    tree.add_node(Node(9))
    tree.add_node(Node(10))
    tree.add_node(Node(11))

    for i in tree:
        print(i)

我们保留了之前定义的Node类,只是现在我们添加了一个Container类,即MyTree。这个类实现了迭代器协议,因此可以在普通的 Python for循环中使用,正如我们在列表迭代器中看到的。我们还包含了一个方便的函数,允许我们通过简单地调用MyTree的树实例上的add_node方法来构建一个二叉树。这让我们忘记了树是如何实现的。向树中插入新节点的过程只是查看当前节点,如果要添加的节点的数据值小于或等于树中当前节点的数据值,则向左移动。我们一直这样做,直到找到一个空白点,新的节点被放置在这个空白点上。

为了遍历树,我们保存一个节点堆栈,然后我们开始重复推送,向左移动并将当前节点推送到堆栈上。这种情况一直持续到不再有留守儿童。然后,我们从堆栈中弹出顶部节点,并检查它是否有一个正确的子节点。如果是这样的话,右边的子节点将被推送到堆栈上,同时还有从该节点开始的树的最左边的分支。然后,我们返回从堆栈中弹出的节点的值。堆栈作为树对象的一个实例变量来维护,这样我们就能够在每次调用__next__()方法时从停止的地方继续。

这将导致打印有序序列,如下所示:

1
8
9
10
11
13
14
16
17

我们可以使用现有的 Python 函数和结构与二叉树接口,比如for循环和maxsum函数。只是为了好玩,看看对完全自定义的数据类型使用这些构造有多容易。

您已经看到for循环按预期工作;现在,我们将使用与在bin_tree_iterator.py中相同的代码,但是我们将在最后一个if语句中添加一个maxsum调用。因此,代码片段中只包含了if语句。

if __name__ == "__main__":
    tree = MyTree(Node(16))
    tree.add_node(Node(8))
    tree.add_node(Node(1))
    tree.add_node(Node(17))
    tree.add_node(Node(13))
    tree.add_node(Node(14))
    tree.add_node(Node(9))
    tree.add_node(Node(10))
    tree.add_node(Node(11))

    for i in tree:
        print(i)

    print("maximum value: {}".format(max(tree)))

    print("total of values: {}".format(sum(tree)))

如您所料,结果如下:

1
8
9
10
11
13
14
16
17
maximum value: 17
total of values: 99

这些结构中的每一个的算法的实现都与以后使用它们的数据类型完全分离。非常有趣的是,您可以使用 list comprehension 创建一个可迭代的列表对象(这几乎就像创建可迭代对象的简写),如上所述,只有if语句被更改,所以我将再次只包括if的代码和将树转换成列表所需的代码。

if __name__ == "__main__":
    tree = MyTree(Node(16))
    tree.add_node(Node(8))
    tree.add_node(Node(1))
    tree.add_node(Node(17))
    tree.add_node(Node(13))
    tree.add_node(Node(14))
    tree.add_node(Node(9))
    tree.add_node(Node(10))
    tree.add_node(Node(11))

    print([x for x in tree])

从而产生有序的值列表

[1, 8, 9, 10, 11, 13, 14, 16, 17]

列表理解很容易理解。他们接受一个函数或操作,并将其映射到一个 iterable。您还可以在迭代之后添加某种条件语句来排除某些元素。

让我们改变我们刚刚看到的理解,现在忽略所有 3 的倍数。

再一次用 if x % 6!= 0

if __name__ == "__main__":
    tree = MyTree(Node(16))
    tree.add_node(Node(8))
    tree.add_node(Node(1))
    tree.add_node(Node(17))
    tree.add_node(Node(13))
    tree.add_node(Node(14))
    tree.add_node(Node(9))
    tree.add_node(Node(10))
    tree.add_node(Node(11))

    print([x for x in tree if x % 3 != 0])

正如你所希望的,9 不在最终名单中:

[1, 8, 10, 11, 13, 14, 16, 17]

迭代工具

我相信你现在已经确信迭代器的用处了。因此,我将快速提及标准库中包含的 Itertools 包。它包含许多函数,允许您以一些有趣的方式组合和操作迭代器。这些构件取自函数式编程和 Haskell 等语言的世界,并以 pythonic 的方式重新构思。

要获得这些工具的详细列表,请阅读 Itertools 库的文档。给大家介绍库,我只想提三个功能。

第一个是chain(),它允许你一个接一个地链接多个迭代器。

import itertools

print(list(itertools.chain([1, 2, 3, 4], range(5,9), "the quick and the slow")))

这将遍历三个 iterables 中的每个值,并将它们全部转换为一个打印的列表,如下所示:

[1, 2, 3, 4, 5, 6, 7, 8, 't', 'h', 'e', ' ', 'q', 'u', 'i', 'c', 'k', ' ', 'a', 'n', 'd', ' ', 't', 'h', 'e', ' ', 's', 'l', 'o', 'w']

接下来,我们有了无限迭代器cycle(),它允许你创建一个迭代器,这个迭代器将无限循环传递给它的元素。

import itertools

cycler = itertools.cycle([1, 2, 3])
print(cycler.__next__())
print(cycler.__next__())
print(cycler.__next__())
print(cycler.__next__())
print(cycler.__next__())
print(cycler.__next__())
print(cycler.__next__())
print(cycler.__next__())

每当循环程序到达最后一个元素时,它就从头开始。

1
2
3
1
2
3
1
2

第三个也是最后一个函数是zip_longest(),它组合了一组 iterables,并在每次迭代中返回它们匹配的元素。

zip_longest example

import itertools

list1 = [1, 2, 3]
list2 = ['a', 'b', 'c']

zipped = itertools.zip_longest(list1, list2)

print(list(zipped))

结果是一组对,其中第一对包含两个列表的第一个元素,第二对包含两个列表的第二个元素,依此类推。

[(1, 'a'), (2, 'b'), (3, 'c')]

使用迭代器还可以做许多其他有趣的事情,其中许多将帮助您找到复杂问题的简单解决方案。就像 Python 标准库中包含的大多数库一样,探索和掌握 Itertools 包是值得的。

发电机功能

在我们开始研究生成器表达式之前,让我们写一个简单的脚本来演示它们是做什么的。

gen_  func.py

def gen_squares(n):
    i = 0
    while i < n:
        yield i*i
        print("next i")
        i += 1

if __name__ == "__main__":
    g = gen_squares(4)
    print(g.__next__())
    print(g.__next__())
    print(g.__next__())
    print(g.__next__())
    print(g.__next__())

请求next得到的结果是下一个方块,正如你在这里看到的:

0
next i
1
next i
4
next i
9
next i
Traceback (most recent call last):
  File "gen_func.py", line 15, in <module>
    print(g.__next__())
StopIteration

您将从输出中观察到一些有趣的事情,第一个是每次您调用函数时,输出都会发生变化。您应该注意的第二件事是,函数从刚好在yield语句下面开始执行,并继续执行,直到它再次到达yield为止。

当解释器遇到yield语句时,它记录下函数的当前状态,并返回产生的值。一旦通过__next__()方法请求了下一个值,就加载内部状态,函数从停止的地方继续运行。

生成器是简化迭代器创建的一个很好的方法,因为生成器是一个产生结果序列的函数,它遵循集合的接口,可以按照迭代器模式的 Python 实现进行迭代。对于生成器函数,Python 内部会为您处理迭代器协议。

当调用生成器函数时,它返回一个生成器对象,但是直到第一次调用__next__()方法时才开始执行。此时,函数开始执行并运行,直到达到产量。一旦生成器到达末尾,它会像迭代器一样引发同样的异常。生成器函数返回的生成器也是一个迭代器。

生成器表达式

就像我们使用列表理解作为创建迭代器的速记一样,对于生成器有一种特殊的速记,叫做生成器表达式。

请看下面的生成器表达式示例。它只是我们之前定义的生成器函数的一个替代。

g = (x*x for x in range(4))

生成器表达式可以用作某些使用迭代器的函数的参数,比如max()函数。

print(max((x*x for x in range(4))))

这印出了数字 9。

使用 Python,如果调用函数中只有一个参数,我们可以去掉生成器两边的括号。

print(max(x*x for x in range(4)))

这个更整洁一点。

临别赠言

迭代器和生成器将帮助您在探索 Python 世界时完成大量繁重的工作,并且熟悉使用它们将帮助您更快地编码。它们还将您的代码扩展到函数式编程领域,在这里您更关注于定义程序必须做什么,而不是必须如何做。

说到更快地编码,这里有一些提高开发速度的技巧。

  • 学会触摸打字。这就像能读会写一样。如果你每天都要做某件事,学会尽可能有效地去做是一个重要的力量倍增器。
  • 研究并掌握你选择的编辑器或 IDE,无论是 Vim、Emacs、Pycharm,还是你使用的任何其他工具,确保学会所有的快捷键。
  • 排除杂念,因为每次你需要切换上下文时,你将需要 30 分钟来重新找到你的最佳状态。不值得。
  • 确保你的测试运行得很快。你的测试越慢,跳过它们的诱惑就越大。
  • 花在网上搜索答案或例子的每一刻都是编码和思考时间的浪费,所以学习这个领域,掌握这门语言,减少你在论坛上的时间。
  • 偿还技术债务,就像你的生活依赖于它一样——你的幸福确实如此。

练习

  • 使用生成器和常规迭代器实现斐波那契数列的可迭代。
  • 使用生成器函数创建一个二叉树迭代器。

十四、观察者模式

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

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

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

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


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 绑定,但是由于某种原因,我们无法访问设备的内部工作。将此对象添加到我们的网络中,并定义一个虚拟接口来模拟所提供的接口。然后,为家庭中的每个状态添加自定义设置。
  • 将状态模式添加到本章的最后一段代码中,以允许您处理我们讨论的四种场景。你需要如何以不同的方式思考我们在本章中写的代码?然后,决定在这种情况下,实现状态模式会使解决方案更好还是更差。特别考虑一下维护和我们到目前为止讨论过的原则。

十九、模型-视图-控制器模式

总是,世界中的世界。——可利夫·巴克,编织世界

在您作为程序员的旅程中,您开始看到一些其他模式的出现——更像是模式的模式。就像这个。即使不是所有的程序,也是大部分由某种启动程序的外部动作组成。发起动作可能有数据,也可能没有数据,但是如果有,程序会使用这些数据,有时会持久化,有时不会。最后,你的计划对世界产生了影响。基本上就是这样;你将遇到的所有程序都有这种模式。

让我给你一个简单的例子,然后是一个你可能从一开始就看不到的例子。

第一个例子是访问者注册接收来自网站的电子邮件更新。用户如何输入信息并不重要,因为从系统的角度来看,一切都是从点击程序的POST请求开始的。该请求包含姓名和电子邮件地址。现在程序像前面的例子一样被初始化,我们有了一些额外的数据。该程序会检查电子邮件是否已经在数据库中。如果不是,它将与访问者的姓名一起保存。如果是,则不采取进一步的行动。最后,系统返回一个状态代码,警告调用程序一切正常。请注意,程序在两个地方改变了程序本身执行之外的世界,即数据库和对调用系统的响应。

我们也可以看一个游戏,游戏处于某种状态,玩家通过键盘和鼠标与游戏互动。这些外围动作被游戏引擎翻译成命令,这些命令被处理,并且游戏状态被更新,游戏状态以视觉、听觉和可能的触觉反馈的形式被传递回给玩家。流程保持不变:输入、处理、输出。

以这种方式看程序是有帮助的,因为你变得更善于询问过程中每一步发生了什么。遗憾的是,这种观点过于笼统,遗漏了太多内容,因此在实际的具体开发工作中,它并不完全有用。尽管如此,它仍然是一个有用的工具。

这本书叫做实用 Python 设计模式,而不是抽象编程模型,所以让我们实际一点。

没有完全抛弃程序的三个部分的想法,不同类型的程序对这个想法有不同的观点。游戏执行一个游戏循环,而 web 应用接受用户输入,并使用该输入和某种形式的存储数据来产生一个输出,显示在用户的浏览器窗口中。

出于本章的考虑,我们将使用一个命令行虚拟程序来演示我希望您能够理解的想法。一旦您清楚地掌握了基本思想,我们将使用几个 Python 库构建一个基本的 web 应用来实现它们,该应用可以从您的本地机器上提供 web 页面。

我们的第一个程序只是将一个名字作为参数。如果名称已经存储,它将欢迎访问者回来;否则,它会告诉来访者很高兴见到他们。

import sys

def main(name):
    try:
        with open('names.dat', 'r') as data_file:
            names = [x for x in data_file.readlines()]

    except FileNotFoundError as e:
        with open('names.dat', 'w') as data_file:
            data_file.write(name)

        names = []

    if name in names:
        print("Welcome back {}!".format(name))
    else:
        print("Hi {}, it is good to meet you".format(name))
        with open('names.dat', 'a') as data_file:
            data_file.write(name)

if __name__ == "__main__":
    main(sys.argv[1])

程序使用标准库中的sys包来公开传递给程序的参数。第一个参数是正在执行的脚本的名称。在我们的示例程序中,第一个额外的参数是一个人的名字。我们包含了一些检查,确保一些字符串确实被传递给了程序。接下来,我们检查程序以前是否遇到过这个名字,并显示相关的文本。

现在,由于整本书讨论的所有原因,我们知道这并不好。这个脚本在主函数中做了太多的事情,所以我们继续把它分解成不同的函数。

import sys
import os

def get_append_write(filename):
    if os.path.exists(filename):
        return 'a'

    return 'w'

def name_in_file(filename, name):
    if not os.path.exists(filename):
        return False

    return name in read_names(filename)

def read_names(filename):
    with open(filename, 'r') as data_file:
        names = data_file.read().split('\n')

    return names

def write_name(filename, name):
    with open(filename, get_append_write(filename)) as data_file:
            data_file.write("{}\n".format(name))

def get_message(name):
    if name_in_file('names.dat', name):
        return "Welcome back {}!".format(name)

    write_name('names.dat', name)
    return "Hi {}, it is good to meet you".format(name)

def main(name):
    print(get_message(name))

if __name__ == "__main__":
    main(sys.argv[1])

我敢肯定,您怀疑在清理这些代码方面还会有更多的工作要做,那么为什么还要再次经历这个过程呢?遵循这些步骤的原因是,我想让你知道什么时候需要解构一个函数或方法。如果所有的程序都保持在项目开始时计划的方式,就不需要这种何时以及如何将事情分成更小部分的意识。按照同样的逻辑,这个过程是渐进的。你可能会像我们一样,把程序分成不同的功能。过了一段时间,程序变得很大,现在单个文件变成了一个负担,这是一个好的迹象,表明您需要将单个文件的程序分解成单独的文件。但是你要怎么做呢?随着我们最初计划的发展,我们将会看到。

他们说永远做你自己,除非你能成为一头狮子,在这种情况下,永远做一头狮子。

因此,本着这种精神,我们现在将扩展程序来显示某些参数的特殊消息,特别是 lion。

import sys
import os

def get_append_write(filename):
    if os.path.exists(filename):
        return 'a'

    return 'w'

def name_in_file(filename, name):
    if not os.path.exists(filename):
        return False

    return name in read_names(filename)

def read_names(filename):
    with open(filename, 'r') as data_file:
        names = data_file.read().split('\n')

    return names

def write_name(filename, name):
    with open(filename, get_append_write(filename)) as data_file:
            data_file.write("{}\n".format(name))

def get_message(name):
    if name == "lion":
        return "RRRrrrrroar!"

    if name_in_file('names.dat', name):
        return "Welcome back {}!".format(name)

    write_name('names.dat', name)
    return "Hi {}, it is good to meet you".format(name)

def main(name):
    print(get_message(name))

if __name__ == "__main__":
    main(sys.argv[1])

现在,我们终于开始看到另一种模式的出现。这里我们有三种不同类型的函数,它们是我们尽可能分离关注点的结果。我们最终得到了处理返回问候的代码,将问候写入控制台的代码,以及一些处理程序流的代码,将相关请求发送到数据检索代码,获取问候,并将其发送到最终在控制台上显示它的代码。

模型-视图-控制器框架

总的来说,我们希望在一个可重用的模式中捕获上一段中讨论的模式。我们还想将开闭原则融入到代码中,为此,我们想将程序的关键部分封装在对象中。

在任何事情发生之前,我们的程序必须收到某种形式的请求。这个请求必须被解释,然后开始相关的动作。所有这些都发生在一个只为控制程序流而存在的对象内部。该对象处理请求数据、接收数据和向命令行发送响应等操作。正在讨论的对象都是关于控制的,不出所料,这类对象被称为控制器。它们是将系统结合在一起的粘合剂,通常也是大多数活动发生的地方。

这是我们之前程序中的控制函数。

controller_functions.py

def name_in_file(filename, name):
    if not os.path.exists(filename):
        return False

    return name in read_names(filename)

def get_message(name):
    if name_in_file('names.dat', name):
        return "Welcome back {}!".format(name)

    write_name('names.dat', name)
    return "Hi {}, it is good to meet you".format(name)

if __name__ == "__main__":
    main(sys.argv[1])

这些还没有封装在一个对象中,但至少它们在一个地方。现在,让我们继续进行系统的下一部分,在我们回到它们并清理这些部分之前,对功能进行分组。

一旦接收到请求,控制器决定必须对请求进行什么处理,就需要来自系统的一些数据。由于我们正在处理共享功能的代码的分组,我们现在将获取所有与数据检索有关的函数,并将它们放入一个函数文件中。通常,数据的结构化表示被称为数据模型,因此程序中处理数据的部分被称为模型。

这是我们之前程序中的模型函数。

model_functions.py

def get_append_write(filename):
    if os.path.exists(filename):
        return 'a'

    return 'w'

def read_names(filename):
    with open(filename, 'r') as data_file:
        names = data_file.read().split('\n')

    return names

def write_name(filename, name):
    with open(filename, get_append_write(filename)) as data_file:
            data_file.write("{}\n".format(name))

一旦代码被封装到一个对象中,你会发现随着程序的增长,需要不同类型的数据,每一种数据都被放入它自己的model对象中。通常,model对象最终都在各自的文件中。

剩下的是专注于向用户传递某种信息的代码。在我们的例子中,这是通过控制台的标准输出端口完成的。正如您现在已经猜到的,这被称为视图代码。

以下是我们之前程序中剩余的视图函数。

view_functions.py

def main(name):
    print(get_message(name))

我相信你已经注意到,职能的划分并不像我们希望的那样明确。理想情况下,您应该能够改变任何一组函数中的任何内容(不改变它们的接口),并且这不应该对其他模块有任何影响。

在我们进行清晰的划分并将这三类功能封装到单独的类中之前,让我们看看进入模式的元素。

控制器

控制器是模式的核心,是编组所有其他类的部分。用户与控制器交互,并通过它控制整个系统。控制器接受用户输入,处理所有的业务逻辑,从模型中获取数据,并将数据发送到视图,以转换成返回给用户的表示。

class GenericController(object):

    def __init__(self):
        self.model = GenericModel()
        self.view = GenericView()

    def handle(self, request):
        data = self.model.get_data(request)
        self.view.generate_response(data)

模型

模型处理数据:获取数据、设置数据、更新数据和删除数据。就这样。你会经常看到模特做的不止这些,这是个错误。您的模型应该是数据的程序端接口,它抽象出与数据存储直接交互的需要,允许您从基于文件的存储切换到某种键值存储或完整的关系数据库系统。

通常,模型会包含用作对象属性的字段,允许您像与任何其他对象一样与数据库进行交互,使用某种save方法将数据保存到数据存储中。

我们的GenericModel类只包含一个简单的方法来返回某种形式的数据。

class GenericModel(object):

    def __init__(self):
        pass

    def get_data(self, request):
        return {'request': request}

视图

与模型一样,您不希望视图中有任何业务逻辑。视图应该只处理传递给它的数据的输出或呈现,将其转换成某种返回给用户的格式,可以是对控制台的打印语句或对游戏玩家的 3D 呈现。输出的格式对视图的功能没有太大的影响。您还会尝试为视图添加逻辑;这是一条滑坡,不是你应该走的路。

下面是一个简单的GenericView类,您可以使用它作为构建自己的视图的基础,无论是将 JSON 返回到 HTTP 调用还是绘制图表进行数据分析。

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

    def generate_response(self, data):
        print(data)

将这一切结合在一起

最后,这里有一个完整的程序,使用模型、视图和控制器来让您了解这些元素是如何交互的。

import sys

class GenericController(object):

    def __init__(self):
        self.model = GenericModel()
        self.view = GenericView()

    def handle(self, request):
        data = self.model.get_data(request)
        self.view.generate_response(data)

class GenericModel(object):

    def __init__(self):
        pass

    def get_data(self, request):
        return {'request': request}

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

    def generate_response(self, data):
        print(data)

def main(name):
    request_handler = GenericController()
    request_handler.handle(name)

if __name__ == "__main__":
    main(sys.argv[1])

现在我们已经清楚了每个对象类应该是什么样子,让我们根据这些对象类来实现本章示例中的代码。

正如我们到目前为止所做的,我们将从Controller对象开始。

controller.py

import sys
from model import NameModel
from view import GreetingView

class GreetingController(object):

    def __init__(self):
        self.model = NameModel()
        self.view = GreetingView()

    def handle(self, request):
        if request in self.model.get_name_list():
            self.view.generate_greeting(name=request, known=True)
        else:
            self.model.save_name(request)
            self.view.generate_greeting(name=request, known=False)

def main(main):
    request_handler = GreetingController()
    Request_handler.handle(name)

if __name__ == "__main__":
    main(sys.argv[1])

接下来,我们创建Model对象,该对象从文件中检索问候语或返回标准问候语。

model.py

import os

class NameModel(object):

    def __init__(self):
        self.filename = 'names.dat'

    def _get_append_write(self):
        if os.path.exists(self.filename):
            return 'a'

        return 'w'

    def get_name_list(self):
        if not os.path.exists(self.filename):
            return False

        with open(self.filename, 'r') as data_file:
            names = data_file.read().split('\n')

        return names

    def save_name(self, name):
        with open(self.filename, self._get_append_write()) as data_file:
            data_file.write("{}\n".format(name))

最后,我们创建一个View对象来向用户显示问候。

view.py

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

    def generate_greeting(self, name, known):
        if name == "lion":
            print("RRRrrrrroar!")
            return

        if known:
            print("Welcome back {}!".format(name))
        else:
            print("Hi {}, it is good to meet you".format(name))

太好了。如果您想提出请求,请像这样启动程序:

$ python controller.py YOUR_NAME

第一次运行该脚本时,响应与预期的一样,如下所示:

Hi YOUR_NAME, it is good to meet you

在我们继续之前,我想让你明白为什么这是个好主意。假设我们增加了我们的程序,并且在某个时候我们决定根据一天中的时间来包含不同类型的问候。为此,我们可以向系统添加一个时间模型,然后系统将检索系统时间,并根据时间决定是上午、下午还是晚上。该信息与姓名数据一起发送给视图,以生成相关的问候语。

controller.py

class GreetingController(object):

    def __init__(self):
        self.name_model = NameModel()
        self.time_model = TimeModel()
        self.view = GreetingView()

    def handle(self, request):
        if request in self.name_model.get_name_list():
            self.view.generate_greeting(
                name=request,
                time_of_day=self.time_model.get_time_of_day(),
                known=True
                )
        else:
            self.name_model.save_name(request)
            self.view.generate_greeting(
                name=request,
                time_of_day=self.time_model.get_time_of_day(),
                known=False
                )

view.py

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

    def generate_greeting(self, name, time_of_day, known):
        if name == "lion":
            print("RRRrrrrroar!")
            return

        if known:
            print("Good {} welcome back {}!".format(time_of_day, name))
        else:
            print("Good {} {}, it is good to meet you".format(time_of_day, name))

models.py

class NameModel(object):

    def __init__(self):
        self.filename = 'names.dat'

    def _get_append_write(self):
        if os.path.exists(self.filename):
            return 'a'

        return 'w'

    def get_name_list(self):
        if not os.path.exists(self.filename):
            return False

        with open(self.filename, 'r') as data_file:
            names = data_file.read().split('\n')

        return names

    def save_name(self, name):
        with open(self.filename, self._get_append_write()) as data_file:
            data_file.write("{}\n".format(name))

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

    def get_time_of_day(self):
        time = datetime.datetime.now()
        if time.hour < 12:
            return "morning"
        if 12 <= time.hour < 18:
            return "afternoon"
        if time.hour >= 18:
            return "evening"

或者,我们可以使用数据库来存储问候,而不是将它们保存在不同的文件中。这一切的美妙之处在于,我们在存储方面所做的事情与系统的其他部分无关。为了说明这一事实,我们将修改我们的模型,将问候语存储在一个 JSON 文件中,而不是存储在纯文本中。

实现这个功能需要标准库中的json包。

将这种方法应用到编程中最困难的部分是,您必须决定什么去哪里。您在模型中放置了多少内容,或者在视图中进行了什么级别的处理?在这里,我喜欢胖控制器和瘦模型和视图的概念。我们希望模型只处理数据的创建、检索、更新和删除。视图应该只关心显示数据。其他的都应该放在控制器里。有时,您可能会为控制器实现助手类,这很好,但是不要让自己受到诱惑,在模型或视图中做了不应该做的事情。

一个模型做得太多的经典例子是一个模型需要另一个模型,比如一个UserProfile类需要一个User类,这样你就知道这个概要文件属于哪个用户。诱惑在于将所有信息发送给UserProfile模型的构造器,让它动态创建User实例;毕竟都是数据。

这将是一个错误,因为您将让模型控制程序的某些流程,而这些流程应该依赖于控制器及其助手类。更好的解决方案是让控制器创建User实例,然后强制它将User对象传递给UserProfile模型类的构造函数。

同样的问题也可能出现在图片的视图侧。要记住的关键是,每个对象都应该有一个责任,而且只能有一个责任。如果它向用户显示信息,它不应该对信息进行计算;如果它正在处理数据,它不应该关心如何动态地创建缺失的部分;如果它在控制流程,它就不应该关注数据是如何存储或检索的,或者数据是如何格式化或显示的。

临别赠言

您在对象中分离关注点越干净,并且您越好地隔离它们的目的,维护您的代码、更新它或者发现和修复 bug 就越容易。

在你迈向编程大师的旅程中,你应该带着的最重要的概念之一是忒修斯之船的悖论。悖论是这样陈述的:想象你有一艘船,船上有足够的木材来完全重建这艘船。船出发了,一路上,船上的每一块木材都被船上的一些存货所替代。被替换的部分被丢弃。当船到达目的地时,原来船上的一块木板都没有了。问题是,你在什么时候有了一艘新船?这艘船什么时候不再是你出发时的船了?

这和编程有什么关系?

你应该以这样一种方式构建你的程序,即你可以不断地交换程序的一些部分,而不改变代码的任何其他部分。每个时期——取决于项目的规模和范围,这可能意味着一年、五年或六个月——您都希望淘汰所有旧代码,并用更好、更优雅、更高效的代码替换它。

不断地问自己,用完全不同的东西替换你当前正在处理的代码部分会有多痛苦。如果答案是您希望在继续前进之前不需要这样的替换,那么撕掉代码,重新开始。

练习

  • 从问候程序的最后一个实现中换出视图,以在图形弹出窗口中显示其输出;您可以查看 pygame 或 pyqt,或者任何其他可用的图形包。
  • 将模型改为读写 JSON 文件,而不是平面文本文件
  • 在本地系统上安装 SQLAlchemy 和 SQLite,并换出model对象,这样它就使用数据库而不是文件系统。

二十、发布-订阅模式

大喊大叫是出版的一种形式。—玛格丽特·阿特伍德

如果你回想一下我们在第十四章中看到的观察者模式,你会记得我们有一个Observable类和一些Observer类。每当Observable对象改变其状态并被轮询是否有变化时,它会提醒所有向它注册的观察者,他们需要激活一个回调。

没有必要跳回那一章,因为我们正在讨论的代码在下面的代码片段中:

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)

尽管观察者模式允许我们将被观察的对象从了解观察它们的对象中分离出来,但是观察者对象仍然需要知道他们需要观察哪些对象。因此,与上一章相比,我们仍然有更多的耦合,在上一章中,我们能够在不需要改变控制器或模型代码的情况下改变视图代码,模型代码也是如此。即使是位于模型和视图之间的控制器,也与两者完全分离。当我们改变视图时,我们不需要更新控制器,反之亦然。

当观察者需要注册的潜在可观察类的数量有限时,我们之前处理的观察者模式工作得很好。与编程中的许多事情一样,我们经常面临不符合理想情况的挑战,因此我们需要适应。

在这一章中,我们要寻找的是一种将观察者从可观察事物中分离出来的方法。我们不希望观察者和被观察者知道彼此的任何事情。每个类及其实例都应该能够改变,而不需要等式另一端的任何改变。新的观察者和可观察对象不应该要求修改系统中其他类的代码,因为这会导致我们的代码不太灵活。为了达到这一理想状态,我们将寻找将观察者模式的一对多方法扩展到可观察对象和观察者之间的多对多关系的方法,这样一类对象不需要太了解另一类。

这些盲目的观察者将被称为发布者,而断开的观察者将被称为订阅者。

第一步可能看起来微不足道,但你很快就会意识到改变你对事物的看法是多么简单,只要改变你对它们的称呼。

class Subscriber(object):

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

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

就像我说的,这对于程序实际上能做什么没有影响,但是它确实能帮助我们以不同的方式看待事物。我们现在可以向前迈进一步,重命名这两个类中的方法,以更好地反映发布者-订阅者模型。

class Subscriber(object):

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

class Publisher(object):

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

    def subscribe(self, subscriber):
        self.subscribers.add(subscriber)

    def unsubscribe(self, subscriber):
        self.subscribers.discard(subscriber)

    def unsubscribe_all(self):
        self.subscribers = set()

    def publish(self):
        for subscriber in self.subscribers:
            subscriber.process(self)

现在很清楚,我们正在处理两个类,一个处理发布,另一个完全专注于处理某些东西。在代码片段中,还不清楚应该发布什么,应该处理什么。我们继续尝试通过添加一个可以发布和处理的Message类来清除模型定义中的任何不确定性。

class Message(object):

    def __init__(self):
        self.payload = None

class Subscriber(object):

    def process(self, message):
        print("Message: {}".format(message.payload))

class Publisher(object):

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

    def subscribe(self, subscriber):
        self.subscribers.add(subscriber)

    def unsubscribe(self, subscriber):
        self.subscribers.discard(subscriber)

    def unsubscribe_all(self):
        self.subscribers = set()

    def publish(self, message):
        for subscriber in self.subscribers:
            subscriber.process(message)

接下来,我们通过添加一个所有消息都将通过的 dispatcher 类来删除发布者和订阅者之间的最后一个链接。目标是有一个发布者发送消息的单一位置。同一位置保存了所有订户的索引。结果是发布者和订阅者的数量可以变化,而不会对系统的其余部分产生任何影响。这给了我们一个非常干净和分离的架构。

class Message(object):
    def __init__(self):
        self.payload = None

class Subscriber(object):
    def __init__(self, dispatcher):
        dispatcher.subscribe(self)

    def process(self, message):
        print("Message: {}".format(message.payload))

class Publisher(object):
    def __init__(self, dispatcher):
        self.dispatcher = dispatcher

    def publish(self, message):
        self.dispatcher.send(message)

class Dispatcher(object):
    def __init__(self):
        self.subscribers = set()

    def subscribe(self, subscriber):
        self.subscribers.add(subscriber)

    def unsubscribe(self, subscriber):
        self.subscribers.discard(subscriber)

    def unsubscribe_all(self):
        self.subscribers = set()

    def send(self, message):
        for subscriber in self.subscribers:
            subscriber.process(message)

并非所有订阅者都对来自所有发布者的所有消息感兴趣。因为整个练习的目的是将订阅者和发布者分离,所以我们不想以任何方式将它们耦合在一起。我们正在寻找的解决方案应该允许调度程序向特定的订阅者发送消息类别。我们将在邮件中添加一个主题。发布者现在将在他们发布的消息中添加一个主题,然后订阅者可以订阅特定的主题。

class Message(object):
    def __init__(self):
        self.payload = None
        self.topic = "all"

class Subscriber(object):
    def __init__(self, dispatcher, topic):
        dispatcher.subscribe(self, topic)

    def process(self, message):
        print("Message: {}".format(message.payload))

class Publisher(object):
    def __init__(self, dispatcher):
        self.dispatcher = dispatcher

    def publish(self, message):
        self.dispatcher.send(message)

class Dispatcher(object):
    def __init__(self):
        self.topic_subscribers = dict()

    def subscribe(self, subscriber, topic):
        self.topic_subscribers.setdefault(topic, set()).add(subscriber)

    def unsubscribe(self, subscriber, topic):
        self.topic_subscribers.setdefault(topic, set()).discard(subscriber)

    def unsubscribe_all(self, topic):
        self.subscribers = self.topic_subscribers[topic] = set()

    def send(self, message):
        for subscriber in self.topic_subscribers[message.topic]:
            subscriber.process(message)

def main():
    dispatcher = Dispatcher()

    publisher_1 = Publisher(dispatcher)
    subscriber_1 = Subscriber(dispatcher, 'topic1')

    message = Message()
    message.payload = "My Payload"
    message.topic = 'topic1'

    publisher_1.publish(message)

if __name__ == "__main__":
    main()

现在我们看到了订户打印的消息(如下所示)中的代码。

Message: My Payload

分布式消息发送器

从许多机器发送信息。现在我们已经清楚地实现了 PubSub 模式,让我们使用目前为止开发的思想来构建我们自己的简单消息发送器。为此,我们将使用 Python 标准库中的socket包。我们使用 XML-RPC 来处理远程连接;它允许我们调用远程过程,就好像它们是本地系统的一部分。为了让我们的 dispatcher 处理消息,我们需要将字典作为参数发送,而不是作为 Python 对象,我们就是这样做的。下面的代码说明了我们如何做到这一点。

dispatcher.py

from xmlrpc.client import ServerProxy
from xmlrpc.server import SimpleXMLRPCServer

class Dispatcher(SimpleXMLRPCServer):
    def __init__(self):
        self.topic_subscribers = dict()
        super(Dispatcher, self).__init__(("localhost", 9000))
        print("Listening on port 9000...")

        self.register_function(self.subscribe, "subscribe")
        self.register_function(self.unsubscribe, "unsubscribe")
        self.register_function(self.unsubscribe_all, "unsubscribe_all")

        self.register_function(self.send, "send")

    def subscribe(self, subscriber, topic):
        print('Subscribing {} to {}'.format(subscriber, topic))
        self.topic_subscribers.setdefault(topic, set()).add(subscriber)
        return "OK"

    def unsubscribe(self, subscriber, topic):
        print('Unsubscribing {} from {}'.format(subscriber, topic))
        self.topic_subscribers.setdefault(topic, set()).discard(subscriber)
        return "OK"

    def unsubscribe_all(self, topic):
        print('unsubscribing all from {}'.format(topic))
        self.subscribers = self.topic_subscribers[topic] = set()
        return "OK"

    def send(self, message):
        print("Sending Message:\nTopic: {}\nPayload: {}".format(message["topic"], message["payload"]))
        for subscriber in self.topic_subscribers[message.get("topic", "all")]:
            with ServerProxy(subscriber) as subscriber_proxy:
                subscriber_proxy.process(message)

        return "OK"

def main():
    dispatch_server = Dispatcher()
    dispatch_server.serve_forever()

if __name__ == "__main__":
    main()

publisher.py

from xmlrpc.client import ServerProxy

class Publisher(object):
    def __init__(self, dispatcher):
        self.dispatcher = dispatcher

    def publish(self, message):
        with ServerProxy(self.dispatcher) as dispatch:
            dispatch.send(message)

def main():
    message = {"topic": "MessageTopic", "payload": "This is an awesome payload"}
    publisher = Publisher("http://localhost:9000")
    publisher.publish(message)

if __name__ == "__main__":
    main()

subscriber.py

from xmlrpc.client import ServerProxy
from xmlrpc.server import SimpleXMLRPCServer

class Subscriber(SimpleXMLRPCServer):
    def __init__(self, dispatcher, topic):
        super(Subscriber, self).__init__(("localhost", 9001))
        print("Listening on port 9001...")
        self.register_function(self.process, "process")

        self.subscribe(dispatcher, topic)

    def subscribe(self, dispatcher, topic):
        with ServerProxy(dispatcher) as dispatch:
            dispatch.subscribe("http://localhost:9001", topic)

    def process(self, message):
        print("Message: {}".format(message.get("payload", "Default message")))
        return "OK"

def main():
    subscriber_server = Subscriber("http://localhost:9000", "MessageTopic")
    subscriber_server.serve_forever()

if __name__ == "__main__":
    main()

要运行程序并看到它的真实效果,您必须打开三个终端窗口,并在每个窗口中为您的代码激活虚拟环境。我们将在自己的窗口中运行应用的每个部分,以模拟独立系统之间的交互。下面的每个命令都假设 virtualenv 已经被激活。

窗口 1:调度员

$ python dispatcher.py

窗口 2:订户

$ python subscriber.py

窗口 3:发布者

$ python publisher.py

随着每个窗口运行它的进程,让我们在 publisher 窗口中输入一条消息,看看会发生什么。

我们看到调度程序收到了消息并将其发送给了订阅者。

Listening on port 9000...
Subscribing http://localhost:9001 to MessageTopic
127.0.0.1 - - [16/Aug/2017 20:59:06] "POST /RPC2 HTTP/1.1" 200 -
Sending Message:
Topic: MessageTopic
Payload: This is an awesome payload
127.0.0.1 - - [16/Aug/2017 20:59:09] "POST /RPC2 HTTP/1.1" 200 -

订户接收消息并打印该消息。

Listening on port 9001...
Message: This is an awesome payload
127.0.0.1 - - [16/Aug/2017 20:59:09] "POST /RPC2 HTTP/1.1" 200 -

临别赠言

PubSub 模式有许多用途,尤其是当系统扩展并发展到云中时。这种成长、扩展和进化的过程是我选择从我们在第十四章中看到的观察者模式的简单实现开始,然后通过改进设计的步骤移动到最终的完整 PubSub 实现的原因之一。这是你将在自己的项目中重复的相同的增长模式。一个简单的项目增长了一点点,随着你添加的每一点功能,你应该问自己从这本书或其他地方得到的什么想法会使这个项目在将来更容易维护。一旦你有了这样的想法,不要犹豫——改进代码。您将很快意识到,在项目的末尾花一点时间会使您在项目的未来阶段的开发时间显著缩短。

正如 Jocko Willink(海豹突击队海军 Bruser 特遣部队指挥官)喜欢说的,纪律等于自由。您应用到编码实践中的纪律将使您从维护繁琐系统的痛苦中解脱出来。您还将避免不可避免的转储和重写,这是大多数软件产品从编写第一行代码开始就要做的事情。

练习

  • 看看能否找到关于 proto 缓冲区的信息,以及如何使用它们向远程过程调用发送数据。
  • 阅读 ZeroMQ 和 RabbitMQ 等排队软件,以及芹菜等软件包。使用其中之一实现简单的聊天应用,而不是本章中的 Python 实现。
  • 就如何使用发布-订阅模式将游戏化添加到现有的“软件即服务- SaaS”应用中写一份建议书。
posted @ 2024-08-10 15:28  绝不原创的飞龙  阅读(118)  评论(0)    收藏  举报