《Python编程实战:运用设计模式、并发和程序库创建高质量程序》—— 1.3 工厂方法模式

简介:

本节书摘来自华章出版社《Python编程实战:运用设计模式、并发和程序库创建高质量程序》一 书中的第1章,第1.3节,作者:(美) Mark Summerfield,更多章节内容可以访问云栖社区“华章计算机”公众号查看。

1.3 工厂方法模式

如果子类的某个方法要根据情况来决定用什么类去实例化相关对象,那么可以考虑工厂方法模式。此模式可单独使用,也可在无法预知对象类型时使用(比方说,待初始化的对象类型要从文件中读入,或是由用户来输入)。

238674a6afc9c60c17b0888625f08e3b6b2e1d93

本节编写一段棋盘生成程序,用以生成“国际跳棋”(checker)和“国际象棋”(chess)的棋盘。该程序所输出的两张棋盘如图1.3所示。这段程序有四个版本,其源代码分别存放在gameboard1.py至gameboard4.py中。
我们先设计出抽象的棋盘类,然后用其子类创建特定的棋盘。每个子类都会生成相应的棋盘,并把棋子摆放好。每个棋子也有对应的类(比如黑色的跳棋棋子用BlackDraught类表示,白色的跳棋棋子用WhiteDraught类表示,黑色的“象”用BlackChessBishop表示,白色的“马”用WhiteChessKnight表示)。为了和Unicode中的字符名称保持一致,我们在表示跳棋棋子时使用了Draught一词,而没有使用Checker,比如白色的跳棋棋子叫做WhiteDraught,而不叫WhiteChecker。
我们打算先讲最顶层的代码,这部分代码用于实例化棋盘对象,并把棋盘打印到控制台。然后来看表示棋盘的类和表示棋子的类。一开始,我们采用“硬代码”类的方式来创建这些棋子。然后设法将其改写,去掉那些硬代码类,并缩减代码行数。
screenshot

四个版本的程序都要用到上面这个函数。该函数分别创建两种棋盘对象,并将其打印到控制台,打印的时候会调用AbstractBoard的__str__()方法,以便将棋盘对象的内容转换成字符串。
screenshot

BLACK与WHITE常量表示棋盘格子的背景色。在后续版本中,还会用来表示棋子的颜色。上面这段代码是从gameboard1.py中节录的,其他三个版本也与此相同。
BLACK, WHITE = ("BLACK", "WHITE")这行代码本来可以按惯例写成BLACK, WHITE = range(2)。但是用字符串来定义常量在调试时更容易看出错误信息的含义,而且Python还会自动把内容相同的字符串规整起来,只保留一份。
棋盘对象里包含一份二维列表,其中每个一维列表都表示棋盘中的一行,而一维列表中的元素则表示行中对应单元格上的棋子,如果某个格子上没有棋子,那么对应的元素就是None。console()函数(此函数没有出现在上述程序清单中)所返回的字符串用于表示棋子及其背景色。(在“类Unix”系统中,console()函数所返回的字符串里会包含转义符,用于修改字符的背景色。)
你可以把AbstractBoard类的metaclass设置成abc.ABCMeta(1.2节中的AbstractFormBuilder类就是如此),这样的话,它就成了真正的抽象基类。不过此处我们改用另一种做法:凡是需要由子类重新实现的方法都抛出NotImplementedError异常。
screenshot

上述子类用于创建10×10的国际跳棋棋盘。该类的populate_board()目前还算不上工厂方法,因为它是用硬编码的类来实例化棋子对象的,稍后我们会以此为基础将之改写成工厂方法。
screenshot

在gameboard1.py这一版程序中,ChessBoard类的populate_board()方法与CheckersBoard类的同名方法一样,都不能称为工厂方法,不过,由上面这段代码我们可以看出国际象棋的棋盘是如何生成的。
screenshot

上面这个Piece类是所有棋子的基类,本来也可以直接用str表示,但如果那样做的话,就没办法判断某个对象是不是棋子了(比如我们想用isinstance(x, Piece)来判断x对象是不是棋子)。__slots__ = ()这行语句可以保证实例中不会有任何数据,我们把这个话题放在2.6节中讨论。
screenshot

上面这两个类是所有棋子的范本。每种棋子所对应的类都是Piece的子类,而Piece本身又是str的子类,棋子对象都是“不可变的”(immutable),我们用与之对应的Unicode字符来初始化它。__new__()中所用的Unicode字符其模样与对应的棋子相同。总共有14个这样的子类,它们都非常相似,只是类名与所含字符串不同,最好能把这些近乎重复的代码清理掉。
screenshot
screenshot

