原型模式
# TL;DR
Creates new object instances by cloning prototype. 通过克隆原型创建新的对象实例
# 意图
有时,我们需要原原本本地为对象创建一个副本。举例来说,假设你想创建一个应用来存储、分享、编辑(比如,修改、添加注释及删除)食谱。用户 Bob 找到一份蛋糕食谱,在做了一些改变后,觉得自己做的蛋糕非常美味,想要与朋友 Alice 分享这个食谱。但是该如何分享食谱呢?如果在与 Alice 分享之后,Bob 想对食谱做进一步的试验,Alice 手里的食谱也能跟着变化吗?Bob 能 够持有蛋糕食谱的两个副本吗?对蛋糕食谱进行的试验性变更不应该对原本美味蛋糕的食谱造成影响。 这样的问题可以通过让用户对同一份食谱持有多个独立的副本来解决。每个副本被称为一个克隆,是某个时间点原有对象的一个完全副本。这里时间是一个重要因素。因为它会影响克隆所包含的内容。例如,如果 Bob 在对蛋糕食谱做改进以臻完美之前就与 Alice 分享了,那么 Alice 就绝不可能像 Bob 那样烘烤出自己的美味蛋糕,只能按照 Bob 原来找到的食谱烘烤蛋糕。 注意引用与副本之间的区别。如果 Bob 和 Alice 持有的是同一个蛋糕食谱对象的两个引用,那么 Bob 对食谱做的任何改变,对于 Alice 的食谱版本都是可见的,反之亦然。我们想要的是 Bob 和 Alice 各自持有自己的副本,这样他们可以各自做变更而不会影响对方的食谱。实际上 Bob 需要蛋糕食谱的两个副本:美味版本和试验版本。
原型模式是一种创建型设计模式, 使你能够复制已有对象, 而又无需使代码依赖它们所属的类。
# 问题
如果你有一个对象,并希望生成与其完全相同的一个复制品, 你该如何实现呢? 首先,你必须新建一个属于相同类的对象。 然后,你必须遍历原始对象的所有成员变量, 并将成员变量值复制到新对象中。
不错!但有个小问题。并非所有对象都能通过这种方式进行复制,因为有些对象可能拥有私有成员变量,它们在对象本身以外是不可见的。
“从外部” 复制对象并非 (opens new window)总是可行。
直接复制还有另外一个问题。 因为你必须知道对象所属的类才能创建复制品, 所以代码必须依赖该类。 即使你可以接受额外的依赖性, 那还有另外一个问题: 有时你只知道对象所实现的接口, 而不知道其所属的具体类, 比如可向方法的某个参数传入实现了某个接口的任何对象。
# 解决方案
原型模式将克隆过程委派给被克隆的实际对象。 模式为所有支持克隆的对象声明了一个通用接口, 该接口让你能够克隆对象, 同时又无需将代码和对象所属类耦合。 通常情况下, 这样的接口中仅包含一个 克隆
方法。
所有的类对 克隆
方法的实现都非常相似。 该方法会创建一个当前类的对象, 然后将原始对象所有的成员变量值复制到新建的类中。 你甚至可以复制私有成员变量, 因为绝大部分编程语言都允许对象访问其同类对象的私有成员变量。
支持克隆的对象即为_原型_。 当你的对象有几十个成员变量和几百种类型时, 对其进行克隆甚至可以代替子类的构造。
预生成原型可以代替子类的构造。
其运作方式如下: 创建一系列不同类型的对象并不同的方式对其进行配置。 如果所需对象与预先配置的对象相同, 那么你只需克隆原型即可, 无需新建一个对象。
# 真实世界类比
现实生活中, 产品在得到大规模生产前会使用原型进行各种测试。 但在这种情况下, 原型只是一种被动的工具, 不参与任何真正的生产活动。
一个细胞的分裂。
由于工业原型并不是真正意义上的自我复制, 因此细胞有丝分裂 (还记得生物学知识吗?) 或许是更恰当的类比。 有丝分裂会产生一对完全相同的细胞。 原始细胞就是一个原型, 它在复制体的生成过程中起到了推动作用。
# 原型模式结构
# 基本实现
原型 (Prototype) 接口将对克隆方法进行声明。 在绝大多数情况下, 其中只会有一个名为
clone
克隆的方法。具体原型 (Concrete Prototype) 类将实现克隆方法。 除了将原始对象的数据复制到克隆体中之外, 该方法有时还需处理克隆过程中的极端情况, 例如克隆关联对象和梳理递归依赖等等。
客户端 (Client) 可以复制实现了原型接口的任何对象。
# 原型注册表实现
- 原型注册表 (Prototype Registry) 提供了一种访问常用原型的简单方法, 其中存储了一系列可供随时复制的预生成对象。 最简单的注册表原型是一个
名称 → 原型
的哈希表。 但如果需要使用名称以外的条件进行搜索, 你可以创建更加完善的注册表版本。
# 伪代码
在本例中, 原型模式能让你生成完全相同的几何对象副本, 同时无需代码与对象所属类耦合。
克隆一系列位于同一类层次结构中的对象。
所有形状类都遵循同一个提供克隆方法的接口。 在复制自身成员变量值到结果对象前, 子类可调用其父类的克隆方法。
// 基础原型。
abstract class Shape is
field X: int
field Y: int
field color: string
// 常规构造函数。
constructor Shape() is
// ……
// 原型构造函数。使用已有对象的数值来初始化一个新对象。
constructor Shape(source: Shape) is
this()
this.X = source.X
this.Y = source.Y
this.color = source.color
// clone(克隆)操作会返回一个形状子类。
abstract method clone():Shape
// 具体原型。克隆方法会创建一个新对象并将其传递给构造函数。直到构造函数运
// 行完成前,它都拥有指向新克隆对象的引用。因此,任何人都无法访问未完全生
// 成的克隆对象。这可以保持克隆结果的一致。
class Rectangle extends Shape is
field width: int
field height: int
constructor Rectangle(source: Rectangle) is
// 需要调用父构造函数来复制父类中定义的私有成员变量。
super(source)
this.width = source.width
this.height = source.height
method clone():Shape is
return new Rectangle(this)
class Circle extends Shape is
field radius: int
constructor Circle(source: Circle) is
super(source)
this.radius = source.radius
method clone():Shape is
return new Circle(this)
// 客户端代码中的某个位置。
class Application is
field shapes: array of Shape
constructor Application() is
Circle circle = new Circle()
circle.X = 10
circle.Y = 10
circle.radius = 20
shapes.add(circle)
Circle anotherCircle = circle.clone()
shapes.add(anotherCircle)
// 变量 `anotherCircle(另一个圆)`与 `circle(圆)`对象的内
// 容完全一样。
Rectangle rectangle = new Rectangle()
rectangle.width = 10
rectangle.height = 20
shapes.add(rectangle)
method businessLogic() is
// 原型是很强大的东西,因为它能在不知晓对象类型的情况下生成一个与
// 其完全相同的复制品。
Array shapesCopy = new Array of Shapes.
// 例如,我们不知晓形状数组中元素的具体类型,只知道它们都是形状。
// 但在多态机制的帮助下,当我们在某个形状上调用 `clone(克隆)`
// 方法时,程序会检查其所属的类并调用其中所定义的克隆方法。这样,
// 我们将获得一个正确的复制品,而不是一组简单的形状对象。
foreach (s in shapes) do
shapesCopy.add(s.clone())
// `shapesCopy(形状副本)`数组中包含 `shape(形状)`数组所有
// 子元素的复制品。
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
# 原型模式适合应用场景
如果你需要复制一些对象, 同时又希望代码独立于这些对象所属的具体类, 可以使用原型模式。
这一点考量通常出现在代码需要处理第三方代码通过接口传递过来的对象时。 即使不考虑代码耦合的情况, 你的代码也不能依赖这些对象所属的具体类, 因为你不知道它们的具体信息。
原型模式为客户端代码提供一个通用接口, 客户端代码可通过这一接口与所有实现了克隆的对象进行交互, 它也使得客户端代码与其所克隆的对象具体类独立开来。
如果子类的区别仅在于其对象的初始化方式, 那么你可以使用该模式来减少子类的数量。 别人创建这些子类的目的可能是为了创建特定类型的对象。
在原型模式中, 你可以使用一系列预生成的、 各种类型的对象作为原型。
客户端不必根据需求对子类进行实例化, 只需找到合适的原型并对其进行克隆即可。
# 实现方式
创建原型接口, 并在其中声明
克隆
方法。 如果你已有类层次结构, 则只需在其所有类中添加该方法即可。原型类必须另行定义一个以该类对象为参数的构造函数。 构造函数必须复制参数对象中的所有成员变量值到新建实体中。 如果你需要修改子类, 则必须调用父类构造函数, 让父类复制其私有成员变量值。
如果编程语言不支持方法重载, 那么你可能需要定义一个特殊方法来复制对象数据。 在构造函数中进行此类处理比较方便, 因为它在调用
new
运算符后会马上返回结果对象。克隆方法通常只有一行代码: 使用
new
运算符调用原型版本的构造函数。 注意, 每个类都必须显式重写克隆方法并使用自身类名调用new
运算符。 否则, 克隆方法可能会生成父类的对象。你还可以创建一个中心化原型注册表, 用于存储常用原型。
你可以新建一个工厂类来实现注册表, 或者在原型基类中添加一个获取原型的静态方法。 该方法必须能够根据客户端代码设定的条件进行搜索。 搜索条件可以是简单的字符串, 或者是一组复杂的搜索参数。 找到合适的原型后, 注册表应对原型进行克隆, 并将复制生成的对象返回给客户端。
最后还要将对子类构造函数的直接调用替换为对原型注册表工厂方法的调用。
# 原型模式优缺点
- 优点
- 你可以克隆对象, 而无需与它们所属的具体类相耦合。
- 你可以克隆预生成原型, 避免反复运行初始化代码。
- 你可以更方便地生成复杂对象。
- 你可以用继承以外的方式来处理复杂对象的不同配置。
- 缺点
- 克隆包含循环引用的复杂对象可能会非常麻烦。
# 与其他模式的关系
在许多设计工作的初期都会使用工厂方法模式 (opens new window) (较为简单, 而且可以更方便地通过子类进行定制), 随后演化为使用抽象工厂模式 (opens new window)、 原型模式 (opens new window)或生成器模式 (opens new window) (更灵活但更加复杂)。
抽象工厂模式 (opens new window)通常基于一组工厂方法 (opens new window), 但你也可以使用原型模式 (opens new window)来生成这些类的方法。
原型 (opens new window)可用于保存命令模式 (opens new window)的历史记录。
大量使用组合模式 (opens new window)和装饰模式 (opens new window)的设计通常可从对于原型 (opens new window)的使用中获益。 你可以通过该模式来复制复杂结构, 而非从零开始重新构造。
原型 (opens new window)并不基于继承, 因此没有继承的缺点。 另一方面, _原型_需要对被复制对象进行复杂的初始化。 工厂方法 (opens new window)基于继承, 但是它不需要初始化步骤。
有时候原型 (opens new window)可以作为备忘录模式 (opens new window)的一个简化版本, 其条件是你需要在历史记录中存储的对象的状态比较简单, 不需要链接其他外部资源, 或者链接可以方便地重建。
抽象工厂 (opens new window)、 生成器 (opens new window)和原型 (opens new window)都可以用单例模式 (opens new window)来实现。
# 示例代码
import copy
class SelfReferencingEntity:
def __init__(self):
self.parent = None
def set_parent(self, parent):
self.parent = parent
class SomeComponent:
"""
Python provides its own interface of Prototype via `copy.copy` and
`copy.deepcopy` functions. And any class that wants to implement custom
implementations have to override `__copy__` and `__deepcopy__` member
functions.
"""
def __init__(self, some_int, some_list_of_objects, some_circular_ref):
self.some_int = some_int
self.some_list_of_objects = some_list_of_objects
self.some_circular_ref = some_circular_ref
def __copy__(self):
"""
Create a shallow copy. This method will be called whenever someone calls
`copy.copy` with this object and the returned value is returned as the
new shallow copy.
"""
# First, let's create copies of the nested objects.
some_list_of_objects = copy.copy(self.some_list_of_objects)
some_circular_ref = copy.copy(self.some_circular_ref)
# Then, let's clone the object itself, using the prepared clones of the
# nested objects.
new = self.__class__(
self.some_int, some_list_of_objects, some_circular_ref
)
new.__dict__.update(self.__dict__)
return new
def __deepcopy__(self, memo=None):
"""
Create a deep copy. This method will be called whenever someone calls
`copy.deepcopy` with this object and the returned value is returned as
the new deep copy.
What is the use of the argument `memo`? Memo is the dictionary that is
used by the `deepcopy` library to prevent infinite recursive copies in
instances of circular references. Pass it to all the `deepcopy` calls
you make in the `__deepcopy__` implementation to prevent infinite
recursions.
"""
if memo is None:
memo = {}
# First, let's create copies of the nested objects.
some_list_of_objects = copy.deepcopy(self.some_list_of_objects, memo)
some_circular_ref = copy.deepcopy(self.some_circular_ref, memo)
# Then, let's clone the object itself, using the prepared clones of the
# nested objects.
new = self.__class__(
self.some_int, some_list_of_objects, some_circular_ref
)
new.__dict__ = copy.deepcopy(self.__dict__, memo)
return new
if __name__ == "__main__":
list_of_objects = [1, {1, 2, 3}, [1, 2, 3]]
circular_ref = SelfReferencingEntity()
component = SomeComponent(23, list_of_objects, circular_ref)
circular_ref.set_parent(component)
shallow_copied_component = copy.copy(component)
# Let's change the list in shallow_copied_component and see if it changes in
# component.
shallow_copied_component.some_list_of_objects.append("another object")
if component.some_list_of_objects[-1] == "another object":
print(
"Adding elements to `shallow_copied_component`'s "
"some_list_of_objects adds it to `component`'s "
"some_list_of_objects."
)
else:
print(
"Adding elements to `shallow_copied_component`'s "
"some_list_of_objects doesn't add it to `component`'s "
"some_list_of_objects."
)
# Let's change the set in the list of objects.
component.some_list_of_objects[1].add(4)
if 4 in shallow_copied_component.some_list_of_objects[1]:
print(
"Changing objects in the `component`'s some_list_of_objects "
"changes that object in `shallow_copied_component`'s "
"some_list_of_objects."
)
else:
print(
"Changing objects in the `component`'s some_list_of_objects "
"doesn't change that object in `shallow_copied_component`'s "
"some_list_of_objects."
)
deep_copied_component = copy.deepcopy(component)
# Let's change the list in deep_copied_component and see if it changes in
# component.
deep_copied_component.some_list_of_objects.append("one more object")
if component.some_list_of_objects[-1] == "one more object":
print(
"Adding elements to `deep_copied_component`'s "
"some_list_of_objects adds it to `component`'s "
"some_list_of_objects."
)
else:
print(
"Adding elements to `deep_copied_component`'s "
"some_list_of_objects doesn't add it to `component`'s "
"some_list_of_objects."
)
# Let's change the set in the list of objects.
component.some_list_of_objects[1].add(10)
if 10 in deep_copied_component.some_list_of_objects[1]:
print(
"Changing objects in the `component`'s some_list_of_objects "
"changes that object in `deep_copied_component`'s "
"some_list_of_objects."
)
else:
print(
"Changing objects in the `component`'s some_list_of_objects "
"doesn't change that object in `deep_copied_component`'s "
"some_list_of_objects."
)
print(
f"id(deep_copied_component.some_circular_ref.parent): "
f"{id(deep_copied_component.some_circular_ref.parent)}"
)
print(
f"id(deep_copied_component.some_circular_ref.parent.some_circular_ref.parent): "
f"{id(deep_copied_component.some_circular_ref.parent.some_circular_ref.parent)}"
)
print(
"^^ This shows that deepcopied objects contain same reference, they "
"are not cloned repeatedly."
)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
输出
Adding elements to `shallow_copied_component`'s some_list_of_objects adds it to `component`'s some_list_of_objects.
Changing objects in the `component`'s some_list_of_objects changes that object in `shallow_copied_component`'s some_list_of_objects.
Adding elements to `deep_copied_component`'s some_list_of_objects doesn't add it to `component`'s some_list_of_objects.
Changing objects in the `component`'s some_list_of_objects doesn't change that object in `deep_copied_component`'s some_list_of_objects.
id(deep_copied_component.some_circular_ref.parent): 4429472784
id(deep_copied_component.some_circular_ref.parent.some_circular_ref.parent): 4429472784
^^ This shows that deepcopied objects contain same reference, they are not cloned repeatedly.
2
3
4
5
6
7
"""
*What is this pattern about?
This patterns aims to reduce the number of classes required by an
application. Instead of relying on subclasses it creates objects by
copying a prototypical instance at run-time.
This is useful as it makes it easier to derive new kinds of objects,
when instances of the class have only a few different combinations of
state, and when instantiation is expensive.
*What does this example do?
When the number of prototypes in an application can vary, it can be
useful to keep a Dispatcher (aka, Registry or Manager). This allows
clients to query the Dispatcher for a prototype before cloning a new
instance.
Below provides an example of such Dispatcher, which contains three
copies of the prototype: 'default', 'objecta' and 'objectb'.
*TL;DR
Creates new object instances by cloning prototype.
"""
from __future__ import annotations
from typing import Any
class Prototype:
def __init__(self, value: str = "default", **attrs: Any) -> None:
self.value = value
self.__dict__.update(attrs)
def clone(self, **attrs: Any) -> Prototype:
"""Clone a prototype and update inner attributes dictionary"""
# Python in Practice, Mark Summerfield
# copy.deepcopy can be used instead of next line.
obj = self.__class__(**self.__dict__)
obj.__dict__.update(attrs)
return obj
class PrototypeDispatcher:
def __init__(self):
self._objects = {}
def get_objects(self) -> dict[str, Prototype]:
"""Get all objects"""
return self._objects
def register_object(self, name: str, obj: Prototype) -> None:
"""Register an object"""
self._objects[name] = obj
def unregister_object(self, name: str) -> None:
"""Unregister an object"""
del self._objects[name]
def main() -> None:
"""
>>> dispatcher = PrototypeDispatcher()
>>> prototype = Prototype()
>>> d = prototype.clone()
>>> a = prototype.clone(value='a-value', category='a')
>>> b = a.clone(value='b-value', is_checked=True)
>>> dispatcher.register_object('objecta', a)
>>> dispatcher.register_object('objectb', b)
>>> dispatcher.register_object('default', d)
>>> [{n: p.value} for n, p in dispatcher.get_objects().items()]
[{'objecta': 'a-value'}, {'objectb': 'b-value'}, {'default': 'default'}]
>>> print(b.category, b.is_checked)
a True
"""
if __name__ == "__main__":
import doctest
doctest.testmod()
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
# coding: utf-8
import copy
from collections import OrderedDict
class Book:
def __init__(self, name, authors, price, **rest):
'''rest的例子有:出版商,长度,标签,出版日期'''
self.name = name
self.authors = authors
self.price = price # 单位为美元
self.__dict__.update(rest)
def __str__(self):
mylist = []
ordered = OrderedDict(sorted(self.__dict__.items()))
for i in ordered.keys():
mylist.append('{}: {}'.format(i, ordered[i]))
if i == 'price':
mylist.append('$')
mylist.append('\n')
return ''.join(mylist)
class Prototype:
def __init__(self):
self.objects = dict()
def register(self, identifier, obj):
self.objects[identifier] = obj
def unregister(self, identifier):
del self.objects[identifier]
def clone(self, identifier, **attr):
found = self.objects.get(identifier)
if not found:
raise ValueError('Incorrect object identifier: {}'.format(identifier))
obj = copy.deepcopy(found)
obj.__dict__.update(attr)
return obj
def main():
b1 = Book('The C Programming Language', ('Brian W. Kernighan', 'Dennis M.Ritchie'), price=118, publisher='Prentice Hall',
length=228, publication_date='1978-02-22', tags=('C', 'programming', 'algorithms', 'data structures'))
prototype = Prototype()
cid = 'k&r-first'
prototype.register(cid, b1)
b2 = prototype.clone(cid, name='The C Programming Language(ANSI)', price=48.99,
length=274, publication_date='1988-04-01', edition=2)
for i in (b1, b2):
print(i)
print('ID b1 : {} != ID b2 : {}'.format(id(b1), id(b2)))
if __name__ == '__main__':
main()
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62