创建型设计模式处理对象创建相关的问题,目标是当直接创建对象不太方便时,提供更好的方式。
在工厂设计模式中,客户端可以请求一个对象,而无需知道这个对象来自哪里;也就是, 使用哪个类来生成这个对象。
工厂背后的思想是简化对象的创建。与客户端自己基于类实例化直接创建对象相比,基于一个中心化函数来实现,更易于追踪创建了哪些对象。通过将创建对象的代码和使用对象的代码解耦,工厂能够降低应用维护的复杂度。
工厂通常有两种形式:
- 一种是工厂方法(Factory Method),它是一个方法(一个函数),对不同的输入参数返回不同的对象;
- 第二种是抽象工厂,它是一组用于创建一系列相关事物对象的工厂方法。
工厂方法
在工厂方法模式中,执行单个函数,传入一个参数(提供信息表明我们想要什么),但并不要求知道任何关于对象如何实现以及对象来自哪里的细节。
创建多个工厂方法也完全没有问题,对相似的对象创建进行逻辑分组,每个工厂方法负责一个分组。例如, 有一个工厂方法负责连接到不同的数据库(MySQL、SQLite),另一个工厂方法负责创建要求的 几何对象(圆形、三角形),等等。
现实生活中的例子:制造塑料玩具的压塑粉都是一样 的,但使用不同的塑料模具就能产出不同的外形。比如,有一个工厂方法,输入是目标外形(鸭子或小车)的名称,输出则是要求的塑料外形。
实践:有一些输入数据存储在一个XML文件和一个JSON文件中,要对这两个文件进行解析,获取一些信息。同时,希望能够对这些(以及将来涉及的所有)外部服务进行集中式的客户端连接。使用工厂方法来解决这个问题。虽然仅以XML和JSON为例,但为更多的服务添加支持也很简单。
import sys
import json
import xml.etree.ElementTree as etree
# 类JSONConnector解析JSON文件
class JsonConnector(object):
def __init__(self, filepath):
self.data = dict()
with open(filepath, 'r') as f:
self.data = json.load(f)
@property
def parse_data(self):
return self.data
# 类XMLConnector解析 XML 文件
class XmlConnector(object):
def __init__(self, filepath):
self.tree = etree.parse(filepath)
# 通过parsed_data()方法以一个字典(dict)的形式 返回数据。修饰器property使parsed_data()显得更像一个常规的变量,而不是一个方法
@property
def parse_data(self):
return self.tree
# 函数connection_factory是一个工厂方法,基于输入文件路径的扩展名返回一个JSONConnector或XMLConnector的实例
def connect_factary(filepath):
try:
if filepath.find("json") >= 0:
return JsonConnector(filepath)
elif filepath.find("xml") >= 0:
return XmlConnector(filepath)
except Exception as e:
print(str(e))
# 函数main()演示如何使用工厂方法设计模式
def main():
xml_factory = connect_factary("person.xml")
xml_data = xml_factory.parse_data
liars = xml_data.findall(".//{person}[{lastName}='{}']".format('Liar'))
print('found: {} persons'.format(len(liars)))
json_factory = connect_factary('donut.json')
# parse_data不是以方法的形式调用,而是变量。
json_data = json_factory.parse_data
print('found: {} donuts'.format(len(json_data)))
虽然JSONConnector和XMLConnector拥有相同的接口,但是对于parsed_data() 返回的数据并不是以统一的方式进行处理。对于每个连接器,需使用不同的Python代码来处理。 若能对所有连接器应用相同的代码当然最好,但是在多数时候这是不现实的,除非对数据使用某种共同的映射,这种映射通常是由外部数据提供者提供。即使假设可以使用相同的代码来处理 XML和JSON文件,当需要支持第三种格式(例如,SQLite)时,又该对代码作哪些改变呢?
像现在这样,代码并未禁止直接实例化一个连接器。如果要禁止直接实例化,是否可以实现?答案:Python中的函数可以内嵌类。
抽象工厂
抽象工厂设计模式是抽象方法的一种泛化。概括来说,一个抽象工厂是(逻辑上的)一组工厂方法,其中的每个工厂方法负责产生不同种类的对象。
现实生活中的例子:汽车制造业应用了抽象工厂的思想。冲压不同汽车模型的部件(车门、仪表盘、车篷、挡泥 板及反光镜等)所使用的机件是相同的。机件装配起来的模型随时可配置,且易于改变。
因为抽象工厂模式是工厂方法模式的一种泛化,所以它能提供相同的好处:让对象的创建更
容易追踪;将对象创建与使用解耦;提供优化内存占用和应用性能的潜力。
我们怎么知道何时该使用工厂方法,何时又该使用抽象工厂?答案是, 通常一开始时使用工厂方法,因为它更简单。如果后来发现应用需要许多工厂方法,那么将创建一系列对象的过程合并在一起更合理,从而最终引入抽象工厂。
抽象工厂有一个优点,在使用工厂方法时从用户视角通常是看不到的,那就是抽象工厂能够 通过改变激活的工厂方法动态地(运行时)改变应用行为。一个经典例子是能够让用户在使用应 用时改变应用的观感(比如,Apple风格和Windows风格等),而不需要终止应用然后重新启动。
实践:一个应用至少包含两个游戏, 一个面向孩子,一个面向成人。在运行时,基于用户输入,决定该创建哪个游戏并运行。游戏的创建部分由一个抽象工厂维护。
#!/usr/bin/env python
# -*- coding:utf-8 -*
import sys
# 主人公是一只青蛙
class Frog(object):
def __init__(self, name):
self.name = name
def __str__(self):
return self.name
# interact_with()用于描述青蛙与障碍物(比如,虫子、迷宫或其他青蛙)之间的交互
def interact_with(self, obstacle):
print("%s Frog encounters %s and %s" % (self, obstacle, obstacle.action()))
class Bug(object):
def __str__(self):
return "a Bug"
# 当青蛙遇到一只虫子,只支持一 种动作,那就是吃掉它!
def action(self):
print("eats it!!")
# 孩子游戏,
# 类FrogWorld是一个抽象工厂,其主要职责是创建游戏的主人公和障碍物
# 区分创建方法并使其名字通用(比如,make_character()和make_obstacle()),
# 这让我们可以动态改变当前激活的工厂(也因此改变了当前激活的游戏),而无需进行任何代码变更。
# 在一门静态语言中, 抽象工厂是一个抽象类/接口,具备一些空方法,但在Python中无需如此,因为类型是在运行时检测的.
class FrogWord:
def __init__(self, name):
print(self)
self.player_name = name
def __str__(self):
return "--FrogWorld--"
def make_character(self):
return Frog(self.player_name)
def make_obstacle(self):
return Bug()
# 成人游戏
# WizardWorld游戏也类似。在故事中唯一的区别是男巫战怪兽(如兽人)而不是吃虫子
class Wizard(object):
def __init__(self, name):
self.name = name
def __str__(self):
return self.name
def interact_with(self, obstacle):
print("%s Wizard encounters %s and %s" % (self, obstacle, obstacle.action()))
class Ord(object):
def __init__(self):
pass
def __str__(self):
return "an evil ork"
def action(self):
return "kills it"
class WizardWrold:
def __init__(self, name):
self.player_name = name
def make_character(self):
return Wizard(self.player_name)
def make_obstacle(self):
return Ord()
# 类GameEnvironment是我们游戏的主入口。它接受factory作为输入,用其创建游戏的世界。
class GameEnvironment:
def __init__(self, factory):
self.hero = factory.make_character()
self.obstacle = factory.make_obstacle()
# 方法play()则会启动hero和obstacle之间的交互,如下所示:
def play(self):
self.hero.interact_with(self.obstacle)
# main()函数,该函数请求用户的姓名和年龄,并根据用户的年龄决定该玩 哪个游戏
def main():
name = input("please input the role name")
age = input("please input your age")
if int(age) > 18:
factory = FrogWord(name)
else:
factory = WizardWrold(name)
game = GameEnvironment(factory)
game.play()
if __name__ == '__main__':
main()
原型模式
有时需要原原本本地为对象创建一个副本。
假设创建一个应用来存储、 分享、编辑食谱。Bob找到一份蛋糕食谱,在做了一些改变后,觉得自己做的蛋糕非常美味,想要与Alice分享这个食谱。但是该如何分享食谱呢?如果在与Alice分享之后,Bob想对食谱做进一步的试验,Alice手里的食谱也能跟着变化吗?Bob能够持有蛋糕食谱的两个副本吗?对蛋糕食谱进行的试验性变更不应该对原本美味蛋糕的食谱造成影响。
这样的问题可以通过让用户对同一份食谱持有多个独立的副本来解决。每个副本被称为一个克隆,是某个时间点原有对象的一个完全副本。时间是一个重要因素。因为它会影响克隆所包含的内容。
注意引用与副本之间的区别(图左边是引用,右边是副本):
- 若Bob和Alice持有的是同一个蛋糕食谱对象的两个引用,那 么Bob对食谱做的任何改变,对于Alice的食谱版本都是可见的,反之亦然。
- 我们想要的是Bob和 Alice各自持有自己的副本,这样他们可以各自做变更而不会影响对方的食谱。
原型设计模式(Prototype design pattern)帮助创建对象的克隆。在Python中,可使用copy.deepcopy()函数来完成。
#!/usr/bin/env python
# -*- coding:utf-8 -*
import copy
# A是父类
class A:
def __init__(self):
self.x = 18
self.msg = 'Hello'
# B是衍生类/子类
class B(A):
def __init__(self):
A.__init__(self)
self.y = 34
def __str__(self):
return '{}, {}, {}'.format(self.x, self.msg, self.y)
if __name__ == '__main__':
b = B()
# 创建一个类B的实例b,使用deepcopy() 创建b的一个克隆c。结果是所有成员都被复制到了克隆c。
c = copy.deepcopy(b)
print([str(i) for i in (b, c)])
print([i for i in (b, c)])
输出
['18, Hello, 34', '18, Hello, 34']
[<__main__.B object at 0x7fcfb020afd0>, <__main__.B object at 0x7fcfb020adc0>]
现实中的例子:
有丝分裂,即细胞分裂的过程,是生物克隆的一个例 子。在这个过程中,细胞核分裂产生两个新的细胞核,其中每个都有与原来细胞完全相同的染色体和DNA内容。
应用场景:
- 当我们已有一个对象,并希望创建该对象的一个完整副本时,原型模式就派上用场了。在知道对象的某些部分会被变更但又希望保持原有对象不变之时,通常需要对象的一个副本。
- 另一个案例是,当我们想复制一个复杂对象时,使用原型模式会很方便
副本又可以进一步分为深副本与浅副本:
- 深副本就是目前为止所看到的:原始对象的所有数据都被简单地复制到克隆对象中,没有例外。
- 浅副本则依赖引用。我们可以引入数据共享和写时复制一类的技术来优化性能(例如, 减小克隆对象的创建时间)和内存使用。如果可用资源有限(例如,嵌入式系统)或性能至关重要(例如,高性能计算),那么使用浅副本可能更佳。
浅副本(copy.copy())和深副本(copy.deepcopy())之间的区别:
- 浅副本构造一个新的复合对象后,(会尽可能地)将在原始对象中找到的对象的引用插入新对象中。
- 深副本构造一个新的复合对象后,会递归地将在原始对象中找到的对象的副本插入新对象中。
实践
import copy
from collections import OrderedDict
class Book(object):
# 除了常规的初始化之外,Book类展示了一种有趣的技术可避免可伸缩构造器问题。
# 仅有三个形参是固定的:name、authors和price
# 使用rest变长列表,调用者能以关键词的形式(名称=值)传入更多的参数
def __init__(self, name, authors, price, **rest):
self.name = name
self.authors = authors
self.price = price
# self.__dict__.update(rest)一行将rest 的内容添加到Book类的内部字典中,成为它的一部分
self.__dict__.update(rest)
def __str__(self):
mylist = []
# 字典的内容并不遵循任何特定的顺序,所以使用一个OrderedDict来强制元素有序,否则,每次程序执行都会产生不同的输出。
ordered = OrderedDict(sorted(self.__dict__.items()))
for i in ordered.keys():
mylist.append("%s:%s" % (i, ordered[i]))
return ",".join(mylist)
class Prototype(object):
def __init__(self):
self.objects = dict()
# 它包含了方法register()和unregister(),这两个方法用于在一个字典中追踪被克隆的对象。这仅是一个方便之举,并非必需。
def register(self, identifier, obj):
self.objects[identifier] = obj
def unregister(self, identifier):
del self.objects[identifier]
def clone(self, identifier, **attr):
origin_obj = self.objects.get(identifier)
if origin_obj is None:
return
# 该方法使用copy.deepcopy()函数来完成真正的克隆工作
new_obj = copy.deepcopy(origin_obj)
# 和上面book类的str定义相同技术,使用变长列表attr,可以仅传递那些在克隆一个对象时真正需要变更的属性变量。
new_obj.__dict__.update(attr)
return new_obj
def main():
book = Book("computer", "debby_author", 10, length=20, high=10)
print(book)
prototype = Prototype()
prototype.register("computer1", book)
new_book = prototype.clone("computer1", price=20)
print(new_book)
print(id(book), id(new_book))
if __name__ == '__main__':
main()
输出:
authors:debby_author,high:10,length:20,name:computer,price:10
authors:debby_author,high:10,length:20,name:computer,price:20
140510131652976 140510131651968
原型模式用于创建对象的完全副本。创建一个对象的副本可以指代以下两件事情。
- 当创建一个浅副本时,副本依赖引用-》关注提升应用性能和优化内存使用,在对象之间引入数据共享,但需小心地修改数据,因为所有变更对所有副本都是可见的。
- 当创建一个深副本时,副本复制所有东西-》希望能够对一个副本进行更改而不会影响其他对象。这里不会进行数据共享,所以需要关注因对象克隆而引入的资源耗用问题。