上面列出了从gameboard2.py中节选的新版CheckersBoard.populate_board()方法,这次就可以把它叫做工厂方法了,因为此方法会用名为create_piece()的“工厂函数”(factory function)来创建棋子,而不像以前那样直接用硬编码的棋子名称来创建。create_piece()函数会根据其参数返回适当类型的对象(比方说,如果color是"black",那就创建BlackDraught对象;如果color是"white",则创建WhiteDraught对象)。新版代码中的ChessBoard.populate_board()方法(并未列在上述程序清单中)与之类似,也会调用其create_piece()函数,根据棋子颜色及名称来创建相应的对象。
screenshot

工厂函数使用Python语言内置的eval()函数来创建对应类的实例。比方说,如果参数是"knight"与"black",那么交由eval()函数执行的字符串就是"BlackChessKnight()"。虽说这样做完全可行,但可能会有风险,因为任何字符串都会当成Python代码交给eval()函数执行。稍后我们将换用另一种办法,以Python语言内置的type()函数来创建实例。
screenshot

这次不需要再把14个相似的类逐个写出来了,而是以一段代码块为模板,将其全都创建好。
调用itertools.chain()函数时,可传入一个或多个iterable(可迭代物,可遍历物),此函数将返回另外一个iterable,在返回那个的iterable上面遍历时,会先遍历刚才调用时传入的首个iterable,然后再遍历刚才调用时传入的第二个iterable,依此类推。本例中,我们给函数传了两个iterable,第一个iterable是个二元组(2-tuple),其中的两个值分别是黑色与白色跳棋棋子的“Unicode码位”(Unicode code point),而第二个iterable则是个range对象(实际上就是个生成器),用于指定各种黑色与白色的国际象棋棋子。
对于每个码位来说,我们都创建一个仅包含该字符的字符串(比如“”),并根据其“Unicode名称”(Unicode name)来确定类名(例如,黑色“马”的Unicode名称是“black chess knight”,所以创建出来的类就叫做BlackChessKnight)。确定了字符与类名之后,就可以用exec()来创建所需的类了。原来那版程序需要用100多行代码来逐个创建这些类,而现在只用十几行就够了。
但是,用exec()所带来的风险比用eval()还要高,所以必须得找个更好的办法才行。
screenshot

上面这个CheckersBoard.populate_board()方法是从gameboard3.py中节选的。与前一版相比,这个版本的棋子与颜色用的都是常量,而不是“字符串字面值”(string literal),因为那样很容易打错字,而且这一版采用新的create_piece()方法来创建棋子。
gameboard4.py程序将“列表推导”(list comprehension)技术与两个itertools函数结合起来,用另一种办法实现了CheckersBoard.populate_board()函数(此函数没有列在上述程序清单里)。
screenshot

在这一版程序(也就是gameboard3.py)中,create_piece()工厂函数是AbstractBoard类的方法,CheckersBoard与ChessBoard类都会继承它。该方法接受两个常量做参数,根据棋子种类及颜色在静态的(也就是类级别的)字典中找到对应的类,这个字典的键是(piece kind, color)二元组,值是“类对象”(class object)。找到值(也就是所需的类)之后,立即用()操作符将其实例化,并返回创建好的棋子对象。
字典中的类本来可以像gameboard1.py那样直接写成硬代码,或是像gameboard2.py那样用不太安全的办法动态创建出来,但在gameboard3.py中,我们要采用一种较为安全的办法来做:这次仍然是动态创建,只是不再使用eval()与exec()了。
screenshot

上面这段代码的结构与早前动态创建14个子类的那段代码基本相同,只是这次没使用eval()与exec(),而是改用了一种较为安全的办法。
知道了与棋子相对应的字符及类名之后,就可以用自定义的make_new_method()函数来创建new()函数了。创建好new()函数后,再用Python内置的type()函数创建新的类。以这种方式创建类的时候,必须传入类型名称、含有基类名称的元组(在本例中,只有一个基类,就是Piece)以及含有类属性的字典。在字典中,我们将__slots__属性设为空元组(这样的话,在类的实例中就不会出现私有的__dict__了),并将__new__“方法属性”(method attribute)设置成刚才创建好的new()函数。
最后,调用内置的setattr()函数,把新创建的类(用Class变量表示)当作属性(属性名用name变量表示,比方说,在创建白色的“兵”时,name变量的值就是"WhiteChessPawn")添加到当前模块(sys.modules[__name__])中。gameboard4.py用更为简洁的方式改写了上述程序清单中的最后一行代码:
screenshot

上面这种写法的意思是:在存放全局变量的dict里添加元素,新元素的键是name,值是刚才创建的Class。这种写法的效果与gameboard3.py中的setattr()那行语句完全一样。
screenshot

