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


发布时间:2020-05-21 00:04:29

点击下载

作者:(法)卡蒙·阿耶娃(Kamon Ayeva), (荷) 萨基斯·卡萨姆帕利斯(Sakis Kasampalis)

出版社:人民邮电出版社有限公司

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

精通Python设计模式(第2版)

精通Python设计模式(第2版)试读:

前言

Python是一种面向对象的脚本语言,应用十分广泛。在软件工程领域,设计模式意为解决软件设计问题的方案。虽然设计模式的概念已经存在了一段时间,但它仍是软件工程领域的热门话题。设计模式能为软件开发人员提供优质的信息源,以解决他们经常碰到的问题。

本书将介绍各种设计模式,并辅以现实生活中的例子进行讲解。你将掌握Python编程的底层细节与概念,与此同时,你并不需要关注Java与C++中对相同问题的常用解法。你也会阅读到有关修改代码、最佳实践、系统架构及其设计等方面的章节。

本书将会帮助你学习设计模式的核心概念,并用其解决软件设计问题。我们将着重讨论“四人组”(GoF,Gang of Four)的设计模式——一些用于解决日常问题的设计模式的统称。它们能通过有效的响应式模式,帮助你构建有弹性、可伸缩、稳健的应用程序,并将你的编程技能提升至新的高度。阅读完本书后,你将能高效地开发应用,并解决常见的问题。同时,你也能够轻松地处理任何规模的可伸缩、可维护的项目。读者对象

本书适合中级Python开发者阅读。没有设计模式相关知识的读者同样可以畅快地阅读本书。本书内容

第1章“工厂模式”介绍如何使用工厂设计模式(工厂方法和抽象工厂)来初始化对象,并说明相较于直接实例化对象,使用工厂设计模式的优势。

第2章“建造者模式”对于由多个相关对象构成的对象,介绍如何简化其创建过程。

第3章“其他创建型模式”介绍如何用一些技巧解决其他对象创建问题,如使用原型模式,通过完全复制(也就是克隆)一个已有对象来创建一个新对象。你也会了解到单例模式。

第4章“适配器模式”介绍如何以最小的改变实现现有代码与外来接口(例如外部代码库)的兼容。

第5章“装饰器模式”介绍如何在不使用继承的情况下增强对象的功能。

第6章“桥接模式”介绍如何将一个对象的实现细节从其继承结构中暴露给其他对象的继承结构。这一章鼓励你进行组合而非继承。

第7章“外观模式”介绍如何创建单个入口点来隐藏系统的复杂性。

第8章“其他结构型模式”介绍享元模式、MVC(Model-View-Controller,模型-视图-控制器)模式与代理模式。享元模式通过复用对象池中的对象来提高内存利用率及应用性能。MVC模式用于桌面与Web应用开发,通过避免业务逻辑与用户界面代码的耦合,提高应用的可维护性。代理模式通过提供一个特殊对象作为其他对象的代理来控制对其他对象的访问,以降低复杂性,增强应用性能。

第9章“职责链模式”介绍另一种提高应用程序可维护性的技巧,其通过避免业务逻辑与用户界面代码的耦合,提高应用的可维护性。

第10章“命令模式”介绍如何将撤销、复制、粘贴等操作封装成对象,从而使指令的调用与执行解耦。

第11章“观察者模式”介绍如何向多个接收者发送指令。

第12章“状态模式”介绍如何创建一个状态机以对问题进行建模,并说明这种技术的优势。

第13章“其他行为型模式”介绍一些其他的高级编程技巧,包括如何基于Python创建一种简单的语言。领域专家可以使用这种语言,而不必学习Python。

第14章“响应式编程中的观察者模式”介绍如何在状态发生变化时,向已注册的相关者发送数据流与事件。

第15章“微服务与面向云的模式”介绍一些系统设计模式,其对于当今日益广泛使用的云原生应用与微服务架构十分重要。面向微服务的框架、容器和其他技术可将应用划分为功能性和技术性服务,以实现维护和部署的独立。人们越来越依赖远程服务作为应用程序的一部分(如API),这为重试机制提供了使用场景。在这些场景下,请求有可能失败,但如果多次重复请求,成功的概率就会增大。作为容错重试的补充,你将会学到如何使用断路器,这样在子系统发生故障之时不至于摧毁整个系统。在重度依赖从数据存储中获取数据的应用程序之中,使用旁路缓存模式能够通过缓存从数据存储中读取数据,从而提升性能。这种模式可以用于从数据存储中读取数据和向数据存储更新数据。最后,这一章将介绍节流模式,这一概念基于限速,或者说替代技术。你可以控制用户使用API或服务的方式,并确保你的服务不因某个特定的租户而过载。如何充分利用本书● 使用最新版本的Windows、Linux或macOS。● 安装Python 3.6。同时,了解Python 3中的高级语法与新语法也

