Creational design patterns deal with an object creation . The aim of a creational design pattern is to provide
better alternatives for situations where a direct object creation (which in Python happens by the __init__()
function ) is not convenient.
创建型设计模式主要负责处理对象的创建。对于不适合直接创建对象的情况(一般在python的__init__()方法初始化对象的时候),它能够给我们提供更好的替代方案,同时这也是创建型设计模式的目标。
In the Factory design pattern, a client asks for an object without knowing where the object is coming from
(that is, which class is used to generate it). The idea behind a factory is to simplify an object creation.
It is easier to track which objects are created if this is done through a central function, in contrast to letting
a client create objects using a direct class instantiation . A factory reduces the complexity of maintaining
an application by decoupling the code that creates an object from the code that uses it.
在工厂设计模式中,客户端只需要知道如何调用对象,而不需要知道对象是从哪里来的(也就是说,它不用知道对象是由哪个类来创建)。工厂模式背后的思想就是简化对象的创建。相对于直接实例化类来创建对象,如果可以通过中央函数来创建对象,将会使得对象更容易跟踪管理。使用工厂方法,可以分离对象的创建和使用,从而解耦代码,降低维护应用的复杂程度。
Factories typically come in two forms: the Factory Method, which is a method (or in Pythonic terms,
a function) that returns a different object per input parameter; the Abstract Factory, which is a group
of Factory Methods used to create a family of related products.
工厂模式主要有两种形式:
第一,工厂方法,它是根据不同的输入返回不同对象的方法;
第二,抽象工厂,它是创建系列相关对象的方法组。
Factory Method
工厂方法
In the Factory Method, we execute a single function, passing a parameter that provides information
about what we want. We are not required to know any details about how the object is implemented
and where it is coming from.
在工厂方法中,我们只需要执行一个函数,然后当我们向函数传递一个参数,它就会返回我们想要的对象。关于对象创建的细节(对象是从哪里来的,是如何实现的),我们根本不需要知道。
A real-life example
一个生活实例
An example of the Factory Method pattern used in reality is in plastic toy construction. The molding
powder used to construct plastic toys is the same, but different figures can be produced using
different plastic molds. This is like having a Factory Method in which the input is the name of the figure
that we want (duck and car) and the output is the plastic figure that we requested.
The toy construction case is shown in the following figure, which is provided by.
在实际的生活当中,工厂生产塑料玩具就是一个应用工厂方法模式的实例。虽然制造塑料玩具的原料(成型粉)是相同的,但是工厂可以通过使用不同的模具,制造出不同的玩具。这就像工厂方法模式一样,只要输入玩具的名称(例如:鸭和汽车),工厂就会生产出我们想要的塑料玩具。生产玩具案例的用例图如下图所示。
A software example
一个软件实例
The Django framework uses the Factory Method pattern for creating the fields of a form.
The forms module of Django supports the creation of different kinds of fields (CharField,
EmailField) and customizations (max_length, required).
Django框架就是使用工厂方法模式来创建表单的输入域。Django框架的表单模块不但支持不同输入域的创建(例如:文本输入域,电子邮件输入域),而且还支持自定义输入域的属性(例如:最大长度、是否必填)。
Use cases
用例
If you realize that you cannot track the objects created by your application because the code
that creates them is in many different places instead of a single function/method,
you should consider using the Factory Method pattern. The Factory Method centralizes
an object creation and tracking your objects becomes much easier. Note that it is absolutely fine
to create more than one Factory Method, and this is how it is typically done in practice.
Each Factory Method logically groups the creation of objects that have similarities. For example,
one Factory Method might be responsible for connecting you to different databases (MySQL, SQLite),
another Factory Method might be responsible for creating the geometrical object
that you request (circle, triangle), and so on.
如果你已经意识到,由于对象的创建存在于代码的各种地方,而使得你不能踪管理它们,这个时候你就应该考虑使用工厂方法模式,通过统一函数/方法来创建管理它们。使用工厂方法可以集中创建对象,并且更加容易跟踪管理它们。请注意,在实际操作当中,人们通常会建立多个工厂方法。然后把工厂方法逻辑分组,创建相类似对象的方法放在一个工厂里面。例如,一个工厂方法可能负责连接到不同的数据库(MySQL,SQLite),另一个工厂方法可能负责创造你请求的几何对象(圆,三角形),等等。
The Factory Method is also useful when you want to decouple an object creation from
an object usage. We are not coupled/bound to a specific class when creating an object,
we just provide partial information about what we want by calling a function. This means that
introducing changes to the function is easy without requiring any changes to the code that uses it .
工厂方法模式对于分离对象的创建和使用是非常合适的。在创建对象的时候,我们不会耦合特定的类,我们只需把部分对象的信息传递到特定函数,然后函数就会返回我们需要的对象。这意味着,当应用程序的功能发生变化时,我们只需要修改创建对象的函数,而不需要对调用对象的代码进行任何更改。
Another use case worth mentioning is related to improving the performance and
memory usage of an application. A Factory Method can improve the performance
and memory usage by creating new objects only if it is absolutely necessary.
When we create objects using a direct class instantiation, extra memory is allocated every time
a new object is created (unless the class uses caching internally, which is usually not the case).
We can see that in practice in the following code (file id.py), it creates two instances
of the same class A and uses the id() function to compare their memory addresses.
The addresses are also printed in the output so that we can inspect them. The fact that
the memory addresses are different means that two distinct objects are created as follows:
另外还有一个值得一提的用例,是关于提高应用程序的性能和内存使用。因为使用了工厂方法,使得只有当对象是必要的时候,工厂方法才会创建它,所以,应用程序的性能和内存的使用得以相应的提升。每当我们直接使用类实例化对象,都会开销额外的内存(除非在类的内部使用缓存机制,但一般不是这样的)。我们从在下面的代码(id.py)可以看到,它使用同一类的创建两个实例,并使用的内建函数id()来比较两者的内存地址。并且两者的内存地址都打印输出到前台,这样方便我们观察他们。根据结果,两个实例的内存地址是不同的,这意味着它们创建了两个独立的实例,代码如下:
class A(object):
pass
if __name__ == '__main__':
a = A()
b = A()
print(id(a) == id(b))
print(a, b)
Executing id.py on my computer gives the following output:
运行id.py,它输出如下内容:
>> python3 id.py
False
Note that the addresses that you see if you execute the file are not the same as I see
because they depend on the current memory layout and allocation. But the result
must be the same: the two addresses should be different. There's one exception
that happens if you write and execute the code in the Python Read-Eval-Print Loop (REPL)
(interactive prompt), but that's a REPL-specific optimization which is not happening normally.
请注意,如果你运行该文件,你看到的输出的结果(两个实例的内存地址),应该跟我这里输出的结果不一样,因为它们依赖于当前的内存布局和分配。但是,两者对比的结果应该是一样的,就是输出的内存地址应该不一样。如果你编写与执行代码都是在Python的REPL(交互式解释器)里面进行,也是会有例外的,但是这种特殊的REPL优化一般不会出现。
Implementation
实现
Data comes in many forms. There are two main file categories for storing/retrieving data:
human_readable files and binary files. Examples of human_readable files are XML, Atom,
YAML, and JSON. Examples of binary files are the .sq3 file format used by SQLite and
the .mp3 file format used to listen to music.
数据在现实中是以多种形式存在。一般用于存储或者检索数据的文件格式主要有两种:文字文件格式和二进制文件格式。而文字文件的常用格式有:XML,Atom,YAML和JSON。二进制文件的常用格式有:用于SQLite存储数据的.sq3文件格式,用于保存音乐数据的.mp3文件格式。
In this example, we will focus on two popular human-readable formats: XML and JSON.
Although human_readable files are generally slower to parse than binary files,
they make data exchange, inspection, and modification much easier. For this reason,
it is advised to prefer working with human_readable files, unless there are other restrictions
that do not allow it (mainly unacceptable performance and proprietary binary formats).
在下面的例子中,我们将集中介绍XML和JSON这两种文字文件格式。虽然文字文件的解析速度一般比二进制文件慢,但它对于数据交换、检查和修改来得更简单。出于这个理由,我们推荐人们在工作的时候使用文字文件,除非有其他限制(一般是不能接收的性能问题或者是需要专有二进制格式)。
In this problem, we have some input data stored in an XML and a JSON file,
and we want to parse them and retrieve some information. At the same time,
we want to centralize the client's connection to those (and all future) external services.
We will use the Factory Method to solve this problem.
The example focuses only on XML and JSON, but adding support for more services
should be straightforward.
在下面的例子中,我们先输入一些数据,将它们分别存储在XML文件和JSON文件里面,然后解析它们,并且检索某些信息。同时,我们需要把解析这一部分外接服务集中起来管理。我们将使用工厂方法来解决这个问题。虽然这个例子仅仅只是XML和JSON解析,但是当应用程序添加更多支持服务的时候,代码必须具备良好的扩展能力。
First, let's take a look at the data files. The XML file, person.xml, is based on
the Wikipedia example and contains information about individuals
(firstName, lastName, gender, and so on) as follows:
首先,让我们先看看这两个数据文件。第一个是xml文件,person.xml,它是摘自维基百科的一个例子,包含一些个人信息(名字,姓氏,性别,等等),具体文件如下:
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
The JSON file, donut.json, comes from the GitHub account of Adobe and
contains donut information (type, price/unit that is, ppu, topping, and so on) as follows:
第二个是json文件,donut.json,它由Adobe 在GitHub上面的账户提供的,主要描述甜甜圈的信息,包括种类、价格等等),具体文件如下:
[
{
"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" }
]
}
]
We will use two libraries that are part of the Python distribution for working with XML and JSON:
xml.etree.ElementTree and json as follows:
我们将使用Python的XML库和JSON库来实现功能,它们是 xml.etree.elementtree和json,具体引入代码如下:
import xml.etree.ElementTree as etree
import json
The JSONConnector class parses the JSON file and has a parsed_data() method
that returns all data as a dictionary (dict). The property decorator is used to
make parsed_data() appear as a normal variable instead of a method as follows:
我们使用JSONConnector 来解析JSON文件,json库里面有parsed_data()这个方法,它可以以字典的形式返回所有数据。通过@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
The XMLConnector class parses the XML file and has a parsed_data() method that
returns all data as a list of xml.etree.Element as follows:
我们使用XMLConnector 解析XML文件,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
The connection_factory() function is a Factory Method. It returns an instance
of JSONConnector or XMLConnector depending on the extension of the input file path
as follows:
connection_factory()函数是一个工厂方法。它根据输入的文件路径,返回JSONConnector或者XMLConnector 的实例,具体代码如下:
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)
The connect_to() function is a wrapper of connection_factory(). It adds exception handling
as follows:
connect_to()方法封装了connection_factory(),并且增加了异常处理功能,具体代码如下:
def connect_to(filepath):
factory = None
try:
factory = connection_factory(filepath)
except ValueError as ve:
print(ve)
return factory
The main() function demonstrates how the Factory Method design pattern can be used.
The first part makes sure that exception handling is effective as follows:
main()方法演示如何使用工厂方法,方法的第一部分测试异常处理的有效性,具体代码如下:
def main():
sqlite_factory = connect_to('data/person.sq3')
The next part shows how to work with the XML files using the Factory Method. XPath is used to
find all person elements that have the last name Liar. For each matched person,
the basic name and phone number information are shown as follows:
跟着那部分代码演示如何使用工厂方法来解析这个xml文件。它使用XPath来查询所有姓Liar的人。对于匹配的人,需要把他的姓名以及电话号码显示出来,具体代码如下:
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')]
The final part shows how to work with the JSON files using the Factory Method. Here,
there's no pattern matching, and therefore the name, price, and topping of all donuts
are shown as follows:
最后一部分的代码演示如何使用工厂方法来解析这个JSON文件。在这里,不需要匹配甜甜圈的信息,因此,所有甜甜圈的信息(包括名称,价格等)都会显示出来,具体代码如下:
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']]
For completeness, here is the complete code of the Factory Method implementation
(factory_method.py) as follows:
下面是完整的工厂方法实现代码(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()
Here is the output of this program as follows:
下面是程序的输出,具体如下:
>>> 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
Notice that although JSONConnector and XMLConnector have the same interfaces,
what is returned by parsed_data() is not handled in a uniform way. Different python code
must be used to work with each connector. Although it would be nice to be able to
use the same code for all connectors, this is at most times not realistic unless
we use some kind of common mapping for the data which is very often provided
by external data providers. Assuming that you can use exactly the same code
for handling the XML and JSON files, what changes are required to support a third format,
for example, SQLite? Find an SQLite file or create your own and try it.
注意,虽然JSONConnector和XMLConnector具有相同的接口,但是,parsed_data()返回的数据不是以统一的方式处理。不同的Python代码必须使用不同的连接器工作。虽然所有连接器能使用相同的代码是非常好的,但是在大多数时间里,这是不现实的,除非我们使用数据供应商提供的常见映射来处理数据。假设,你是使用相同的代码来处理XML和JSON文件,现在需要增加处理第三种文件格式,例如,SQLite呢?你可以找一个SQLite文件,然后尝试一下。
As it is now, the code does not forbid a direct instantiation of a connector. Is it possible to do this? Try doing it.
到现在为止,代码都不禁止直接实例化一个连接器。它有可能用这种方式实现吗?你可以自己尝试一下。
Tip
提示
Hint: Functions in Python can have nested classes.
提示:在Python函数可以有嵌套类。
译者:由于水平有限,暂时只能翻译成这样子了,请大家指出相应的问题,谢谢。