上面这个函数是用来创建new()函数的(而创建好的函数将成为类的__new__()方法)。在创建new()函数时不能调用super(),因为此处并没有super()函数所需的“类环境”。请注意,尽管Piece类没有__new__()方法,但其基类str有,所以make_new_method()函数所调用的Piece.__new__()实际上指的是str.__new__()。
前面代码中的new = make_new_method(char)语句以及make_new_method()函数其实都可以删掉,把原来调用make_new_method()函数的代码改成下面这两行语句就好:
screenshot

上面这段代码先写了lambda表达式,然后立刻用char来填充外围的lambda,以此创建出new()函数。(gameboard4.py用的就是这种写法。)
所有的lambda函数都叫做"lambda",这在调试的时候不易区分,所以创建好new()函数之后,我们又给它起了个新名字。
screenshot

为了使范例代码完整一些,笔者在上面列出了ChessBoard.populate_board()方法的代码,gameboard3.py及gameboard4.py都使用此方法。它用棋子颜色及棋子类型常量来生成棋盘(也可以不写成硬代码,而是从文件中读入,或令用户通过菜单来选择)。gameboard3.py使用的是早前列出的那个create_piece()工厂函数,而gameboard4.py所使用的create_piece()则是最终版。
screenshot

上面是gameboard4.py的create_piece()工厂函数,其所用的常量与gameboard3.py相同,但它并没有专门把类对象保存到字典中,而是调用内置的globals()函数,在返回的全局变量字典里查出所需的类对象,立刻将其实例化,并返回创建好的棋子对象。

相关文章
|
2天前
|
设计模式 开发者
探讨常见设计模式 - 工厂方法模式的最佳实践和潜在的实施问题
【4月更文挑战第7天】工厂方法模式是创建型设计模式,提供了一种在不指定具体类情况下创建对象的方式。它定义创建对象的接口,允许子类决定实例化哪个类,从而解耦对象的创建和使用。最佳实践包括明确接口、封装创建逻辑、提供扩展点和避免过度使用。然而,过度工程、违反开闭原则、性能影响和依赖管理是可能的问题。通过权衡利弊并遵循最佳实践,工厂方法模式能在适当场景下提升代码灵活性和可扩展性。
|
22小时前
|
Java 测试技术 Python
Python的多线程允许在同一进程中并发执行任务
【5月更文挑战第17天】Python的多线程允许在同一进程中并发执行任务。示例1展示了创建5个线程打印"Hello World",每个线程调用同一函数并使用`join()`等待所有线程完成。示例2使用`ThreadPoolExecutor`下载网页,创建线程池处理多个URL,打印出每个网页的大小。Python多线程还可用于线程间通信和同步,如使用Queue和Lock。
12 1
|
2天前
|
设计模式 Java uml
【设计模式】什么是工厂方法模式?
【设计模式】什么是工厂方法模式?
7 1
|
2天前
|
设计模式 存储 JavaScript
[设计模式Java实现附plantuml源码~创建型] 多态工厂的实现——工厂方法模式
[设计模式Java实现附plantuml源码~创建型] 多态工厂的实现——工厂方法模式
|
2天前
|
设计模式 测试技术 Go
[设计模式 Go实现] 创建型~工厂方法模式
[设计模式 Go实现] 创建型~工厂方法模式
|
2天前
|
设计模式 算法 程序员
Python从入门到精通:2.1.3深入学习面向对象编程——设计模式的学习与实践
Python从入门到精通:2.1.3深入学习面向对象编程——设计模式的学习与实践
|
2天前
|
监控 Python
Python监控主机是否存活,并发报警邮件
Python监控主机是否存活,并发报警邮件
|
2天前
|
设计模式
设计模式(二)工厂方法模式(Factory Method)
设计模式(二)工厂方法模式(Factory Method)
15 0
|
2天前
|
数据采集 缓存 算法
使用Python打造爬虫程序之Python中的并发与异步IO:解锁高效数据处理之道
【4月更文挑战第19天】本文探讨了Python中的并发与异步IO,区分了并发(同时处理任务)与并行(同时执行任务)的概念。Python的多线程受限于GIL,适合IO密集型任务,而多进程适用于CPU密集型任务。异步IO通过非阻塞和回调/协程实现高效IO,Python的asyncio库提供了支持。应用场景包括Web开发和网络爬虫等。实践指南包括理解任务类型、使用asyncio、避免阻塞操作、合理设置并发度和优化性能。理解并运用这些技术能提升Python程序的效率和性能。
|
2天前
|
存储 Java Python
【Python小知识】如何解决代理IP在多线程环境下的并发问题?
【Python小知识】如何解决代理IP在多线程环境下的并发问题?