十分有用。你可能还需要了解如何编写符合Python规范的代码。

为此,你可以在互联网上查找相关问题的资源。● 在你的计算机上安装并使用Docker,以简单地安装并运行第15

章示例需要的RabbitMQ服务器。如果你选择使用Docker安装方

法——打包为容器的许多服务器软件和服务愈发需要Docker安

装方法,可以通过https://hub.docker.com/_/rabbitmq/和https://

docs.nameko.io/en/stable/installation.html找到有用的信息。下载示例代码

你可以从www.PacktPub.com下载本书的示例代码文件。如果你在其他地方购买了本书,可以访问www.packtpub.com/support并注册,这些文件将直接通过电子邮件发送给你。

你可以通过以下步骤下载代码文件:● 在www.packtpub.com登录或注册;● 选择SUPPORT标签;● 点击Code Downloads & Errata;● 在搜索框输入书名并遵循屏幕上的指示。

下载完文件后,确保使用如下软件的最新版本来解压或提取文件夹。● Windows:WinRAR/7-Zip● Mac:Zipeg/iZip/UnRarX● Linux:7-Zip/PeaZip

本书的代码包也托管在GitHub上,地址为https://github.com/PacktPublishing/Mastering-Python-Design-Patterns-Second-Edition。如果代码更新了,现有的GitHub仓库上也会进行更新。

你还可以在https://github.com/PacktPublishing/上下载我们丰富的图书和视频中的其他代码包。来看看吧!排版约定

本书中使用了许多文本样式。

文本中的代码、数据库表名等采用等宽字体。例如:“在Musician类中,主要动作是由play()方法执行的。”

代码块的格式如下:class Musician: def __init__(self, name): self.name = name def __str__(self): return f'the musician {self.name}' def play(self): return 'plays music'

新术语、重点强调的内容,或你在屏幕上看到的内容用黑体字表示。例如,出现在文本中的菜单或对话框中的单词。例如:“远程代理充当一个对象的本地表示,该对象实际上位于不同的地址空间(例如,网络服务器)中。” 此图标表示警告或重要的注释。 此图标表示提示和技巧。联系我们

我们始终欢迎读者的反馈。

一般反馈:发邮件到feedback@packtpub.com,并在邮件主题中注明书名。如果你对本书的任何方面有任何疑问,请通过questions@packtpub.com联系我们。

勘误:虽然我们已经竭尽全力确保内容的准确性,但错误在所难免。如果你在本书中发现了错误,请向我们报告,我们将不胜感激。请访问www.packtpub.com/submit-errata,选择你的书名,点击勘误提交表单链接,并输入详细信息。1

1本书中文版勘误请到http://ituring.cn/book/2680查看和提交。——编者注

反盗版:如果你在互联网上看到我们作品的任何形式的非法复制品,如果能向我们提供地址或网站名称,我们将不胜感激。请通过copyright@packtpub.com与我们联系。

成为作者:如果你有擅长的专题,并且对图书写作或出版感兴趣,请访问authors.packtpub.com。评论

请留下评论。你阅读并使用本书之后,为何不在购买它的网站上留下评论呢?首先,潜在的读者可以看到并参考你的公正意见,从而做出是否购买的决定。其次,Packt出版社可以了解你对我们产品的看法。最后,作者也可以看到你对他们的书的反馈。谢谢你!

更多关于Packt的信息,请访问packtpub.com。电子书

扫描如下二维码,即可购买本书中文电子版。第 1 章 工厂模式

设计模式是可复用的编程解决方案,在各种现实场景中被广泛使用,并且已被证实能够产生预期的结果。它们在程序员中广为流传,并与时俱进。设计模式的流行得益于Erich Gamma、Richard Helm、Ralph Johnson与John Vlissides合著的《设计模式:可复用面向对象软件的基础》(后面简称《设计模式》)一书。 “四人组”:这本由Erich Gamma、Richard

