`
李灵晖-raylee
  • 浏览: 128628 次
博客专栏
Group-logo
从头认识java
浏览量:0
文章分类
社区版块
存档分类
最新评论

翻译-精通python设计模式-工厂模式

 
阅读更多

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.

如果你已经意识到,由于对象的创建存在于代码的各种地方,而使得你不能踪管理它们,这个时候你就应该考虑使用工厂方法模式,通过统一函数/方法来创建管理它们。使用工厂方法可以集中创建对象,并且更加容易跟踪管理它们。请注意,在实际操作当中,人们通常会建立多个工厂方法。然后把工厂方法逻辑分组,创建相类似对象的方法放在一个工厂里面。例如,一个工厂方法可能负责连接到不同的数据库(MySQLSQLite),另一个工厂方法可能负责创造你请求的几何对象(圆,三角形),等等。

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.

数据在现实中是以多种形式存在。一般用于存储或者检索数据的文件格式主要有两种:文字文件格式和二进制文件格式。而文字文件的常用格式有:XMLAtom,YAML和JSON。二进制文件的常用格式有:用于SQLite存储数据的.sq3文件格式,用于保存音乐数据的.mp3文件格式。

Inthisexample,wewillfocusontwopopularhuman-readableformats:XMLandJSON.

Althoughhuman_readablefilesaregenerallyslowertoparsethanbinaryfiles,

theymakedataexchange,inspection,andmodificationmucheasier.Forthisreason,

itisadvisedtopreferworkingwithhuman_readablefiles,unlessthereareotherrestrictions

thatdonotallowit(mainlyunacceptableperformanceandproprietarybinaryformats).

在下面的例子中,我们将集中介绍XMLJSON这两种文字文件格式。虽然文字文件的解析速度一般比二进制文件慢,但它对于数据交换、检查和修改来得更简单。出于这个理由,我们推荐人们在工作的时候使用文字文件,除非有其他限制(一般是不能接收的性能问题或者是需要专有二进制格式)。

Inthisproblem,wehavesomeinputdatastoredinanXMLandaJSONfile,

andwewanttoparsethemandretrievesomeinformation.Atthesametime,

wewanttocentralizetheclient'sconnectiontothose(andallfuture)externalservices.

WewillusetheFactoryMethodtosolvethisproblem.

TheexamplefocusesonlyonXMLandJSON,butaddingsupportformoreservices

shouldbestraightforward.

在下面的例子中,我们先输入一些数据,将它们分别存储在XML文件和JSON文件里面,然后解析它们,并且检索某些信息。同时,我们需要把解析这一部分外接服务集中起来管理。我们将使用工厂方法来解决这个问题。虽然这个例子仅仅只是XMLJSON解析,但是当应用程序添加更多支持服务的时候,代码必须具备良好的扩展能力。

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:

我们将使用PythonXML库和JSON库来实现功能,它们是xml.etree.elementtreejson具体引入代码如下:


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.

注意,虽然JSONConnectorXMLConnector具有相同的接口,但是,parsed_data()返回的数据不是以统一的方式处理。不同的Python代码必须使用不同的连接器工作。虽然所有连接器能使用相同的代码是非常好的,但是在大多数时间里,这是不现实的,除非我们使用数据供应商提供的常见映射来处理数据。假设,你是使用相同的代码来处理XMLJSON文件,现在需要增加处理第三种文件格式,例如,SQLite呢?你可以找一个SQLite文件,然后尝试一下。

Asitisnow,thecodedoesnotforbidadirectinstantiationofaconnector.Isitpossibletodothis?Trydoingit.

到现在为止,代码都不禁止直接实例化一个连接器。它有可能用这种方式实现吗?你可以自己尝试一下。

Tip


提示


Hint:FunctionsinPythoncanhavenestedclasses.


提示:在Python函数可以有嵌套类。



译者:由于水平有限,暂时只能翻译成这样子了,请大家指出相应的问题,谢谢。

分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics