精通Python设计模式(txt+pdf+epub+mobi电子书下载)


发布时间:2021-08-03 04:18:56

点击下载

作者:萨基斯·卡萨姆帕里斯(Sakis Kasampalis)

出版社:人民邮电出版社

格式: AZW3, DOCX, EPUB, MOBI, PDF, TXT

精通Python设计模式

精通Python设计模式试读:

译者序

在我读大学的那几年,设计模式可谓红极一时,各大公司校招面试也几乎都会考设计模式;反观现在,则似乎很少有人聊设计模式的话题。这是因为设计模式过时了吗?还是说它只是一个错误的概念?从个人这几年的开发经验来看,答案是否定的:设计模式并未过时,更不是一个错误的概念。从曾经的“红极一时”到如今的“门可罗雀”,只是说明软件开发行业以更加客观理性的态度来看待设计模式。软件开发领域的技术概念也似乎总是遵循这样的流行度变迁,最终一次又一次地证明不存在“银弹”。

正确看待设计模式的前提是明白什么是设计模式。正如本书一开始就强调的,“设计模式的本质是在已有的方案之上发现更好的方案(而不是全新发明)”。这是一种务实的态度,设计模式并非是某种高大上或者神秘的东西,而是一些常见的软件工程设计问题的最佳实践方案。

那么应该如何学习设计模式?个人认为软件开发技术的学习都应该以实践为前提,只有理解实践过程中遇到的种种问题,才能明白那些技术的本质和目的是什么,因为每种新技术都是因某个/某些问题而出现的;软件开发高手一般都反对新手一开始就一股脑地学习设计模式。有些新手学了点设计模式的理论后,甚至在软件开发过程中生搬硬套,结果适得其反。因此,软件开发人员应该在积累了一定的开发经验之后,再系统地学习设计模式,往往能事半功倍。

现在有些积累了一定开发经验的软件开发人员在谈起设计模式时一脸鄙夷。我想这也不是一种客观务实的态度。软件开发不是简单的累积代码,在实现业务功能的同时应该仔细考虑如何控制软件的复杂度。软件的复杂度分为两个层面:业务逻辑复杂度和代码实现复杂度。对同一个业务系统,不同的软件开发人员会有不同的实现,复杂度也不同;相应地,实现的易理解性、可维护性和可扩展性也不同。软件开发人员应该不断学习如何控制软件的复杂度,学习并恰当地使用设计模式是应对软件复杂度的有效方法。

然而,设计模式并非是固定不变的(如《设计模式:可复用面向对象软件的基础》一书总结的23种模式),使用不同的编程语言来编写代码,需要学习的设计模式也不一样。一方面是因为软件开发领域迅猛发展,一些新的软件工程问题也随之出现;另一方面则是因为新的语言、新的平台会把一些常见设计模式吸收为内置特性。所以,软件开发人员应以实际问题为驱动,不断更新设计模式方面的知识。

本书以Python编程语言为例,针对目前的软件开发领域,分三大类讲解了16种常用的设计模式。使用Python语言编写示例代码,我认为作者主要是考虑到Python的抽象层次高、应用范围广,读者不会被一些实现细节所干扰,从而能快速直接地掌握模式的要领。

全书始终保持务实的态度,列举了大量现实生活的例子和软件开发的例子,并为每个模式提供了完整可运行的示例代码。虽然在书中给出所有示例代码似乎没什么必要,但个人认为作者的用意是希望读者能亲自动手,照着示例代码写一遍并运行,然后看看结果,从而加强学习的效果。

虽然是示例,但作者还是坚持以地道的Python风格编写代码,以此说明不同语言和不同平台要求软件开发人员学习的设计模式也有所不同。另外,开发人员也能从示例代码中学习到一些Python语言的高级特性,所以把本书当作Python开发进阶图书也无不可。

本书是个人正式翻译的第一本书。虽然以前翻译过很多文章,其中有些译文还有一些影响,但毕竟与正式出版有些不同,所以接手本书的翻译工作,我内心是有些忐忑的。为保证翻译的质量,我将翻译过程分为以下几个阶段来进行。

(1) 大致地预读一遍全书,整体上把握原书内容。

(2) 将原书翻译成初稿,此阶段基本保证译文的正确性。

(3) 通读审校初稿,此阶段确保译文的流畅性,以及用词和逻辑的一致性。

(4) 对着译稿,翻译相关图表中的单词;整理示例代码,并确保运行无误。

希望能通过这种方式基本保证译稿的质量。不过因为个人精力有限、能力不足,译稿中可能还会存在疏漏甚至错误之处,敬请谅解。如发现有错误之处,请将问题反馈给出版社,以便在再版时更正。