Helm、Ralph Johnson与John Vlissides合著的书又被简称为“四人组”书(还有一种更简洁的形式——GoF书)。

以下是一段有关设计模式的描述,引自《设计模式》一书:设计模式针对面向对象系统中重复出现的设计问题,提

出一个通用的设计方案,并予以系统化的命名和动机解释。

它描述问题,提出解决方案,指出何时适用此方案,并说明

方案的效果。它同时也提供实现代码的提示与示例。该解决

方案是用以解决该问题的一组通用的类和对象,经过定制和

实现就可用来解决特定上下文中的问题。

面向对象编程中有多种设计模式可以使用,具体使用哪种,取决于问题类型或者解决方案类型。在《设计模式》中,“四人组”向我们呈现了23种设计模式,并分为3类:创建型、结构型和行为型。

创建型设计模式是本书将要介绍的第一种类型。我们将通过本章、第2章和第3章来阐述。这些模式对应于对象创建过程的不同方面。它们的目的是在不便直接创建对象的时候(如在Python中使用__init__()函数),提供更好的替代方案。 查看https://docs.python.org/3/tutorial/classes.html以

了解对象类和特殊的__init__()函数。Python用它们来创建新

的类实例。

我们将从工厂设计模式入手,它是《设计模式》一书中的第一个创建型设计模式。在工厂模式中,客户端(意为调用后文所提及对象的代码)在不知道对象来源(即不知道该对象是用哪个类产生的)的情况下,要求创建一个对象。工厂模式背后的思想是简化对象的创建过程。与客户端直接使用类实例化来创建对象相比,使用一个中心函数来创建对象显然更容易追踪。通过将创建对象的代码与使用对象的代码解耦,工厂模式能够降低维护应用的复杂度。

工厂模式通常有两种形式:一种是工厂方法,它是一个方法(或以地道的Python术语来说,是一个函数),针对不同的输入参数返回不同的对象;另一种是抽象工厂,它是一组用于创建一系列相关对象的工厂方法。

本章将讨论:● 工厂方法● 抽象工厂1.1 工厂方法

工厂方法基于单一的函数来处理对象创建任务。执行工厂方法、传入一个参数以提供体现意图的信息,就可以创建想要的对象。

有趣的是,使用工厂方法并不要求知道对象的实现细节及其来源。1.1.1 现实生活中的例子

现实生活中使用工厂方法的一个例子是塑料玩具制造。用于制造塑料玩具的材料都是相同的,但使用不同的塑料模具能生产出不同的玩具(不同形象或形状)。这就像有一个工厂方法,输入是所期望玩具的名称(例如,鸭子或小车),输出(成型后)则是所需的塑料玩具。

在软件世界,Django框架使用工厂方法来创建表单字段。Django的forms模块支持创建不同种类的字段(如CharField和EmailField等),其部分行为可以通过max_length或required(j.mp/djangofac)等属性来定制。例如,在下面这段代码中,开发者创建了一个表单(PersonForm表单包括name与birth_date字段)作为Django应用UI代码的一部分:from django import formsclass PersonForm(forms.Form): name = forms.CharField(max_length=100) birth_date = forms.DateField(required=False)1.1.2 用例

如果你发现,创建对象的代码分布在许多不同的地方,而不是在单一的函数/方法中,导致难以跟踪应用中创建的对象,这时就应该考虑使用工厂方法模式了。工厂方法将对象创建过程集中化,使得追踪对象变得更容易。注意,创建多个工厂方法完全没有问题,实践中也通常这么做。每个工厂方法从逻辑上将具有相同点的对象的创建过程划分为一组。例如,一个工厂方法负责连接到不同的数据库(MySQL、SQLite),另一个工厂方法负责创建所要求的几何对象(圆形、三角形),等等。

要将对象的创建与使用解耦,工厂方法也十分有用。创建对象时,我们没有与某个特定的类耦合/绑定到一起,而只是通过调用某个函数来提供关于自身需求的部分信息。这意味着修改函数十分容易,而且不需要同时修改使用这个函数的代码。

