Creationaldesignpatternsdealwithanobjectcreation.Theaimofacreationaldesignpatternistoprovide
better alternativesforsituationswhereadirectobjectcreation(whichinPythonhappensbythe__init__()
function) isnotconvenient.
创建型设计模式主要负责处理对象的创建。对于不适合直接创建对象的情况(一般在python的__init__()方法初始化对象的时候),它能够给我们提供更好的替代方案,同时这也是创建型设计模式的目标。
IntheFactorydesignpattern,aclientasksforanobjectwithoutknowingwheretheobjectiscomingfrom
(thatis,whichclassisusedtogenerateit).Theideabehindafactoryistosimplifyanobjectcreation.
Itiseasiertotrackwhichobjectsarecreatedifthisisdonethroughacentralfunction,incontrasttoletting
aclientcreateobjectsusingadirectclassinstantiation.Afactoryreducesthecomplexityofmaintaining
anapplicationbydecouplingthecodethatcreatesanobjectfromthecodethatusesit.
在工厂设计模式中,客户端只需要知道如何调用对象,而不需要知道对象是从哪里来的(也就是说,它不用知道对象是由哪个类来创建)。工厂模式背后的思想就是简化对象的创建。相对于直接实例化类来创建对象,如果可以通过中央函数来创建对象,将会使得对象更容易跟踪管理。使用工厂方法,可以分离对象的创建和使用,从而解耦代码,降低维护应用的复杂程度。
Factoriestypicallycomeintwoforms:theFactoryMethod,whichisamethod(orinPythonicterms,
afunction)thatreturnsadifferentobjectperinputparameter;theAbstractFactory,whichisagroup
ofFactoryMethodsusedtocreateafamilyofrelatedproducts.
工厂模式主要有两种形式:
第一,工厂方法,它是根据不同的输入返回不同对象的方法;
第二,抽象工厂,它是创建系列相关对象的方法组。
FactoryMethod
工厂方法
IntheFactoryMethod,weexecuteasinglefunction,passingaparameterthatprovidesinformation
aboutwhatwewant.Wearenotrequiredtoknowanydetailsabouthowtheobjectisimplemented
andwhereitiscomingfrom.
在工厂方法中,我们只需要执行一个函数,然后当我们向函数传递一个参数,它就会返回我们想要的对象。关于对象创建的细节(对象是从哪里来的,是如何实现的),我们根本不需要知道。
Areal-lifeexample
一个生活实例
AnexampleoftheFactoryMethodpatternusedinrealityisinplastictoyconstruction.Themolding
powderusedtoconstructplastictoysisthesame,butdifferentfigurescanbeproducedusing
differentplasticmolds.ThisislikehavingaFactoryMethodinwhichtheinputisthenameofthefigure
thatwewant(duckandcar)andtheoutputistheplasticfigurethatwerequested.
Thetoyconstructioncaseisshowninthefollowingfigure,whichisprovidedby.
在实际的生活当中,工厂生产塑料玩具就是一个应用工厂方法模式的实例。虽然制造塑料玩具的原料(成型粉)是相同的,但是工厂可以通过使用不同的模具,制造出不同的玩具。这就像工厂方法模式一样,只要输入玩具的名称(例如:鸭和汽车),工厂就会生产出我们想要的塑料玩具。生产玩具案例的用例图如下图所示。
Asoftwareexample
一个软件实例
TheDjangoframeworkusestheFactoryMethodpatternforcreatingthefieldsofaform.
TheformsmoduleofDjangosupportsthecreationofdifferentkindsoffields(CharField,
EmailField)andcustomizations(max_length,required).
Django框架就是使用工厂方法模式来创建表单的输入域。Django框架的表单模块不但支持不同输入域的创建(例如:文本输入域,电子邮件输入域),而且还支持自定义输入域的属性(例如:最大长度、是否必填)。
Usecases
用例
Ifyourealizethatyoucannottracktheobjectscreatedbyyourapplicationbecausethecode
thatcreatesthemisinmanydifferentplacesinsteadofasinglefunction/method,
youshouldconsiderusingtheFactoryMethodpattern.TheFactoryMethodcentralizes
anobjectcreationandtrackingyourobjectsbecomesmucheasier.Notethatitisabsolutelyfine
tocreatemorethanoneFactoryMethod,andthisishowitistypicallydoneinpractice.
EachFactoryMethodlogicallygroupsthecreationofobjectsthathavesimilarities.Forexample,
oneFactoryMethodmightberesponsibleforconnectingyoutodifferentdatabases(MySQL,SQLite),
anotherFactoryMethodmightberesponsibleforcreatingthegeometricalobject
thatyourequest(circle,triangle),andsoon.
如果你已经意识到,由于对象的创建存在于代码的各种地方,而使得你不能踪管理它们,这个时候你就应该考虑使用工厂方法模式,通过统一函数/方法来创建管理它们。使用工厂方法可以集中创建对象,并且更加容易跟踪管理它们。请注意,在实际操作当中,人们通常会建立多个工厂方法。然后把工厂方法逻辑分组,创建相类似对象的方法放在一个工厂里面。例如,一个工厂方法可能负责连接到不同的数据库(MySQL,SQLite),另一个工厂方法可能负责创造你请求的几何对象(圆,三角形),等等。
TheFactoryMethodisalsousefulwhenyouwanttodecoupleanobjectcreationfrom
anobjectusage.Wearenotcoupled/boundtoaspecificclasswhencreatinganobject,
wejustprovidepartialinformationaboutwhatwewantbycallingafunction.Thismeansthat
introducingchangestothefunctioniseasywithoutrequiringanychangestothecodethatusesit.
工厂方法模式对于分离对象的创建和使用是非常合适的。在创建对象的时候,我们不会耦合特定的类,我们只需把部分对象的信息传递到特定函数,然后函数就会返回我们需要的对象。这意味着,当应用程序的功能发生变化时,我们只需要修改创建对象的函数,而不需要对调用对象的代码进行任何更改。
Anotherusecaseworthmentioningisrelatedtoimprovingtheperformanceand
memoryusageofanapplication.AFactoryMethodcanimprovetheperformance
andmemoryusagebycreatingnewobjectsonlyifitisabsolutelynecessary.
Whenwecreateobjectsusingadirectclassinstantiation,extramemoryisallocatedeverytime
anewobjectiscreated(unlesstheclassusescachinginternally,whichisusuallynotthecase).
Wecanseethatinpracticeinthefollowingcode(fileid.py),itcreatestwoinstances
ofthesameclassAandusestheid()functiontocomparetheirmemoryaddresses.
Theaddressesarealsoprintedintheoutputsothatwecaninspectthem.Thefactthat
thememoryaddressesaredifferentmeansthattwodistinctobjectsarecreatedasfollows:
另外还有一个值得一提的用例,是关于提高应用程序的性能和内存使用。因为使用了工厂方法,使得只有当对象是必要的时候,工厂方法才会创建它,所以,应用程序的性能和内存的使用得以相应的提升。每当我们直接使用类实例化对象,都会开销额外的内存(除非在类的内部使用缓存机制,但一般不是这样的)。我们从在下面的代码(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
<__main__.A object at 0x7f5771de8f60> <__main__.A object at 0x7f5771df2208>
NotethattheaddressesthatyouseeifyouexecutethefilearenotthesameasIsee
becausetheydependonthecurrentmemorylayoutandallocation.Buttheresult
mustbethesame:thetwoaddressesshouldbedifferent.There'soneexception
thathappensifyouwriteandexecutethecodeinthePythonRead-Eval-PrintLoop(REPL)
(interactiveprompt),butthat'saREPL-specificoptimizationwhichisnothappeningnormally.
请注意,如果你运行该文件,你看到的输出的结果(两个实例的内存地址),应该跟我这里输出的结果不一样,因为它们依赖于当前的内存布局和分配。但是,两者对比的结果应该是一样的,就是输出的内存地址应该不一样。如果你编写与执行代码都是在Python的REPL(交互式解释器)里面进行,也是会有例外的,但是这种特殊的REPL优化一般不会出现。
Implementation
实现
Datacomesinmanyforms.Therearetwomainfilecategoriesforstoring/retrievingdata:
human_readablefilesandbinaryfiles.Examplesofhuman_readablefilesareXML,Atom,
YAML,andJSON.Examplesofbinaryfilesarethe.sq3fileformatusedbySQLiteand
the.mp3fileformatusedtolistentomusic.
数据在现实中是以多种形式存在。一般用于存储或者检索数据的文件格式主要有两种:文字文件格式和二进制文件格式。而文字文件的常用格式有:XML,Atom,YAML和JSON。二进制文件的常用格式有:用于SQLite存储数据的.sq3文件格式,用于保存音乐数据的.mp3文件格式。
Inthisexample,wewillfocusontwopopularhuman-readableformats:XMLandJSON.
Althoughhuman_readablefilesaregenerallyslowertoparsethanbinaryfiles,
theymakedataexchange,inspection,andmodificationmucheasier.Forthisreason,
itisadvisedtopreferworkingwithhuman_readablefiles,unlessthereareotherrestrictions
thatdonotallowit(mainlyunacceptableperformanceandproprietarybinaryformats).
在下面的例子中,我们将集中介绍XML和JSON这两种文字文件格式。虽然文字文件的解析速度一般比二进制文件慢,但它对于数据交换、检查和修改来得更简单。出于这个理由,我们推荐人们在工作的时候使用文字文件,除非有其他限制(一般是不能接收的性能问题或者是需要专有二进制格式)。
Inthisproblem,wehavesomeinputdatastoredinanXMLandaJSONfile,
andwewanttoparsethemandretrievesomeinformation.Atthesametime,
wewanttocentralizetheclient'sconnectiontothose(andallfuture)externalservices.
WewillusetheFactoryMethodtosolvethisproblem.
TheexamplefocusesonlyonXMLandJSON,butaddingsupportformoreservices
shouldbestraightforward.
在下面的例子中,我们先输入一些数据,将它们分别存储在XML文件和JSON文件里面,然后解析它们,并且检索某些信息。同时,我们需要把解析这一部分外接服务集中起来管理。我们将使用工厂方法来解决这个问题。虽然这个例子仅仅只是XML和JSON解析,但是当应用程序添加更多支持服务的时候,代码必须具备良好的扩展能力。
First,let'stakealookatthedatafiles.TheXMLfile,person.xml,isbasedon
theWikipediaexampleandcontainsinformationaboutindividuals
(firstName,lastName,gender,andsoon)asfollows:
首先,让我们先看看这两个数据文件。第一个是xml文件,person.xml,它是摘自维基百科的一个例子,包含一些个人信息(名字,姓氏,性别,等等),具体文件如下:
<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>
TheJSONfile,donut.json,comesfromtheGitHubaccountofAdobeand
containsdonutinformation(type,price/unitthatis,ppu,topping,andsoon)asfollows:
第二个是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" }
]
}
]
WewillusetwolibrariesthatarepartofthePythondistributionforworkingwithXMLandJSON:
xml.etree.ElementTreeandjsonasfollows:
我们将使用Python的XML库和JSON库来实现功能,它们是xml.etree.elementtree和json,具体引入代码如下:
import xml.etree.ElementTree as etree
import json
TheJSONConnectorclassparsestheJSONfileandhasaparsed_data()method
thatreturnsalldataasadictionary(dict).Thepropertydecoratorisusedto
makeparsed_data()appearasanormalvariableinsteadofamethodasfollows:
我们使用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
TheXMLConnectorclassparsestheXMLfileandhasaparsed_data()methodthat
returnsalldataasalistofxml.etree.Elementasfollows:
我们使用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
Theconnection_factory()functionisaFactoryMethod.Itreturnsaninstance
ofJSONConnectororXMLConnectordependingontheextensionoftheinputfilepath
asfollows:
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)
Theconnect_to()functionisawrapperofconnection_factory().Itaddsexceptionhandling
asfollows:
connect_to()方法封装了connection_factory(),并且增加了异常处理功能,具体代码如下:
def connect_to(filepath):
factory = None
try:
factory = connection_factory(filepath)
except ValueError as ve:
print(ve)
return factory
Themain()functiondemonstrateshowtheFactoryMethoddesignpatterncanbeused.
Thefirstpartmakessurethatexceptionhandlingiseffectiveasfollows:
main()方法演示如何使用工厂方法,方法的第一部分测试异常处理的有效性,具体代码如下:
def main():
sqlite_factory = connect_to('data/person.sq3')
ThenextpartshowshowtoworkwiththeXMLfilesusingtheFactoryMethod.XPathisusedto
findallpersonelementsthathavethelastnameLiar.Foreachmatchedperson,
thebasicnameandphonenumberinformationareshownasfollows:
跟着那部分代码演示如何使用工厂方法来解析这个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')]
ThefinalpartshowshowtoworkwiththeJSONfilesusingtheFactoryMethod.Here,
there'snopatternmatching,andthereforethename,price,andtoppingofalldonuts
areshownasfollows:
最后一部分的代码演示如何使用工厂方法来解析这个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']]
Forcompleteness,hereisthecompletecodeoftheFactoryMethodimplementation
(factory_method.py)asfollows:
下面是完整的工厂方法实现代码(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()
Hereistheoutputofthisprogramasfollows:
下面是程序的输出,具体如下:
>>> 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
NoticethatalthoughJSONConnectorandXMLConnectorhavethesameinterfaces,
whatisreturnedbyparsed_data()isnothandledinauniformway.Differentpythoncode
mustbeusedtoworkwitheachconnector.Althoughitwouldbenicetobeableto
usethesamecodeforallconnectors,thisisatmosttimesnotrealisticunless
weusesomekindofcommonmappingforthedatawhichisveryoftenprovided
byexternaldataproviders.Assumingthatyoucanuseexactlythesamecode
forhandlingtheXMLandJSONfiles,whatchangesarerequiredtosupportathirdformat,
forexample,SQLite?FindanSQLitefileorcreateyourownandtryit.
注意,虽然JSONConnector和XMLConnector具有相同的接口,但是,parsed_data()返回的数据不是以统一的方式处理。不同的Python代码必须使用不同的连接器工作。虽然所有连接器能使用相同的代码是非常好的,但是在大多数时间里,这是不现实的,除非我们使用数据供应商提供的常见映射来处理数据。假设,你是使用相同的代码来处理XML和JSON文件,现在需要增加处理第三种文件格式,例如,SQLite呢?你可以找一个SQLite文件,然后尝试一下。
Asitisnow,thecodedoesnotforbidadirectinstantiationofaconnector.Isitpossibletodothis?Trydoingit.
到现在为止,代码都不禁止直接实例化一个连接器。它有可能用这种方式实现吗?你可以自己尝试一下。
Tip
提示
Hint:FunctionsinPythoncanhavenestedclasses.
提示:在Python函数可以有嵌套类。
译者:由于水平有限,暂时只能翻译成这样子了,请大家指出相应的问题,谢谢。