另外,本书的示例代码已经保存到GitHub的一个代码库(https://github.com/youngsterxyf/mpdp-code)中,如有需要,可以下载。

因个人原因,本书推延了一段时间才得以翻译完成,感谢图灵朱巍老师的体谅。译书是一件费时费力的事情,感谢妻子郑荣的体谅和支持,也感谢公司领导贾磊和同事的支持,谢谢你们!夏永锋于上海百度研发中心前言什么是设计模式

软件工程中,设计模式是指软件设计问题的推荐方案。设计模式一般是描述如何组织代码和使用最佳实践来解决常见的设计问题。需谨记在心的一点是:设计模式是高层次的方案,并不关注具体的实现细节,比如算法和数据结构(请参考[GOF95,第13页]和网页[t.cn/RP6HFwi])。对于正在尝试解决的问题,何种算法和数据结构最优,则是由软件工程师自己把握。 如果你不了解[]中文字的含义,请暂时先跳到前言的“排版约定”部分,查看一下本书中的引用所遵循的格式。

设计模式最重要的部分可能就是它的名称。给模式起名的好处是大家相互交流时有共同的词汇(请参考[GOF95,第13页])。因此,如果你提交一些代码进行评审,同行评审者的反馈中提到“我认为这个地方你可以使用一个策略模式来代替……”,即使你不知道或不记得策略模式是什么,也可以立即去查阅。

随着编程语言的演进,一些设计模式(如单例)也随之过时,甚至成了反模式(请参考网页[t.cn/zRoxwyD]),另一些则被内置在编程语言中(如迭代器模式)。另外,也有一些新的模式诞生(比如Borg/Monostate,请参考网页[t.cn/zWOOOZC]和[t.cn/RqrKbBe])。关于设计模式的常见误解

关于设计模式有一些误解。第一个误解是,一开始写代码就应该使用设计模式。我们经常能看到开发人员纠结在代码中应该使用哪种设计模式,他们甚至都还没有先尝试一下使用自己的方式解决问题(请参考网页[t.cn/RqrJNDw]和[t.cn/RqrJl0m])。

这不仅是错误的,而且违背了设计模式的本质。设计模式是在已有的方案之上发现更好的方案(而不是全新发明)。若你一个方案都没有,又何谈找一个更好的呢?先行动起来,用你的技能尽可能漂亮地解决问题。若代码评审者没有反对意见,而你经过一段时间后还是觉得自己的方案足够漂亮灵活,那也就意味着没必要浪费时间纠结使用哪种模式。你也许还能发现更好的设计模式。谁知道呢,关键是不要为了强迫自己使用已有的设计模式而限制了你的创造力。

第二个误解是设计模式应随处使用。这会导致方案很复杂,夹杂着多余的接口和分层,而其实往往一个更简单直接的方案就足够了。设计模式并不是万能的,仅当代码确实存在坏味道、难以扩展维护时,才有使用的必要。多思考思考你不会需要它(You Aren't Gonna Need It,YAGNI,请参考网页[t.cn/SGw9Ec])和保持简单直白(Keep It Simple Stupid,KISS,请参考网页[t.cn/RqrKMW4])原则。随处使用设计模式与过早优化一样,都是误入歧途(请参考网页[t.cn/ShMKfD])。设计模式与Python

本书主要介绍Python实现的设计模式。与畅销设计模式书籍中大多使用的常见编程语言(通常是Java,请参考[FFBS04];或C++,请参考[GOF95])不同,Python支持动态类型(duck-typing),函数是一等公民,并且一些模式(例如,迭代器和修饰器)是内置特性。本书旨在演示最基本的设计模式,并非历史记载的所有模式(请参考网页[t.cn/RqrKbBe])。代码示例也使用合适的Python惯用写法(请参考网页[t.cn/hTfLt])。如果你还不熟悉Python之禅,那现在就打开Python交互模式,执行import this。Python之禅趣味十足又意义深远。本书内容

第一部分,创建型模式,介绍处理对象创建的设计模式。● 第1章,工厂模式 介绍如何使用工厂设计模式(工厂方法和抽

象工厂)来初始化对象,并说明与直接实例化对象相比,使用工

厂设计模式的优势。● 第2章,建造者模式 对于由多个相关对象构成的对象,介绍如

何简化其创建过程。● 第3章,原型模式 介绍如何通过完全复制(也就是克隆)一个

已有对象来创建一个新对象。

第二部分,结构型模式,介绍处理一个系统中不同实体(类、对象等)之间关系的设计模式。● 第4章,适配器模式 介绍如何以最小的改变实现已有代码与外

来接口(例如,一个外部代码库)的兼容。● 第5章,修饰器模式 介绍如何无需使用继承也能增强对象的功

能。● 第6章,外观模式 介绍如何创建单个入口点来隐藏系统的复杂

性。● 第7章,享元模式 介绍如何通过复用一个对象池中的对象来提

高内存利用率及应用性能。● 第8章,模型—视图—控制器模式 介绍如何避免业务逻辑与用

户界面代码的耦合,提高应用的可维护性。● 第9章,代理模式 介绍如何增加额外的保护层,提高应用的安

全性。

第三部分,行为型模式,介绍处理系统实体之间通信的设计模式。● 第10章,责任链模式 介绍如何向多个接收者发送请求。● 第11章,命令模式 介绍如何让应用能够取消已经执行的操

作。● 第12章,解释器模式 介绍如何基于Python创建一种简单的语

言,便于领域专家使用,而无需学习Python编程。● 第13章,观察者模式 介绍如何在对象发生变化时,通知已注

册的相关者。● 第14章,状态模式 介绍如何创建一个状态机以对问题进行建

模,并说明这种技术的优势。● 第15章,策略模式 介绍如何基于某些输入标准(例如,元素

大小)在程序运行期间从多个可用算法中选择一个。● 第16章,模板模式 介绍如何明确区分一个算法的通用与不通

用部分,以避免不必要的代码复制。阅读准备

书中的代码仅用Python 3编写。Python 3在很多方面与Python 2.x不兼容(请参考网页[t.cn/Rw8Ycjs])。虽然代码是使用Python 3.4.0进行测试的,但Python 3.3.0应该也可以,因为Python 3.3.0和Python 3.4.0之间并没有语法上的差别(请参考网页[t.cn/RqrK1eX])。一般来说,如果你从www.python.org下载安装最新的Python 3版本,那么运行示例代码应该不会有问题。示例代码中使用的多数模块/库是Python 3自带的。如果有示例要求安装额外的模块,在相关代码之前会给出如何安装的说明。读者对象

本书适合具备一定经验同时又对以地道的Python代码实现设计模式感兴趣的Python开发人员。使用其他语言的开发人员,如果对Python感兴趣,也能从中获益不少,但最好先阅读一些材料,了解一下Python的基本知识(请参考网页[t.cn/hTfLt]和网页[t.cn/hp20G])。排版约定

在书中,你会发现许多文本样式,用于区分不同种类的信息。以下举例说明,并解释其含义。

代码、用户输入、推特用户定位会使用等宽的代码字体,如下面的句子中所示:“我们将使用Python发行版自带的两个库(xml.etree.ElementTree和json)来处理XML和JSON。”

代码块的版式如下所示。 @property def parsed_data(self): return self.data

当希望你关注代码块中某个特别部分时,相关的行或项目会以粗体显示,如下所示。 @property def parsed_data(self): return self.data

任何命令行输入输出都按如下方式编写。>>> python3 factory_method.py

新术语和重要的单词以楷体显示。在菜单或对话框等屏幕上看到的单词会保留原英文,如“点击Next按钮转到下一屏”。 警告或重要的注意事项会这样显示。

  提示和小窍门会这样显示。

书籍引用遵循格式[作者,页码]。例如,[GOF95,第10页]是指引用GOF(《设计模式:可复用面向对象软件的基础》)一书的第10页。

Web引用遵循格式[t.cn/shortened]。可以将这些缩短的URL地址键入或复制到Web浏览器中,按Enter键后会跳转到实际的Web引用(通常也更长,也许会更丑)。例如,在Web浏览器地址栏中键入t.cn/hTfLt,按Enter键后会跳转到http://python.net/~goodger/projects/pycon/2007/idiomatic/handout.html。读者反馈

我们始终欢迎来自读者的反馈。关于本书,如果你有一些想法,无论是喜欢还是不喜欢,都可以告诉我们。我们非常重视读者反馈,因为这能帮助我们开发一些让读者充分受益的出版物。

一般的反馈,只要发送电子邮件到feedback@packtpub.com并在消息标题中提及书名即可。

如果你擅长某个主题,并有兴趣写本书或者为某本书作出贡献,请阅读作者指南www.packtpub.com/authors。客户支持

既然你购买了一本Packt出版的书籍,我们会提供很多服务,让你获得最大的购买利益。下载示例代码

凡是通过http://www.packtpub.com网站账户购买的Packt书籍,都可以在网上下载相应的示例代码文件。如果你是在其他地方购买本书的,则可以访问http://packtpub.com/support,注册之后,相关文件会直接通过电子邮件发送给你。勘误

虽然我们会全力确保书籍内容的准确性,但错误仍然在所避免。如果你在某本书中发现错误,无论是文本错误还是代码错误,都请报告给我们,对此我们将万分感激。这样不仅能消除其他读者的疑虑,也能帮助我们提升本书后续版本的质量。如果你发现任何错误,请访问http://www.packtpub.com/submit-errata进行报告,选择相应图书,单击Errata Submission Form链接,并输入勘误的详细信息。一旦勘误得到证实,我们就会接受你的提交,并将勘误上传到站点或添加到对应书名的勘误一节下面的已有勘误表中。

要查看之前提交的勘误,可以访问https://www.packtpub.com/books/content/support并在搜索框内输入书名。需要的信息会显示在Errata部分的下面。反盗版

对所有媒体来说,互联网盗版行为都是一直存在的问题。在Packt,我们严格保护版权和许可证。如果你在互联网上发现任何形式的我们出版物的非法复制品,请立即把网址或站点名称提供给我们,以便我们进行补救。

请通过copyright@packtpub.com联系我们,提供可疑盗版资料的链接。

感谢你帮助保护我们的作者,让我们能够为你提供有价值的内容。疑问解答

对于本书的任何方面,如果你有问题,可以通过question@packtpub.com联系我们,我们将尽力解决。第一部分创建型模式本部分内容第 1 章 工厂模式第 2 章 建造者模式第 3 章 原型模式第1章工厂模式

创建型设计模式处理对象创建相关的问题(请参考网页[t.cn/RqBoSiu]),目标是当直接创建对象(在Python中是通过__init__()函数实现的,请参考网页[t.cn/RqB3mDM]和[Lott14,第26页])不太方便时,提供更好的方式。1

在工厂设计模式中,客户端可以请求一个对象,而无需知道这个对象来自哪里;也就是,使用哪个类来生成这个对象。工厂背后的思想是简化对象的创建。与客户端自己基于类实例化直接创建对象相比,基于一个中心化函数来实现,更易于追踪创建了哪些对象(请参考[Eckel08,第187页])。通过将创建对象的代码和使用对象的代码解耦,工厂能够降低应用维护的复杂度(请参考[Zlobin13,第30页])。1本书中涉及的“客户端”一词是指调用方,并非网络CS结构中的C。——译者注

工厂通常有两种形式:一种是工厂方法(Factory Method),它是一个方法(或以地道的Python术语来说,是一个函数),对不同的输入参数返回不同的对象(请参考网页[t.cn/RqB1yx2]);第二种是抽象工厂,它是一组用于创建一系列相关事物对象的工厂方法(请参考[GOF95,第100页]和网页[t.cn/RqB1tZS])。1.1 工厂方法

在工厂方法模式中,我们执行单个函数,传入一个参数(提供信息表明我们想要什么),但并不要求知道任何关于对象如何实现以及对象来自哪里的细节。1.1.1 现实生活的例子

现实中用到工厂方法模式思想的一个例子是塑料玩具制造。制造塑料玩具的压塑粉都是一样的,但使用不同的塑料模具就能产出不同的外形。比如,有一个工厂方法,输入是目标外形(鸭子或小车)的名称,输出则是要求的塑料外形。下图展示的是玩具制造案例,该案例源自网站www.sourcemaking.com,请参考网页[t.cn/RqB1yx2]。1.1.2 软件的例子

Django框架使用工厂方法模式来创建表单字段。Django的forms模块支持不同种类字段(CharField、EmailField)的创建和定制(max_length、required),请参考网页[t.cn/Rqr9qD7]。1.1.3 应用案例

如果因为应用创建对象的代码分布在多个不同的地方,而不是仅在一个函数/方法中,你发现没法跟踪这些对象,那么应该考虑使用工厂方法模式(请参考[Eckel08,第187页])。工厂方法集中地在一个地方创建对象,使对象跟踪变得更容易。注意,创建多个工厂方法也完全没有问题,实践中通常也这么做,对相似的对象创建进行逻辑分组,每个工厂方法负责一个分组。例如,有一个工厂方法负责连接到不同的数据库(MySQL、SQLite),另一个工厂方法负责创建要求的几何对象(圆形、三角形),等等。

若需要将对象的创建和使用解耦,工厂方法也能派上用场。创建对象时,我们并没有与某个特定类耦合/绑定到一起,而只是通过调用某个函数来提供关于我们想要什么的部分信息。这意味着修改这个函数比较容易,不需要同时修改使用这个函数的代码(请参考[Zlobin13,第30页])。

另外一个值得一提的应用案例与应用性能及内存使用相关。工厂方法可以在必要时创建新的对象,从而提高性能和内存使用率(请参考[Zlobin13,第28页])。若直接实例化类来创建对象,那么每次创建新对象就需要分配额外的内存(除非这个类内部使用了缓存,一般情况下不会这样)。用行动说话,下面的代码(文件id.py)对同一个类A创建了两个实例,并使用函数id()比较它们的内存地址。输出中也会包含地址,便于检查地址是否正确。内存地址不同就意味着创建了两个不同的对象。class A(object): passif __name__ == '__main__': a = A() b = A() print(id(a) == id(b)) print(a, b)

在我的计算机上执行id.py,输出的内容如下所示。>>> python3 id.py False<__main__.A object at 0x7f5771de8f60> <__main__.A object at0x7f5771df2208>

注意,你执行这个代码文件看到的地址会与我看到的不一样,因为这依赖程序运行时内存的布局和分配。但结果中有一点肯定是一样的,那就是两个地址不同。在Python Read-Eval-Print Loop(REPL)模式(即交互式提示模式)下编写运行这段代码时会出现例外,但这只是交互模式特有的优化,并不常见。1.1.4 实现

数据来源可以有多种形式。存取数据的文件主要有两种分类:人类可读文件和二进制文件。人类可读文件的例子有:XML、Atom、YAML和JSON。二进制文件的例子则有SQLite使用的.sq3文件格式,及用于听音乐的.mp3文件格式。

以下例子将关注两种流行的人类可读文件格式:XML和JSON。虽然人类可读文件解析起来通常比二进制文件更慢,但更易于数据交换、审查和修改。基于这种考虑,建议优先使用人类可读文件,除非有其他限制因素不允许使用这类格式(主要的限制包括性能不可接受以及专有的二进制格式)。

在当前这个问题中,我们有一些输入数据存储在一个XML文件和一个JSON文件中,要对这两个文件进行解析,获取一些信息。同时,希望能够对这些(以及将来涉及的所有)外部服务进行集中式的客户端连接。我们使用工厂方法来解决这个问题。虽然仅以XML和JSON为例,但为更多的服务添加支持也很简单。

首先,来看一看数据文件。基于Wikipedia例子(请参考网页[t.cn/RqB1Y9F])的XML文件person.xml包含个人信息(firstName、lastName、gender等),如下所示。 John Smith 25

21 2nd Street New York NY 10021
212 555-1234 646 555-4567 male Jimy Liar 19
18 2nd Street New York NY 10021
212 555-1234 male
Patty Liar 20
18 2nd Street New York NY 10021
212 555-1234 001 452-8819 female

JSON文件donut.json来自Adobe的GitHub账号(请参考网页[t.cn/RqB1udG]),包含甜甜圈(donut)信息(type、单位价格ppu、topping等),如下所示。[ { "id": "0001", "type": "donut", "name": "Cake", "ppu": 0.55, "batters": { "batter": [ { "id": "1001", "type": "Regular" }, { "id": "1002", "type": "Chocolate" }, { "id": "1003", "type": "Blueberry" }, { "id": "1004", "type": "Devil's Food" } ] }, "topping": [ { "id": "5001", "type": "None" }, { "id": "5002" "type": "Glazed" }, { "id": "5005", "type": "Sugar" }, { "id": "5007", "type": "Powdered Sugar" }, { "id": "5006", "type": "Chocolate with Sprinkles" }, { "id": "5003", "type": "Chocolate" }, { "id": "5004", "type": "Maple" } ] }, { "id": "0002", "type": "donut", "name": "Raised", "ppu": 0.55, "batters": { "batter": [ { "id": "1001", "type": "Regular" } ] }, "topping": [ { "id": "5001", "type": "None" }, { "id": "5002", "type": "Glazed" }, { "id": "5005", "type": "Sugar" }, { "id": "5003", "type": "Chocolate" }, { "id": "5004", "type": "Maple" } ] }, { "id": "0003", "type": "donut", "name": "Old Fashioned", "ppu": 0.55, "batters": { "batter": [ { "id": "1001", "type": "Regular" }, { "id": "1002", "type": "Chocolate" } ] }, "topping": [ { "id": "5001", "type": "None" }, { "id": "5002", "type": "Glazed" }, { "id": "5003", "type": "Chocolate" }, { "id": "5004", "type": "Maple" } ] }]

我们将使用Python发行版自带的两个库(xml.etree.ElementTree和json)来处理XML和JSON,如下所示。import xml.etree.ElementTree as etreeimport json

类JSONConnector解析JSON文件,通过parsed_data()方法以一个字典(dict)的形式返回数据。修饰器property使parsed_data()显得更像一个常规的变量,而不是一个方法,如下所示。class JSONConnector: def __init__(self, filepath): self.data = dict() with open(filepath, mode='r', encoding='utf-8') as f: self.data = json.load(f) @property def parsed_data(self): return self.data

类XMLConnector解析 XML 文件,通过parsed_data()方法以xml.etree.Element列表的形式返回所有数据,如下所示。class XMLConnector: def __init__(self, filepath): self.tree = etree.parse(filepath) @property def parsed_data(self): return self.tree

函数connection_factory是一个工厂方法,基于输入文件路径的扩展名返回一个JSONConnector或XMLConnector的实例,如下所示。def connector_factory(filepath): if filepath.endswith('json'): connector = JSONConnector elif filepath.endswith('xml'): connector = XMLConnector else: raise ValueError('Cannot connect to {}'.format(filepath)) return connector(filepath)

函数connect_to()对connection_factory()进行包装,添加了异常处理,如下所示。def connect_to(filepath): factory = None try: factory = connection_factory(filepath) except ValueError as ve: print(ve) return factory

函数main()演示如何使用工厂方法设计模式。第一部分是确认异常处理是否有效,如下所示。def main(): sqlite_factory = connect_to('data/person.sq3')

接下来的部分演示如何使用工厂方法处理XML文件。XPath用于查找所有包含姓(last name)为Liar的person元素。对于每个匹配到的元素,展示其基本的姓名和电话号码信息,如下所示。 xml_factory = connect_to('data/person.xml') xml_data = xml_factory.parsed_data() liars = xml_data.findall(".//{person}[{lastName}='{}']".format('Liar')) print('found: {} persons'.format(len(liars))) for liar in liars: print('first name: {}'.format(liar.find('firstName').text)) print('last name: {}'.format(liar.find('lastName').text)) [print('phone number ({}):'.format(p.attrib['type']), p.text) for p in liar.find('phoneNumbers')]

最后一部分演示如何使用工厂方法处理JSON文件。这里没有模式匹配,因此所有甜甜圈的name、price和topping如下所示。 json_factory = connect_to('data/donut.json') json_data = json_factory.parsed_data print('found: {} donuts'.format(len(json_data))) for donut in json_data: print('name: {}'.format(donut['name'])) print('price: ${}'.format(donut['ppu'])) [print('topping: {} {}'.format(t['id'], t['type'])) for t in donut['topping']]

为便于整体理解,下面给出工厂方法实现(factory_method.py)的完整代码。import xml.etree.ElementTree as etreeimport jsonclass JSONConnector: def __init__(self, filepath): self.data = dict() with open(filepath, mode='r', encoding='utf-8') as f: self.data = json.load(f) @property def parsed_data(self): return self.dataclass XMLConnector: def __init__(self, filepath): self.tree = etree.parse(filepath) @property def parsed_data(self): return self.treedef connection_factory(filepath): if filepath.endswith('json'): connector = JSONConnector elif filepath.endswith('xml'): connector = XMLConnector else: raise ValueError('Cannot connect to {}'.format(filepath)) return connector(filepath)def connect_to(filepath): factory = None try: factory = connection_factory(filepath) except ValueError as ve: print(ve) return factorydef main(): sqlite_factory = connect_to('data/person.sq3') print() xml_factory = connect_to('data/person.xml') xml_data = xml_factory.parsed_data liars = xml_data.findall(".//{}[{}='{}']".format('person', 'lastName', 'Liar')) print('found: {} persons'.format(len(liars))) for liar in liars: print('first name: {}'.format(liar.find('firstName').text)) print('last name: {}'.format(liar.find('lastName').text)) [print('phone number ({})'.format(p.attrib['type']), p.text) for p in liar.find('phoneNumbers')] print() json_factory = connect_to('data/donut.json') json_data = json_factory.parsed_data print('found: {} donuts'.format(len(json_data))) for donut in json_data: print('name: {}'.format(donut['name'])) print('price: ${}'.format(donut['ppu'])) [print('topping: {} {}'.format(t['id'], t['type'])) for t in donut['topping']]if __name__ == '__main__': main()

该程序的输出如下所示。>>> python3 factory_method.pyCannot connect to data/person.sq3found: 2 personsfirst name: Jimylast name: Liarphone number (home): 212 555-1234first name: Pattylast name: Liarphone number (home): 212 555-1234phone number (mobile): 001 452-8819found: 3 donutsname: Cakeprice: $0.55topping: 5001 Nonetopping: 5002 Glazedtopping: 5005 Sugartopping: 5007 Powdered Sugartopping: 5006 Chocolate with Sprinklestopping: 5003 Chocolatetopping: 5004 Maplename: Raisedprice: $0.55topping: 5001 Nonetopping: 5002 Glazedtopping: 5005 Sugartopping: 5003 Chocolatetopping: 5004 Maplename: Old Fashionedprice: $0.55topping: 5001 Nonetopping: 5002 Glazedtopping: 5003 Chocolatetopping: 5004 Maple

注意,虽然JSONConnector和XMLConnector拥有相同的接口,但是对于parsed_data()返回的数据并不是以统一的方式进行处理。对于每个连接器,需使用不同的Python代码来处理。若能对所有连接器应用相同的代码当然最好,但是在多数时候这是不现实的,除非对数据使用某种共同的映射,这种映射通常是由外部数据提供者提供。即使假设可以使用相同的代码来处理XML和JSON文件,当需要支持第三种格式(例如,SQLite)时,又该对代码作哪些改变呢?找一个SQlite文件或者自己创建一个,尝试一下。

像现在这样,代码并未禁止直接实例化一个连接器。如果要禁止直接实例化,是否可以实现?试试看。 Python中的函数可以内嵌类。1.2 抽象工厂

抽象工厂设计模式是抽象方法的一种泛化。概括来说,一个抽象工厂是(逻辑上的)一组工厂方法,其中的每个工厂方法负责产生不同种类的对象(请参考[Eckel08,第193页])。1.2.1 现实生活的例子

汽车制造业应用了抽象工厂的思想。冲压不同汽车模型的部件(车门、仪表盘、车篷、挡泥板及反光镜等)所使用的机件是相同的。机件装配起来的模型随时可配置,且易于改变。从下图我们能看到汽车制造业抽象工厂的一个例子,该图由www.sourcemaking.com提供(请参考网页[t.cn/RqB1tZS])。1.2.2 软件的例子

程序包django_factory是一个用于在测试中创建Django模型的抽象工厂实现,可用来为支持测试专有属性的模型创建实例。这能让测试代码的可读性更高,且能避免共享不必要的代码,故有其存在的价值(请参考网页[t.cn/RqBBvcw])。1.2.3 应用案例

因为抽象工厂模式是工厂方法模式的一种泛化,所以它能提供相同的好处:让对象的创建更容易追踪;将对象创建与使用解耦;提供优化内存占用和应用性能的潜力。

这样会产生一个问题:我们怎么知道何时该使用工厂方法,何时又该使用抽象工厂?答案是,通常一开始时使用工厂方法,因为它更简单。如果后来发现应用需要许多工厂方法,那么将创建一系列对象的过程合并在一起更合理,从而最终引入抽象工厂。

抽象工厂有一个优点,在使用工厂方法时从用户视角通常是看不到的,那就是抽象工厂能够通过改变激活的工厂方法动态地(运行时)改变应用行为。一个经典例子是能够让用户在使用应用时改变应用的观感(比如,Apple风格和Windows风格等),而不需要终止应用然后重新启动(请参考[GOF95,第99页])。1.2.4 实现

为演示抽象工厂模式,我将重新使用Python 3 Patterns & Idioms(Bruce Eckel著)一书中的一个例子(请参考[Eckel08,第193页]),它是我个人最喜欢的例子之一。想象一下,我们正在创造一个游戏,或者想在应用中包含一个迷你游戏让用户娱乐娱乐。我们希望至少包含两个游戏,一个面向孩子,一个面向成人。在运行时,基于用户输入,决定该创建哪个游戏并运行。游戏的创建部分由一个抽象工厂维护。

从孩子的游戏说起,我们将该游戏命名为FrogWorld。主人公是一只青蛙,喜欢吃虫子。每个英雄都需要一个好名字,在我们的例子中,这个名字在运行时由用户给定。方法interact_with()用于描述青蛙与障碍物(比如,虫子、迷宫或其他青蛙)之间的交互,如下所示。class Frog: def __init__(self, name): self.name = name def __str__(self): return self.name def interact_with(self, obstacle): print('{} the Frog encounters {} and {}!'.format(self, obstacle, obstacle.action()))

障碍物可以有多种,但对于我们的例子,可以仅仅是虫子。当青蛙遇到一只虫子,只支持一种动作,那就是吃掉它!class Bug: def __str__(self): return 'a bug' def action(self): return 'eats it'

类FrogWorld是一个抽象工厂,其主要职责是创建游戏的主人公和障碍物。区分创建方法并使其名字通用(比如,make_character()和make_obstacle()),这让我们可以动态改变当前激活的工厂(也因此改变了当前激活的游戏),而无需进行任何代码变更。在一门静态语言中,抽象工厂是一个抽象类/接口,具备一些空方法,但在Python中无需如此,因为类型是在运行时检测的(请参考[Eckel08,第195页]和网页[t.cn/h47Rs9]),如下所示。class FrogWorld: def __init__(self, name): print(self) self.player_name = name def __str__(self): return '\n\n\t------ Frog World-------' def make_character(self): return Frog(self.player_name) def make_obstacle(self): return Bug()

WizardWorld游戏也类似。在故事中唯一的区别是男巫战怪兽(如兽人)而不是吃虫子!class Wizard: def __init__(self, name): self.name = name def __str__(self): return self.name def interact_with(self, obstacle): print('{} the Wizard battles against {} and {}!'.format(self, obstacle, obstacle.action()))class Ork: def __str__(self): return 'an evil ork' def action(self): return 'kills it'class WizardWorld: def __init__(self, name): print(self) self.player_name = name def __str__(self): return '\n\n\t------ Wizard World -------' def make_character(self): return Wizard(self.player_name) def make_obstacle(self): return Ork()

类GameEnvironment是我们游戏的主入口。它接受factory作为输入,用其创建游戏的世界。方法play()则会启动hero和obstacle之间的交互,如下所示。class GameEnvironment: def __init__(self, factory): self.hero = factory.make_character() self.obstacle = factory.make_obstacle() def play(self): self.hero.interact_with(self.obstacle)

函数validate_age()提示用户提供一个有效的年龄。如果年龄无效,则会返回一个元组,其第一个元素设置为False。如果年龄没问题,元素的第一个元素则设置为True,但我们真正关心的是元素的第二个元素,也就是用户提供的年龄,如下所示。def validate_age(name): try: age = input('Welcom {}. How old are you? '.format(name)) age = int(age) except ValueError as err: print("Age {} is invalid, please try again...".format(age)) return (False, age) return (True, age)

最后一个要点是main()函数,该函数请求用户的姓名和年龄,并根据用户的年龄决定该玩哪个游戏,如下所示。def main(): name = input("Hello, What's your name? ") valid_input = False while not valid_input: valid_input, age = validate_age(name) game = FrogWorld if age < 18 else WizardWorld environment = GameEnvironment(game(name)) environment.play()

抽象工厂实现的完整代码(abstract_factory.py)如下所示。class Frog: def __init__(self, name): self.name = name def __str__(self): return self.name def interact_with(self, obstacle): print('{} the Frog encounters {} and {}!'.format(self, obstacle, obstacle.action()))class Bug: def __str__(self): return 'a bug' def action(self): return 'eats it'class FrogWorld: def __init__(self, name): print(self) self.player_name = name def __str__(self): return '\n\n\t------ Frog World -------' def make_character(self): return Frog(self.player_name) def make_obstacle(self): return Bug()class Wizard: def __init__(self, name): self.name = name def __str__(self): return self.name def interact_with(self, obstacle): print('{} the Wizard battles against {} and {}!'.format(self, obstacle, obstacle.action()))class Ork: def __str__(self): return 'an evil ork' def action(self): return 'kills it'class WizardWorld: def __init__(self, name): print(self) self.player_name = name def __str__(self): return '\n\n\t------ Wizard World -------' def make_character(self): return Wizard(self.player_name) def make_obstacle(self): return Ork()class GameEnvironment: def __init__(self, factory): self.hero = factory.make_character() self.obstacle = factory.make_obstacle() def play(self): self.hero.interact_with(self.obstacle)def validate_age(name): try: age = input('Welcome {}. How old are you? '.format(name)) age = int(age) except ValueError as err: print("Age {} is invalid, please try again...".format(age)) return (False, age) return (True, age)def main(): name = input("Hello. What's your name? ") valid_input = False while not valid_input: valid_input, age = validate_age(name) game = FrogWorld if age < 18 else WizardWorld environment = GameEnvironment(game(name)) environment.play()if __name__ == '__main__': main()

该程序的一个样例输出如下所示。>>> python3 abstract_factory.pyHello. What's your name? NickWelcome Nick. How old are you? 17 ------ Frog World -------Nick the Frog encounters a bug and eats it!

来尝试扩展一下这个游戏使其更完整吧。你可以随意添加障碍物、敌人以及其他任何想要的东西。1.3 小结

本章中,我们学习了如何使用工厂方法和抽象工厂设计模式。两种模式都可以用于以下几种场景:(a)想要追踪对象的创建时,(b)想要将对象的创建与使用解耦时,(c)想要优化应用的性能和资源占用时。场景(c)在本章中并未详细说明,你也许可以将其作为一个练习。

工厂方法设计模式的实现是一个不属于任何类的单一函数,负责单一种类对象(一个形状、一个连接点或者其他对象)的创建。我们看到工厂方法是如何与玩具制造相关联的,提到Django是如何将其用于创建不同表单字段的,并讨论了其他可能的应用案例。作为示例,我们实现了一个工厂方法,提供了访问XML和JSON文件的能力。

抽象工厂设计模式的实现是同属于单个类的许多个工厂方法用于创建一系列种类的相关对象(一辆车的部件、一个游戏的环境,或者其他对象)。我们提到抽象工厂如何与汽车制造业相关联,Django程序包django_factory是如何利用抽象工厂创建干净的测试用例,并学习了抽象工厂的应用案例。作为抽象工厂实现的示例,我们完成了一

试读结束[说明:试读内容隐藏了图片]

下载完整电子书


相关推荐

最新文章


© 2020 txtepub下载