另一个值得一提的用例与提升应用程序的性能以及内存使用率有关。工厂方法仅在绝对必要时才创建新的对象,以提升性能与内存使用率。当直接通过实例化类来创建对象时,每次创建一个新的对象都会分配额外的内存(除非这个类使用了内部缓存,多数情况下并非如此)。我们能够看到,在实践中,下列代码(id.py文件)创建了两个都属于类A的实例,并使用id()函数比较其内存地址。它们的地址都打印在输出中以便我们观察。内存地址不同意味着创建了两个不同的对象。class A: passif __name__ == '__main__': a = A() b = A() print(id(a) == id(b)) print(a, b)

在我的计算机上执行python id.py命令,输出了如下结果:False<__main__.A object at 0x7f5771de8f60> <__main__.A object at 0x7f5771df2208>

注意,你执行该文件得到的地址与我的并不相同,因为它们依赖于实时的内存布局与分配。但结果必然相同——两个地址应该不同。如果你在Python Read-Eval-Print-Loop(REPL,交互式解释器,或简单而言,交互式对话框)中编写并执行代码,可能会出现例外,但那只是一个REPL特有的优化,一般不会发生。1.1.3 工厂方法的实现

数据通常以多种形式呈现。存取数据的文件类型主要有两种:人类可读文件与二进制文件。人类可读文件的例子有XML、RSS/Atom、YAML和JSON等。二进制文件则包括SQLite使用的.sq3文件,以及用于听音乐的.mp3音频文件。

本案例将重点阐述两种流行的人类可读文件——XML和JSON。通常来说,虽然解析人类可读文件要比解析二进制文件慢,但人类可读文件能简化数据交换、检查与修改的过程。因此,建议你使用人类可读文件,除非存在其他限制因素(主要包括不可接受的低性能与专有的二进制格式)。

本例中,我们有一些输入数据,它们被存储在一个XML文件和一个JSON文件之中。我们希望将其解析,并获取一些信息。同时,我们希望集中客户端与那些(以及未来所有的)外部服务的联系。我们将使用工厂方法来解决这个问题。虽然本例只关注XML与JSON,但添加对其他服务的支持也十分简单。

首先,观察这个数据文件。

JSON文件movies.json是GitHub上的一个例子。它是一个包含美国电影信息的数据集合(如标题、年份、导演名、体裁,等等)。实际上,这是一个非常大的文件,这里呈现的只是它的摘录。我们将其简化以便阅读,用于展示其文件结构。[ {"title":"After Dark in Central Park", "year":1900, "director":null, "cast":null, "genre":null}, {"title":"Boarding School Girls' Pajama Parade", "year":1900, "director":null, "cast":null, "genre":null}, {"title":"Buffalo Bill's Wild West Parad", "year":1900, "director":null, "cast":null, "genre":null}, {"title":"Caught", "year":1900, "director":null, "cast":null, "genre":null}, {"title":"Clowns Spinning Hats", "year":1900, "director":null, "cast":null, "genre":null}, {"title":"Capture of Boer Battery by British", "year":1900, "director":"James H. White", "cast":null, "genre":"Short documentary"}, {"title":"The Enchanted Drawing", "year":1900, "director":"J. Stuart Blackton", "cast":null,"genre":null}, {"title":"Family Troubles", "year":1900, "director":null, "cast":null, "genre":null}, {"title":"Feeding Sea Lions", "year":1900, "director":null, "cast":"Paul Boyton", "genre":null}]

XML文件person.xml基于维基百科上的一个例子(j.mp/wikijson)。它包含许多个人信息(如名、姓、性别等)。

(1) 我们以一个名为persons的XML容器的闭合标签作为开始。

(2) 展示一个人的数据的XML元素。 John Smith 25

21 2nd Street New York NY 10021
212 555-1234 646 555-4567 male

(3) 展示另一个人数据的XML元素。 Jimy Liar 19

18 2nd Street New York NY 10021
212 555-1234 male

(4) 展示第三个人数据的XML元素。 Patty Liar 20

18 2nd Street New York NY 10021
212 555-1234 001 452-8819 female

(5) 最后,闭合这个XML容器。

我们将使用两个库——json和xml.etree.ElementTree。它们是Python发行版的一部分,用于解析JSON与XML。import jsonimport xml.etree.ElementTree as etree

类JSONDataExtractor用于解析JSON文件,它有一个parsed_data()方法,返回一个包含所有数据的字典(dict)。装饰器property用于使parsed_data()变得更像一个普通的属性而非方法。代码如下:class JSONDataExtractor: 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

