python 实现 工厂模式

本文目录地址

本文代码地址

创建型设计模式处理对象创建相关的问题,目标是当直接创建对象(在Python中是通过__init__()函数实现的)不太方便时,提供更好的方式。
在工厂设计模式中,客户端①可以请求一个对象,而无需知道这个对象来自哪里;也就是,使用哪个类来生成这个对象。工厂背后的思想是简化对象的创建。与客户端自己基于类实例化直接创建对象相比,基于一个中心化函数来实现,更易于追踪创建了哪些对象。通过将创建对象的代码和使用对象的代码解耦,工厂能够降低应用维护的复杂度。
工厂通常有两种形式:一种是工厂方法(Factory Method),它是一个方法(或以地道的Python术语来说,是一个函数),对不同的输入参数返回不同的对象;第二种是抽象工厂,它是一组用于创建一系列相关事物对象的工厂方法。

目录

1.1 工厂方法

1.1.1应用案例

1.1.2实现

1.2抽象工厂

1.2.1现实生活的例子

1.2.2应用案例

1.2.3实现

扫描二维码关注公众号,回复: 3899213 查看本文章

1.3小结


1.1 工厂方法

在工厂方法模式中,我们执行单个函数,传入一个参数(提供信息表明我们想要什么),但并不要求知道任何关于对象如何实现以及对象来自哪里的细节。

1.1.1应用案例

如果因为应用创建对象的代码分布在多个不同的地方,而不是仅在一个函数/方法中,你发现没法跟踪这些对象,那么应该考虑使用工厂方法模式。工厂方法集中地在一个地方创建对象,使对象跟踪变得更容易。注意,创建多个工厂方法也完全没有问题,实践中通常也这么做,对相似的对象创建进行逻辑分组,每个工厂方法负责一个分组。例如,有一个工厂方法负责连接到不同的数据库(MySQL、SQLite),另一个工厂方法负责创建要求的几何对象(圆形、三角形),等等。
若需要将对象的创建和使用解耦,工厂方法也能派上用场。创建对象时,我们并没有与某个特定类耦合/绑定到一起,而只是通过调用某个函数来提供关于我们想要什么的部分信息。这意味着修改这个函数比较容易,不需要同时修改使用这个函数的代码。
另外一个值得一提的应用案例与应用性能及内存使用相关。工厂方法可以在必要时创建新的对象,从而提高性能和内存使用率。若直接实例化类来创建对象,那么每次创建新对象就需要分配额外的内存(除非这个类内部使用了缓存,一般情况下不会这样)。用行动说话,下面的代码(文件id.py)对同一个类A创建了两个实例,并使用函数id()比较它们的内存地址。输出中也会包含地址,便于检查地址是否正确。内存地址不同就意味着创建了两个不同的对象。

class A():
    pass

a=A()
b=A()

print(id(a)==id(b))
print(a,b)

输出

False
<__main__.A object at 0x7fbeefe5ef98> <__main__.A object at 0x7fbeefe5efd0>

注意,你执行这个代码文件看到的地址会与我看到的不一样,因为这依赖程序运行时内存的布局和分配。但结果中有一点肯定是一样的,那就是两个地址不同。在Python Read-Eval-Print Loop(REPL)模式(即交互式提示模式)下编写运行这段代码时会出现例外,但这只是交互模式特有的优化,并不常见。

1.1.2实现

数据来源可以有多种形式。存取数据的文件主要有两种分类:人类可读文件和二进制文件。人类可读文件的例子有:XML、Atom、YAML和JSON。二进制文件的例子则有SQLite使用的.sq3文件格式,及用于听音乐的.mp3文件格式。
以下例子将关注两种流行的人类可读文件格式:XML和JSON。虽然人类可读文件解析起来通常比二进制文件更慢,但更易于数据交换、审查和修改。基于这种考虑,建议优先使用人类可读文件,除非有其他限制因素不允许使用这类格式(主要的限制包括性能不可接受以及专有的二进制格式)。
在当前这个问题中,我们有一些输入数据存储在一个XML文件和一个JSON文件中,要对这两个文件进行解析,获取一些信息。同时,希望能够对这些(以及将来涉及的所有)外部服务进行集中式的客户端连接。我们使用工厂方法来解决这个问题。虽然仅以XML和JSON为例,但为更多的服务添加支持也很简单。

