原型 Prototype

原型模式是一种创建型设计模式,使你能够复制已有对象,而又无需使代码依赖它们所属的类。


为什么要使用?

原型模式的对象职责:

复制已有对象无需使代码依赖它们所属的类。

使用原型实例指定创建对象的种类,然后通过拷贝这些原型来创建新的对象。

在现实开发中,我们可以会遇到一些情况,无法根据类来生成实例,而要根据现有的实例来生成新的实例。如以下情况:

  1. 对象种类繁多,无法将它们整合到一个类中时。
  2. 难以根据类生成实例时,或者说生成实例的过程太过复杂。
  3. 想解耦框架与生成的实例时,让生成实例的框架不依赖于具体的类。

那么,我们可以使用原型模式来生成新的类,使被复制的具体的类与复制解耦。


模式结构

  • 原型 (Prototype)接口将对克隆方法进行声明。在绝大多数情况下,其中只会有一个名为 clone 克隆的方法。

  • 具体原型(Concrete Prototype)类将实现克隆方法。除了将原始对象的数据复制到克隆(需要考虑浅拷贝还是深拷贝)体中之外,该方法有时还需处理克隆过程中的极端情况,例如克隆关联对象和梳理递归依赖等等。

  • 客户端(Client)可以复制实现了原型接口的任何对象。

原型模式的类图:

通过一个声明 clone 的接口作为原型,让其他需要被克隆的对象实现它。这样使你能够克隆对象,但又不需要将代码和对象所属类耦合。


模式实现

该示例使用原型模式构建一个注册原型的工厂和下划线类原型。将原型注册到工厂中,需要克隆原型时,只需要使用相应的原型 key,即可获取相应原型克隆实例。

示例程序的类图

代码实现

原型接口
1
2
3
4
5
6
7
8
9
10
11
12
13
package example;

/** 原型接口 */
public interface IPrototype extends Cloneable {
/**
* 克隆接口
* 如果在没有实现 Cloneable 接口的实例上调用 Object 的 clone 方法,则会导致抛出 CloneNotSupportedException 异常。
*
* @return IPrototype
* @throws CloneNotSupportedException
*/
IPrototype clone() throws CloneNotSupportedException;
}
具体原型
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
package example;

/** 具体原型 */
public class UnderlinePen implements IPrototype {
/** 下划线符号 */
private String ulChar;

public UnderlinePen(String ulChar) {
this.ulChar = ulChar;
}

/** 打印下划线 */
public void print() {
if (ulChar != null && !"".equals(ulChar)) {
for (int i = 0; i < 10; i++) {
System.out.print(ulChar);
}
System.out.println();
}
}

/**
* 克隆接口
* 如果在没有实现 Cloneable 接口的实例上调用 Object 的 clone 方法,则会导致抛出 CloneNotSupportedException 异常。
* @return IPrototype 克隆后的对象
* @throws CloneNotSupportedException
*/
@Override
public IPrototype clone() throws CloneNotSupportedException {
IPrototype prototype = null;
try {
prototype = (IPrototype) super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return prototype;
}
}
原型工厂
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
package example;

import java.util.HashMap;
import java.util.Map;

/** 原型工厂 */
public class PrototypeFactory {
/** 原型注册表 */
private Map<String, IPrototype> prototypeMap = new HashMap<>();

public void register(String name, IPrototype prototype) {
prototypeMap.put(name, prototype);
}

/**
* 根据原型注册表,拷贝原型
* @param name 原型注册表key
* @return 拷贝原型
*/
public IPrototype getPrototypeInstance(String name) {
IPrototype prototype = null;
try {
prototype = prototypeMap.get(name).clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return prototype;
}
}

代码测试

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
import example.IPrototype;
import example.PrototypeFactory;
import example.UnderlinePen;

/** 测试原型模式 */
public class Test {
public static void main(String[] args) {
PrototypeFactory prototypeFactory = new PrototypeFactory();
UnderlinePen plus = new UnderlinePen("+");
UnderlinePen minus = new UnderlinePen("-");
prototypeFactory.register("plus", plus);
prototypeFactory.register("minus", minus);
UnderlinePen plusClone = (UnderlinePen) prototypeFactory.getPrototypeInstance("plus");
UnderlinePen minusClone = (UnderlinePen) prototypeFactory.getPrototypeInstance("minus");
System.out.print("原生“+”对象(" + System.identityHashCode(plus) + ")输出:");
plus.print();
System.out.print("克隆“+”对象(" + System.identityHashCode(plusClone) + ")输出:");
plusClone.print();

System.out.println("\n------------------- 分割线 -------------------\n");

System.out.print("原生“-”对象(" + System.identityHashCode(minus) + ")输出:");
minus.print();
System.out.print("克隆“-”对象(" + System.identityHashCode(minusClone) + ")输出:");
minusClone.print();

}
}

输出结果

1
2
3
4
5
6
7
原生“+”对象(460141958)输出:++++++++++
克隆“+”对象(1163157884)输出:++++++++++

------------------- 分割线 -------------------

原生“-”对象(1956725890)输出:----------
克隆“-”对象(356573597)输出:----------

从上述结果不难看出,我们用原型模式创建的对象和原来的对象并非同个对象,但是属性又完全一致。


常用场景和解决方案