类XMLDataExtractor用于解析XML文件。它有一个parsed_data()方法,返回由xml.etree.Element组成的、包含所有数据的列表。代码如下:class XMLDataExtractor: def __init__(self, filepath): self.tree = etree.parse(filepath) @property def parsed_data(self): return self.tree

函数dataextraction_factory()是一个工厂方法。它根据输入文件的扩展名,返回一个JSONDataExtractor或XMLDataExtractor的实例。代码如下:def dataextraction_factory(filepath): if filepath.endswith('json'): extractor = JSONDataExtractor elif filepath.endswith('xml'): extractor = XMLDataExtractor else: raise ValueError('Cannot extract data from {}'.format(filepath)) return extractor(filepath)

函数extract_data_from()是dataextraction_factory()的一个装饰器。它增加了异常处理机制。代码如下:def extract_data_from(filepath): factory_obj = None try: factory_obj = dataextraction_factory(filepath) except ValueError as e: print(e) return factory_obj

函数main()展示了如何使用工厂方法。第一部分确保了异常处理机制的有效性。代码如下:def main(): sqlite_factory = extract_data_from('data/person.sq3') print()

第二部分展示了如何使用工厂方法处理JSON文件。(在数据非空的情况下)该解析方法能够展示电影的标题、年份、导演姓名以及体裁。代码如下:json_factory = extract_data_from('data/movies.json')json_data = json_factory.parsed_dataprint(f'Found: {len(json_data)} movies')for movie in json_data: print(f"Title: {movie['title']}") year = movie['year'] if year: print(f"Year: {year}") director = movie['director'] if director: print(f"Director: {director}") genre = movie['genre'] if genre: print(f"Genre: {genre}") print()

最后一部分展示了如何使用工厂方法处理XML文件。Xpath用于寻找所有姓为Liar的person元素(使用liars = xml_data.findall(f".//person[lastName='Liar']"))。对于每一个匹配的人,将展示其基本姓名与电话号码信息:xml_factory = extract_data_from('data/person.xml')xml_data = xml_factory.parsed_dataliars = xml_data.findall(f".//person[lastName='Liar']")print(f'found: {len(liars)} persons')for liar in liars: firstname = liar.find('firstName').text print(f'first name: {firstname}') lastname = liar.find('lastName').text print(f'last name: {lastname}') [print(f"phone number ({p.attrib['type']}):", p.text) for p in liar.find('phoneNumbers')] print()

下面是一份代码实现的总结(你可以在factory_method.py文件中找到代码)。

(1) 导入所需的模块(json和ElementTree)。

(2) 定义JSON数据提取器类(JSONDataExtractor)。

(3) 定义XML数据提取器类(XMLDataExtractor)。

(4) 添加工厂函数dataextraction_factory(),以获得正确的数据提取器类。

(5) 添加处理异常的装饰器函数extract_data_from()。

(6) 最终,添加main()函数,并使用Python传统的命令行方式调用该函数。main函数的要点如下。● 尝试从SQL文件(data/person.sq3)中提取数据,以展示异常处

理的方式。● 从JSON文件中提取数据并解析出结果。● 从XML文件中提取数据并解析出结果。

调用python factory_method.py命令将得到以下输出(对于不同的输入,输出也不同)。

首先,当你试图访问一个SQLite(.sq3)文件时,会出现如下这样一条异常消息。

其次,处理movies文件(JSON)时,你会获得如下结果。

最后,处理person XML文件,并查找姓为Liar的人时,会出现如下结果。

注意,虽然JSONDataExtractor和XMLDataExtractor有相同的接口,但是它们处理parsed_data()返回值的方式并不一致。每个数据解析器必须与不同的Python代码相配套。虽然能对所有的提取器使用相同的代码听起来很美妙,但这在大多数情况下并不符合实际,除非我们使用的数据具有相同的映射,而这些数据常常由外部数据供应商提供。假设你能使用相同的代码来处理XML和JSON文件,那么又需要做何改变以支持第三种格式呢?SQLlite怎么样?找一个SQLite文件,或者自己创建一个并尝试一下。1.2 抽象工厂

抽象工厂设计模式是一种一般化的工厂方法。总的来说,一个抽象工厂是一些工厂方法的(逻辑)集合,其中每一个工厂方法负责生成一种不同的对象。

