《精通Python设计模式》试读:1.1 工厂方法

创建型设计模式处理对象创建相关的问题(请参考网页[t.cn/RqBoSiu]),目标是当直接创建对象(在Python中是通过__init__()函数实现的,请参考网页[t.cn/RqB3mDM]和[Lott14,第26页])不太方便时,提供更好的方式。 在工厂设计模式中,客户端 可以请求一个对象,而无需知道这个对象来自哪里;也就是,使用哪个类来生成这个对象。工厂背后的思想是简化对象的创建。与客户端自己基于类实例化直接创建对象相比,基于一个中心化函数来实现,更易于追踪创建了哪些对象(请参考[Eckel08,第187页])。通过将创建对象的代码和使用对象的代码解耦,工厂能够降低应用维护的复杂度(请参考[Zlobin13,第30页])。 工厂通常有两种形式:一种是工厂方法(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): pass if __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 at 0x7f5771df2208> 注意,你执行这个代码文件看到的地址会与我看到的不一样,因为这依赖程序运行时内存的布局和分配。但结果中有一点肯定是一样的,那就是两个地址不同。在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等),如下所示。 <persons> <person> <firstName>John</firstName> <lastName>Smith</lastName> <age>25</age> <address> <streetAddress>21 2nd Street</streetAddress> <city>New York</city> <state>NY</state> <postalCode>10021</postalCode> </address> <phoneNumbers> <phoneNumber type="home">212 555-1234</phoneNumber> <phoneNumber type="fax">646 555-4567</phoneNumber> </phoneNumbers> <gender> <type>male</type> </gender> </person> <person> <firstName>Jimy</firstName> <lastName>Liar</lastName> <age>19</age> <address> <streetAddress>18 2nd Street</streetAddress> <city>New York</city> <state>NY</state> <postalCode>10021</postalCode> </address> <phoneNumbers> <phoneNumber type="home">212 555-1234</phoneNumber> </phoneNumbers> <gender> <type>male</type> </gender> </person> <person> <firstName>Patty</firstName> <lastName>Liar</lastName> <age>20</age> <address> <streetAddress>18 2nd Street</streetAddress> <city>New York</city> <state>NY</state> <postalCode>10021</postalCode> </address> <phoneNumbers> <phoneNumber type="home">212 555-1234</phoneNumber> <phoneNumber type="mobile">001 452-8819</phoneNumber> </phoneNumbers> <gender> <type>female</type> </gender> </person> </persons> 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 etree import 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 etree import json 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 class XMLConnector: def __init__(self, filepath): self.tree = etree.parse(filepath) @property def parsed_data(self): return self.tree def 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 factory def 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.py Cannot connect to data/person.sq3 found: 2 persons first name: Jimy last name: Liar phone number (home): 212 555-1234 first name: Patty last name: Liar phone number (home): 212 555-1234 phone number (mobile): 001 452-8819 found: 3 donuts name: Cake price: $0.55 topping: 5001 None topping: 5002 Glazed topping: 5005 Sugar topping: 5007 Powdered Sugar topping: 5006 Chocolate with Sprinkles topping: 5003 Chocolate topping: 5004 Maple name: Raised price: $0.55 topping: 5001 None topping: 5002 Glazed topping: 5005 Sugar topping: 5003 Chocolate topping: 5004 Maple name: Old Fashioned price: $0.55 topping: 5001 None topping: 5002 Glazed topping: 5003 Chocolate topping: 5004 Maple 注意,虽然JSONConnector和XMLConnector拥有相同的接口,但是对于parsed_data()返回的数据并不是以统一的方式进行处理。对于每个连接器,需使用不同的Python代码来处理。若能对所有连接器应用相同的代码当然最好,但是在多数时候这是不现实的,除非对数据使用某种共同的映射,这种映射通常是由外部数据提供者提供。即使假设可以使用相同的代码来处理XML和JSON文件,当需要支持第三种格式(例如,SQLite)时,又该对代码作哪些改变呢?找一个SQlite文件或者自己创建一个,尝试一下。 像现在这样,代码并未禁止直接实例化一个连接器。如果要禁止直接实例化,是否可以实现?试试看。 Python中的函数可以内嵌类。

>精通Python设计模式

精通Python设计模式
作者: [荷] Sakis Kasampalis
isbn: 7115428034
书名: 精通Python设计模式
页数: 144
译者: 夏永锋
定价: 45.00元
出版社: 人民邮电出版社
装帧: 平装
出版年: 2016-7