  • 资源优化场景。当对象初始化需要借助许多外部资源时,如 IO 资源。
  • 复制的依赖场景。创建对象时需要多个对象相互依赖。
  • 性能和安全要求的场景。
  • 同一个对象可能被多个修改者使用的场景。
  • 需要保存原始对象状态的场景。例如记录历史操作。
  • 结合工厂模式使用。用原型注册表来记录每个原型,让使用者能克隆所需的原型。

模式的优缺点

优点 缺点
你可以克隆对象,而无需与它们所属的具体类相耦合。 克隆包含循环引用的复杂对象可能会非常麻烦。
你可以克隆预生成原型,避免反复运行初始化代码。
你可以更方便地生成复杂对象。
你可以用继承以外的方式来处理复杂对象的不同配置。

使用原型模式的优势

  • 减少每次创建对象的资源消耗。使用对象拷贝是在内存中进行二进制流的拷贝,资源消耗更少、速度更快。
  • 减低对象创建的时间消耗,在需要查询数据库创建对象时,如果数据库繁忙,就需要等待,那么这个创建过程就可能出现长时间等待。使用原型模式创建对象就相当于进行一次缓存读取,大大加快了创建对象时间。
  • 快速复制对象运行时的状态,不需要了解创建过程也能快速创建对象。
  • 能保存原始对象的副本,方便快速回滚到上一次保存或最初的状态。
  • 可以快速扩展运行时对象的属性和方法。

使用原型模式的劣势

  • 需要一个被初始化过的正确对象。
  • 复制大对象时,可能出现内存溢出的 OOM(Out Of Memory) 错误。
  • 动态扩展对象功能时可能会掩盖新的风险。

拓展知识

  • 原型可用于保存命令模式的历史记录。
  • 大量使用组合模式和装饰模式的设计通常可从对于原型的使用中获益。你可以通过该模式来复制复杂结构,而非从零开始重新构造。
  • 原型并不基于继承,因此没有继承的缺点。另一方面,原型需要对被复制对象进行复杂的初始化。工厂方法基于继承, 但是它不需要初始化步骤。
  • Java 中的 Cloneable 接口是一个标记接口,用来标记可以使用 clone 方法进行复制。其内部根本没有声明 clone 方法。clone 方法的定义在 java.lang.Object 中。
  • clone 方法进行的复制是浅复制。它只是将被复制实例的字段值直接复制到新的实例中。如果字段保存的是数组,那么它只会复制该数组的引用,并不会复制数组中的元素。
  • 使用原型模式时可能需要我们对 IO 流、内存和 JVM 等一些底层的原理有更加深入的理解才行。


🔙 设计模式

📌最后:希望本文能够给您提供帮助,文章中有不懂或不正确的地方,请在下方评论区💬留言!

🔗参考文献:

🌐 设计模式 –refactoringguru

▶️ bilibili-趣学设计模式;黄靖锋. –拉勾教育

📖 图解设计模式 /(日)结城浩著;杨文轩译. –北京:人民邮电出版社,2017.1