我们将讨论一些例子、用例,以及一种可能的实现。1.2.1 现实生活中的例子

抽象工厂常用于汽车制造。工厂使用相同的机械来冲压不同车型的零件(门、仪表盘、发动机盖、挡泥板和后视镜)。机械组装出的模型是可配置的,易于随时更改。

在软件分类中,factory_boy软件包提供一个抽象工厂的实现,用于在测试中创建Django模型。它用于创建支持测试专用属性的模型实例。这很重要,因为通过这种方法,你的测试将变得可读,并避免共享不必要的代码。 Django模型是一些特殊的类。这些类被Django框架

用来帮助你在数据库中存储数据和与数据交互。更多细节详

见Django文档。1.2.2 用例

由于抽象工厂模式是一种一般化的工厂方法模式,它提供了相同的好处:使跟踪对象创建更容易,将对象的创建与使用解耦,并赋予你提升应用内存使用率与性能的可能性。

可是,这也提出了一个问题:我们如何知道该使用工厂方法还是抽象工厂?答案是:通常从简单的工厂方法开始。如果发现应用程序需要许多工厂方法,且将这些方法组合起来创建一系列对象是有意义的,那么就使用抽象工厂。

抽象工厂的一个好处是,它使我们能够通过更改处于激活状态的工厂方法,动态地(在运行时)修改应用程序的行为。而这一点从使用工厂方法的开发人员的角度来看并不是很明显。典型的例子是能够在用户使用应用程序时为其更改应用程序的外观(例如,Apple风格界面、Windows风格界面等),而无须终止应用再重新启动。1.2.3 抽象工厂模式的实现

为了展示抽象工厂模式,我将重新使用我最喜欢的一个示例,它出自Bruce Eckel的Python 3 Patterns, Recipes and Idioms一书。假设我们正在创建一个游戏,或者想将一个迷你游戏作为应用程序的一部分来取悦用户。我们希望至少包括两款游戏:一款儿童游戏,一款成人游戏。我们将根据用户的输入,在运行时决定创建和启动哪款游戏。抽象工厂会负责游戏创建的部分。

让我们从儿童游戏开始。这个游戏叫作FrogWorld。男主角是一只喜欢吃虫子的青蛙。每个主角都需要一个好的名字,在我们的例子中,这个名字是由用户在运行时给出的。interact_with()方法用于描述青蛙与障碍物(例如,虫子、谜题和其他青蛙)的交互。代码如下:class Frog: def __init__(self, name): self.name = name def __str__(self): return self.name def interact_with(self, obstacle): act = obstacle.action() msg = f'{self} the Frog encounters {obstacle} and {act}!' print(msg)

游戏中可以有许多不同种类的障碍物,但在我们的例子中,障碍物只能是一只虫子。当遇到一只虫子时,青蛙只支持一种行为——吃掉虫子。代码如下:class Bug: def __str__(self): return 'a bug' def action(self): return 'eats it'

FrogWorld类是一个抽象工厂。它的主要任务是创建游戏中的主角与障碍物。保持创建方法的独立性及名称的通用性(例如,make_character()和make_obstacle()),我们将能动态地更改处于激活状态的工厂(进而改变处于激活状态的游戏),而不需要修改任何代码。在静态类型语言中,抽象工厂是一个带有空方法的抽象类/接口,但是在Python中,这是不必要的,因为类型是在运行时检查的(j.mp/ginstromdp)。代码如下: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的游戏也是类似的。唯一的区别是,巫师与怪物(如orks兽人)战斗,而不是吃虫子。

下面是Wizard类的定义,它与Frog类相似:class Wizard: def __init__(self, name): self.name = name def __str__(self): return self.name def interact_with(self, obstacle): act = obstacle.action() msg = f'{self} the Wizard battles against {obstacle} and {act}!' print(msg)

下面是Ork类的定义:class Ork: def __str__(self): return 'an evil ork' def action(self): return 'kills it'

我们还需要定义WizardWorld类,它与我们讨论过的FrogWorld类相似。在这种情况下,障碍物是一个Ork实例: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类是我们游戏的主入口。它接受一个工厂作为输入,并使用它来创建游戏世界。play()方法初始化主角与障碍物之间的交互,如下所示: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(f'Welcome {name}. How old are you? ') age = int(age) except ValueError as err: print(f"Age {age} is invalid, please try again...") 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文件中的完整代码)。

