初探原型模式

代码链接

koonchen/design-patterns/prototype

要点

用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。

我们假设存在一个框架,它拥有大量的图形 Graphic ,对图形本身的选择等框架层面的功能存放在 Tool 中,并且每一个图形的创建都需要 Tool 的一个实现 GraphicTool 子类来协助。现在问题来了,框架的 GraphicTool 并不知道我们需要创建的 Graphic 具体是什么,最朴素的想法是为每一个图形都创建一个 GraphicTool 的子类,但这样将产生大量的子类,对象复合是比创建子类更灵活的一种选择

原型模式让 GraphicTool 通过克隆具体图形( Graphic 子类,或者称之为原型 )实例来创建新的图形,或者说将这个具体的图形实例作为了参数,所以如果 Graphic 子类都支持克隆操作,那么 GraphicTool 就可以克隆所有类型的图形。可以看下图更好理解这一模式:

cover

什么时候适用这种模式:

  • 当一个系统独立于它的产品创建、构造、表示时;
  • 当要实例化的类是在运行时指定,比如动态加载时;
  • 为了避免创建一个与产品类层次平行的工厂类层次时 ( 现在工厂类和产品类之间仅仅是聚合关系 ) ;
  • 当一个类的实例只能有几个不同状态组合中的一种的时。

建立相应数目的原型并克隆他们可能比每次用特定的状态手动实例化该类更方便时。

他拥有以下参与者:

  • Prototype—声明一个克隆自身的接口。
  • ConcretePrototype—实现一个克隆自身的操作。
  • Client—让一个原型克隆自身从而创建一个新的对象。

所以一个典型的原型模式结构也就呼之欲出了:

cover

我们发现到现在为止的模式:工厂方法、抽象工厂,以及现在的原型模式。他们都有一个一样的效果:隐藏了具体的产品类,下面是原型模式的一些优点:

  • 运行时增加和删除产品。客户只要注册原型实例就可以将一个新的产品类加入系统中。
  • 改变值以指定新对象。我们现在拥有原型,而原型就是新类别的对象,我们将职责给实例化,就表现出了新的行为。比如刚才的 GraphicTool 类,他就可以创建无数种图形对象,是职责代理的一种应用。
  • 改变结构以指定新对象。在电路设计中,我们存在子电路构造电路的情况,并且允许一次又一次重复使用一个特定的子电路,将子电路作为原型完成电路就是这一优点的体现。
  • 减少子类的构造。工厂方法中还需要一个产品类平行的 Creator 类,原型模式则克隆一个原型,这一平行层次都需要了。更有甚者,在类作为一级类对象的语言中,类对象已经起到了原型的作用,所以获益较少,比如 Python 、 Smalltalk 等等 。
  • 用类动态配置应用。一些运行时刻环境允许你动态将类装载到应用中。有一句很绕口的话是这么说的:一个希望创建动态载入类的实例的应用不能静态引入类的构造器,而应该由运行环境在载入时创建每个类的实例。( 多读几遍还是很好懂的 ) 在像 C++ 这样的语言中,原型模式是这种功能的关键。

原型模式的主要缺陷是每一个原型的子类都必须有克隆操作,这可能是困难的。比方说这个类只要存在了,就难以新增克隆操作;或者当内部一些不支持拷贝或有循环引用的对象时,实现克隆也会困难。

实现

在像 C++ 这样的语言中,类不是对象,所以原型模式特别有用,当实现原型时,要考虑:

  • 使用一个原型管理器。当一个系统中原型数目不固定时,要保持一个可用原型的注册表。
  • 实现克隆操作。原型模式最困难的部分在于正确实现克隆操作。
  • 初始化克隆对象。当一部分人对克隆对象已经满意,另一部分人可能希望使用他们所选择的一些值来初始化该对象。一种是有专门该值更改的方法,一种是初始化方法,后者需要删除已有对象,而前者要逐一修改已有对象。

java 实现

1
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
import java.util.Hashtable;

class Prototype implements Cloneable {
@Override
protected Prototype clone() {
Prototype clone = null;
try {
clone = (Prototype) super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return clone;
}

public void check() {
System.out.println("Prototype instance has been created");
}
}

class ConcretePrototype1 extends Prototype {
public void check() {
System.out.println("ConcretePrototype1 instance has been created");
}
}

class ConcretePrototype2 extends Prototype {
public void check() {
System.out.println("ConcretePrototype2 instance has been created");
}
}

public class PrototypeClient {
public static void main(String[] args) {
Hashtable map = new Hashtable<String,Prototype>();
map.put("Prototype",new Prototype());
map.put("ConcretePrototype1",new ConcretePrototype1());
map.put("ConcretePrototype2",new ConcretePrototype2());
((Prototype) map.get("Prototype")).clone().check();
((Prototype) map.get("ConcretePrototype1")).clone().check();
((Prototype) map.get("ConcretePrototype2")).clone().check();
}
}

这里使用了 Cloneable 接口实现 clone 方法,使用一个 map 作为原型管理器,但不是最理想的状态,因为取出的内容是 object 所以需要从父类转换成子类,这是一个不规范的行为,但是如果全部的行为在父类都有了默认的定义,那就变得可以接受了。

这里的克隆操作是最简单的浅拷贝,如果存在引用类型还是要自己完成深拷贝。

PS: 这里方法完全可以和工厂模式相互结合:

抽象工厂和工厂方法中,可以先保存产品原型的集合,然后返回产品对象,跳过从 FactoryCreator 创建产品;

python 实现

参考 python-patterns/creational/prototype.py

1
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
#!/usr/bin/env python
# coding:utf8

class Prototype(object):
value = 'default'
def clone(self, **kwargs):
obj = self.__class__()
obj.__dict__.update(kwargs)
return obj

class PrototypeDispatcher(object):
def __init__(self):
self._objs={}
def get_objects(self):
return self._objs
def register_obj(self, name, obj):
self._objs[name]=obj
def unregister_obj(self, name):
del self._objs[name]

if __name__ == '__main__':
prototypeDispatcher = PrototypeDispatcher()
prototype = Prototype()
a = prototype.clone(value='a')
b = prototype.clone(value='b')
prototypeDispatcher.register_obj('objDefault', prototype)
prototypeDispatcher.register_obj('objA', a)
prototypeDispatcher.register_obj('objB', b)
print([{n:p.value} for n, p in prototypeDispatcher.get_objects().items()])
  • 在 python 的实现中使用一个 PrototypeDispatcher 作为原型管理器;
  • 克隆操作上创建新的实例,开辟了新的内存空间,如果有其他引用类型应该会引入 copy 包协同工作;
  • 初始化对于该例,可以选择删除全部已有实例或者逐个更改,这值得斟酌。

原型模式一句话概括就是运行时复制原型实例,抽象工厂要求我们按部就班创建工厂然后创建产品 (工厂方法是搭建一个和产品平行层次的 Creator ,所以不加入讨论) ,原型模式直接用一个产品作为原型,然后复制生产,前者在大方向上指明道路,后者在某一条分支上另辟蹊径,这些思想非常有趣。