首先,来看一看数据文件。基于Wikipedia例子的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账号,包含甜甜圈(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()显得更像一个常规的变量,而不是一个方法。类XMLConnector解析 XML 文件,通过parsed_data()方法以xml.etree.Element列表的形式返回所有数据,如下所示。

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

函数connection_factory是一个工厂方法,基于输入文件路径的扩展名返回一个JSONConnector或XMLConnector的实例。

函数connect_to()对connection_factory()进行包装,添加了异常处理,如下所示。

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)

def connect_to(filepath):
    factory=None
    try:
        factory=connector_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(".//{}[{}='{}']".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')]

最后一部分演示如何使用工厂方法处理JSON文件。这里没有模式匹配,因此所有甜甜圈的name、price和topping如下所示。

    json_factory=connect_to('data/donut.json')
    json_data=json_factory.parsed_data
    print('found: {} dnouts'.format(len(json_data)))
    for donut in  json_data:
        print('name: {}'.format(donut['name']))
        print('price: ${}'.format(donut['ppu']))
        [print('topings: \t{}\t{}'.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 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)


def connect_to(filepath):
    factory = None
    try:
        factory = connector_factory(filepath)
    except ValueError as ve:
        print(ve)
    return factory


def main():
    sqlite_factory = connect_to('data/person.sq3')

    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')]

    json_factory = connect_to('data/donut.json')
    json_data = json_factory.parsed_data
    print('found: {} dnouts'.format(len(json_data)))
    for donut in json_data:
        print('name: {}'.format(donut['name']))
        print('price: ${}'.format(donut['ppu']))
        [print('topings: \t{}\t{}'.format(t['id'], t['type']))
         for t in donut['topping']]


main()

输出

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 dnouts
name: Cake
price: $0.55
topings: 	5001	None
topings: 	5002	Glazed
topings: 	5005	Sugar
topings: 	5007	Powdered Sugar
topings: 	5006	Chocolate with Sprinkles
topings: 	5003	Chocolate
topings: 	5004	Maple
name: Raised
price: $0.55
topings: 	5001	None
topings: 	5002	Glazed
topings: 	5005	Sugar
topings: 	5003	Chocolate
topings: 	5004	Maple
name: Old Fashioned
price: $0.55
topings: 	5001	None
topings: 	5002	Glazed
topings: 	5003	Chocolate
topings: 	5004	Maple

注意,虽然JSONConnector和XMLConnector拥有相同的接口,但是对于parsed_data()返回的数据并不是以统一的方式进行处理。对于每个连接器,需使用不同的Python代码来处理。若能对所有连接器应用相同的代码当然最好,但是在多数时候这是不现实的,除非对数据使用某种共同的映射,这种映射通常是由外部数据提供者提供。即使假设可以使用相同的代码来处理XML和JSON文件,当需要支持第三种格式(例如,SQLite)时,又该对代码作哪些改变呢?找一个SQlite文件或者自己创建一个,尝试一下。

像现在这样,代码并未禁止直接实例化一个连接器。如果要禁止直接实例化,是否可以实现?试试看。

提示:Python中的函数可以内嵌类。

1.2抽象工厂

抽象工厂设计模式是抽象方法的一种泛化。概括来说,一个抽象工厂是(逻辑上的)一组工厂方法,其中的每个工厂方法负责产生不同种类的对象。

1.2.1现实生活的例子

汽车制造业应用了抽象工厂的思想。冲压不同汽车模型的部件(车门、仪表盘、车篷、挡泥板及反光镜等)所使用的机件是相同的。机件装配起来的模型随时可配置,且易于改变。从下图我们能看到汽车制造业抽象工厂的一个例子,该图由www.sourcemaking.com提供。

1.2.2应用案例

因为抽象工厂模式是工厂方法模式的一种泛化,所以它能提供相同的好处:让对象的创建更容易追踪;将对象创建与使用解耦;提供优化内存占用和应用性能的潜力。
这样会产生一个问题:我们怎么知道何时该使用工厂方法,何时又该使用抽象工厂?答案是,通常一开始时使用工厂方法,因为它更简单。如果后来发现应用需要许多工厂方法,那么将创建一系列对象的过程合并在一起更合理,从而最终引入抽象工厂。
抽象工厂有一个优点,在使用工厂方法时从用户视角通常是看不到的,那就是抽象工厂能够通过改变激活的工厂方法动态地(运行时)改变应用行为。一个经典例子是能够让用户在使用应用时改变应用的观感(比如,Apple风格和Windows风格等),而不需要终止应用然后重新启动。

1.2.3实现

为演示抽象工厂模式。想象一下,我们正在创造一个游戏,或者想在应用中包含一个迷你游戏让用户娱乐娱乐。我们希望至少包含两个游戏,一个面向孩子,一个面向成人。在运行时,基于用户输入,决定该创建哪个游戏并运行。游戏的创建部分由一个抽象工厂维护。
从孩子的游戏说起,我们将该游戏命名为FrogWorld。主人公是一只青蛙,喜欢吃虫子。每个英雄都需要一个好名字, 在我们的例子中, 这个名字在运行时由用户给定。方法interact_with()用于描述青蛙与障碍物(比如,虫子、迷宫或其他青蛙)之间的交互,如下所示。

障碍物可以有多种,但对于我们的例子,可以仅仅是虫子。当青蛙遇到一只虫子,只支持一种动作,那就是吃掉它!

class Frog:
    def __init__(self,name):
        self.name=name
        
    def __str__(self):
        return self.name
    
    def interact_with(self,obstacle):
        print('{} the Frog encounters {} and {}!'.format(self,obstacle,obstacle.action()))
        
class Bug:
    def __init__(self):
        return 'a bug'
    def action(self):
        return "eats it"

类FrogWorld是一个抽象工厂,其主要职责是创建游戏的主人公和障碍物。区分创建方法并使其名字通用(比如,make_character()和make_obstacle()),这让我们可以动态改变当前激活的工厂(也因此改变了当前激活的游戏),而无需进行任何代码变更。在一门静态语言中,抽象工厂是一个抽象类/接口,具备一些空方法,但在Python中无需如此,因为类型是在运行时检测的,如下所示

class FrogWorld:
    def __init__(self,name):
        print(self)
        self.player_name=name
        
    def __str__(self):
        return '\n\n\t----------Frog World-----------'
    def make_character(self):
        return  Frog(self.player_name)
    def make_obstacle(self):
        return Bug()

WizardWorld游戏也类似。在故事中唯一的区别是男巫战怪兽(如兽人)而不是吃虫子!

class Wizard:
    def __init__(self,name):
        self.name=name
        
    def __str__(self):
        return self.name
    def interact_with(self,obstacle):
        print('{} the Wizard battles against {} and {}!'.format(self,obstacle,obstacle.action()))
        
class Ork:
    def __str__(self):
        return 'an evil ork'
    def action(self):
        return 'kills it'
class WizardWorld:
    def __init__(self,name):
        print(self)
        self.player_name=name
    def __str__(self):
        return '\n\n\t-------------Wizard World---------------'

    def make_character(self):
        return Wizard(self.player_name)

    def make_obstacle(self):
        return Ork()

类GameEnvironment是我们游戏的主入口。它接受factory作为输入,用其创建游戏的世界。方法play()则会启动hero和obstacle之间的交互,如下所示。

class GameEnvironment:
    def __init__(self,factory):
        self.hero=factory.make_character()
        self.obstacle=factory.make_obstacle()
    def play(self):
        self.hero.interact_with(self.obstacle)

函数validate_age()提示用户提供一个有效的年龄。如果年龄无效,则会返回一个元组,其第一个元素设置为False。如果年龄没问题,元素的第一个元素则设置为True,但我们真正关心的是元素的第二个元素,也就是用户提供的年龄,如下所示。

def validate_age(name):
    try:
        age=input('Welcome {}. How old are you?'.format(name))
        age=int(age)
    except ValueError as err:
        print("Age {} is invalid, please try again...".format(age))
        return (False,age)
    return (True,age)

最后一个要点是main()函数,该函数请求用户的姓名和年龄,并根据用户的年龄决定该玩哪个游戏,如下所示。

def main():
    name=input("hello, What's your name?")
    valid_input=False
    while not valid_input:
        valid_input, age=validate_age(name)
    game=FrogWorld if age<18 else WizardWorld
    environment=GameEnvironment(game(name))
    environment.play()

抽象工厂实现的完整代码(abstract_factory.py)如下所示。

class Frog:
    def __init__(self, name):
        self.name = name

    def __str__(self):
        return self.name

    def interact_with(self, obstacle):
        print('{} the Frog encounters {} and {}!'.format(self, obstacle, obstacle.action()))


class Bug:
    def __init__(self):
        return 'a bug'

    def action(self):
        return "eats it"


class FrogWorld:
    def __init__(self, name):
        print(self)
        self.player_name = name

    def __str__(self):
        return '\n\n\t----------Frog World-----------'

    def make_character(self):
        return Frog(self.player_name)

    def make_obstacle(self):
        return Bug()


class Wizard:
    def __init__(self, name):
        self.name = name

    def __str__(self):
        return self.name

    def interact_with(self, obstacle):
        print('{} the Wizard battles against {} and {}!'.format(self, obstacle, obstacle.action()))


class Ork:
    def __str__(self):
        return 'an evil ork'

    def action(self):
        return 'kills it'


class WizardWorld:
    def __init__(self, name):
        print(self)
        self.player_name = name

    def __str__(self):
        return '\n\n\t-------------Wizard World---------------'

    def make_character(self):
        return Wizard(self.player_name)

    def make_obstacle(self):
        return Ork()


class GameEnvironment:
    def __init__(self, factory):
        self.hero = factory.make_character()
        self.obstacle = factory.make_obstacle()

    def play(self):
        self.hero.interact_with(self.obstacle)


def validate_age(name):
    try:
        age = input('Welcome {}. How old are you?'.format(name))
        age = int(age)
    except ValueError as err:
        print("Age {} is invalid, please try again...".format(age))
        return (False, age)
    return (True, age)


def main():
    name = input("hello, What's your name?")
    valid_input = False
    while not valid_input:
        valid_input, age = validate_age(name)
    game = FrogWorld if age < 18 else WizardWorld
    environment = GameEnvironment(game(name))
    environment.play()


main()

输出

hello, What's your name?hbu
Welcome hbu. How old are you?56


	-------------Wizard World---------------
hbu the Wizard battles against an evil ork and kills it!

来尝试扩展一下这个游戏使其更完整吧。你可以随意添加障碍物、敌人以及其他任何想要的东西。

1.3小结

我们学习了如何使用工厂方法和抽象工厂设计模式。两种模式都可以用于以下几种场景:(a)想要追踪对象的创建时,(b)想要将对象的创建与使用解耦时,(c)想要优化应用的性能和资源占用时。场景(c)并未详细说明,你也许可以将其作为一个练习。
工厂方法设计模式的实现是一个不属于任何类的单一函数,负责单一种类对象(一个形状、一个连接点或者其他对象)的创建。作为示例,我们实现了一个工厂方法,提供了访问XML和JSON文件的能力。
抽象工厂设计模式的实现是同属于单个类的许多个工厂方法用于创建一系列种类的相关对象(一辆车的部件、一个游戏的环境,或者其他对象)。我们提到抽象工厂如何与汽车制造业相关联,并学习了抽象工厂的应用案例。作为抽象工厂实现的示例,我们完成了一个迷你游戏,演示了如何在单个类中使用多个相关工厂。
接下来我们将谈论建造者模式,它是另一种创建型模式,可用于细粒度控制复杂对象的创建过程。

猜你喜欢

转载自blog.csdn.net/hbu_pig/article/details/80432305
今日推荐