(1) 为FrogWorld游戏定义Frog和Bug类。

(2) 添加FrogWorld类,在其中使用Frog和Bug类。

(3) 为WizardWorld游戏定义Wizard和Ork类。

(4) 添加WizardWorld类,在其中使用Wizard和Ork类。

(5) 定义GameEnvironment类。

(6) 添加validate_age()函数。

(7) 最后,添加main()函数,并使用传统的技巧调用它。该函数的要点如下。● 获取用户输入的姓名与年龄。● 根据用户的年龄决定使用的游戏。● 实例化正确的游戏类,然后实例化GameEnvironment类。● 调用environment对象中的play()方法来玩这个游戏。

使用python abstract_factory.py命令调用这个程序,并观察一些示例输出。

面向青少年的游戏示例输出如下。

面向成年人的游戏示例输出如下。

尝试扩展游戏,使它更完整。你可以随意创造障碍物、敌人,以及任何你喜欢的东西。1.3 小结

本章介绍了如何使用工厂方法和抽象工厂设计模式。当我们希望跟踪对象创建、解耦对象创建与对象使用,甚至提升应用程序的性能和资源使用率时,这两种模式都会派上用场。本章没有展示如何提升性能。你可以考虑把它作为一个很好的练习。

工厂方法设计模式被实现为不属于任何类的单个函数,并负责创建单个种类的对象(一个形状、一个连接点等)。我们了解了工厂方法与玩具制造的关系,提到了Django如何使用它来创建不同的表单字段,并讨论了其他可能的用例。例如,我们实现了一个工厂方法,来提供对XML和JSON文件的访问途径。

抽象工厂设计模式被实现为许多工厂方法,这些工厂方法属于单个类,并用于创建一系列相关对象(汽车部件、游戏环境,等等)。我们提到了抽象工厂与汽车制造的关系、Django的django_factory包如何使用它来创建干净的测试,然后介绍了它的常见用例。我们对于抽象工厂的实现示例是一个迷你游戏,展示了如何在单个类中使用许多相关的工厂方法。

下一章将讨论建造者模式。它是另一种创建型模式,可用于微调复杂对象的创建过程。第 2 章 建造者模式

前一章介绍了前两种创建型模式——工厂方法和抽象工厂。它们都提供了在重要情况下改进对象创建方法的途径。

现在,假设我们要创建一个由多个部分组成的对象,而且创建过程需要一步一步地完成。只有创建了所有部分,该对象才是完整的。这就是建造者设计模式可以帮助我们的地方。建造者模式将复杂对象的构建与其表示分离。通过将构建与表示分离,相同的构建结构可用于创建几个不同的表示(j.mp/builderpat)。

介绍一个实际的例子会有助于你理解建造者模式的目的。假设我们想创建一个HTML页面生成器。HTML页面的基础结构(构建部分)大同小异:以开始,以结束,在HTML部分中有与元素,在head部分中有元素,以此类推。但是页面的表示可能不同。每个页面都有自己的标题、头部和不同的内容。此外,页面通常按步骤构建:一个函数添加标题,一个函数添加主头部,一个函数添加脚注,等等。只有在页面的整个结构完成之后,才能使用一个最终的渲染函数将其呈现给客户端。我们可以进一步扩展HTML生成器,使其能够生成完全不同的HTML页面。一个页面可能包含表格,一个页面可能包含图像库,一个页面可能包含联系人表单,等等。

HTML页面生成问题可以使用建造者模式解决。这种模式中,主要有两个参与者。● 建造者(builder):负责创建复杂对象的各个部分的组件。本例

中,这些部分是页面的标题、头部、主体和脚注。● 指挥者(director):使用建造者实例控制构建过程的组件。调

用建造者的函数来设置标题、头部,等等。而且,使用不同的建

造者实例能够创建不同的HTML页面,而不需要触及指挥者的任

何代码。

本章将讨论:● 现实生活中的例子● 用例● 实现2.1 现实生活中的例子

日常生活中,建造者设计模式被应用于快餐店。虽然有许多不同种类的汉堡(经典汉堡、芝士汉堡,等等)和不同的包装(小盒子、中型盒子,等等),但制作汉堡和包装(盒子和纸袋)的过程是相同的。经典汉堡和芝士汉堡的区别在于表现形式,而不是制作过程。在这种情况下,指挥者是收银员,他向员工说明需要准备什么汉堡,而建造者是负责处理订单的员工。

我们同样可以找到软件中的例子。● 本章开头提到的HTML示例实际上是Django的第三方(HTML)

树编辑器django-widgy使用的,django-widgy可以用作内容管理

系统(CMS)。django-widgy编辑器包含一个页面建造者,可以

用于创建具有不同布局的HTML页面。● django-query-builder库是另一个依赖于建造者模式的第三方

Django库。这个库可用于动态构建SQL查询语句,允许你控制查

询的各个方面,并创建不同复杂度的查询语句。2.2 用例

当必须用多个步骤创建对象,并且需要相同构造的不同表现形式时,我们将使用建造者模式。这些需求存在于许多应用程序中,例如页面生成器(如本章中提到的HTML页面生成器)、文档转换器和用户界面(UI)表单创建器(j.mp/pipbuild)。

一些在线资源提到,建造者模式也可以用作伸缩构造器问题的解决方案。当我们被迫创建一个新的构造函数以支持创建对象的不同方式时,伸缩构造函数问题就会发生。这个问题是,我们最终会得到许多构造函数和很长的参数列表,而这些都很难管理。伸缩构造函数的一个例子可以在Stack Overflow网站上找到(j.mp/sobuilder)。幸运的是,这个问题在Python中并不存在,因为它至少可以通过两种方式解决:● 命名参数(j.mp/sobuipython);● 参数列表解构(j.mp/arglistpy)。

此时,建造者模式和工厂模式之间的区别可能不是很清楚。主要区别在于,工厂模式在单个步骤中创建对象,而建造者模式在多个步骤中创建对象,而且几乎总是使用指挥者。建造者模式的一些目标实现,例如Java的StringBuilder,绕过了对指挥者的使用,但这是例外。

另一个区别是,工厂模式立即返回创建的对象,而在建造者模式中,客户端代码明确地要求指挥者在需要时返回最终对象(j.mp/builderpat)。

我们用购买新计算机做个类比,来帮助你更好地区分建造者模式和工厂模式。假设你想买一台新计算机。如果你决定购买特定的预配置计算机型号,例如最新的Apple 1.4 GHz Mac Mini,就需要使用工厂模式。所有的硬件规格已经由制造商预先定义,他们不咨询你也知道要做什么。制造商通常只收到一条指令。代码如下所示(apple_factory.py):MINI14 = '1.4GHz Mac mini'class AppleFactory: class MacMini14: def __init__(self): self.memory = 4 # 单位为GB self.hdd = 500 # 单位为GB self.gpu = 'Intel HD Graphics 5000' def __str__(self): info = (f'Model: {MINI14}', f'Memory: {self.memory}GB', f'Hard Disk: {self.hdd}GB', f'Graphics Card: {self.gpu}') return '\n'.join(info) def build_computer(self, model): if model == MINI14: return self.MacMini14() else: msg = f"I don't know how to build {model}" print(msg)

现在,我们添加程序的主要部分,AppleFactory类的代码片段:if __name__ == '__main__': afac = AppleFactory() mac_mini = afac.build_computer(MINI14) print(mac_mini) 注意这个嵌套的MacMini14类。这是一种禁止类直

接实例化的简洁方法。

另一个选择是购买一台定制的个人计算机。本例中,你可以使用建造者模式。你就是指挥者,向制造商(builder)发出指令,说明你理想的计算机规格。代码如下所示(computer_builder.py)。● 定义Computer类。

class Computer:

def __init__(self, serial_number):

self.serial = serial_number

self.memory = None # 单位为GB

self.hdd = None # 单位为GB

self.gpu = None

def __str__(self):

info = (f'Memory: {self.memory}GB',

f'Hard Disk: {self.hdd}GB',

f'Graphics Card: {self.gpu}')

return '\n'.join(info)● 定义ComputerBuilder类。

class ComputerBuilder:

def __init__(self):

self.computer = Computer('AG23385193')

def configure_memory(self, amount):

self.computer.memory = amount

def configure_hdd(self, amount):

self.computer.hdd = amount

def configure_gpu(self, gpu_model):

self.computer.gpu = gpu_model● 定义HardwareEngineer类。

class HardwareEngineer:

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

下载完整电子书


相关推荐

最新文章


© 2020 txtepub下载