原型模式

设计模式笔记:原型模式 (Prototype Pattern)

一、一句话概括

通过复制一个已经存在的实例(原型)来创建新的对象,而无需关心创建的细节。简单说,就是 “别用 new 了,用 clone() 吧”

二、为什么需要它 (Why - The Pain Point)

在软件开发中,我们常常会遇到以下痛点:

  1. 创建成本高昂:一个对象的初始化过程可能非常复杂和耗时。例如,需要查询数据库、进行RPC调用、读取大文件、进行复杂的计算等。如果需要频繁创建这类对象,系统的性能会受到严重影响。
  2. 避免与具体类耦合:假设你正在开发一个框架,它需要能创建和处理用户提供的各种对象。你的框架代码不应该硬编码具体的用户类名(例如 new UserDefinedClass()),否则框架的通用性就无从谈起。
  3. 简化对象状态的复制:当需要一个与现有对象状态完全相同或大部分相同的新对象时,从头开始设置所有属性会非常繁琐。直接复制一个现有对象的状态会便捷得多。

核心问题:如何高效、灵活地创建复杂对象,同时又不让创建过程与系统的其他部分紧密耦合?

三、它是什么 (What - The Solution)

原型模式提供了一种优雅的解决方案:克隆

它将创建新对象的过程,从传统的“构造函数逐步构建”转变为“从一个原型对象拷贝”。这个原型对象就像一个模板,后续所有的新对象都是它的一个副本。

核心思想:

  1. **原型接口 (Prototype Interface)**:定义一个用于克隆自身的接口。在 Java 中,语言层面已经提供了 java.lang.Cloneable 标记接口和 java.lang.Object 类中的 clone() 方法。
  2. **具体原型 (Concrete Prototype)**:实现克隆接口的类。它负责实现具体的克隆逻辑。
  3. **客户端 (Client)**:客户端不通过 new 来创建对象,而是获取一个原型实例,然后调用其克隆方法来得到新的实例。

C++ 背景对比 & Java 关键点:

  • 与C++的联系:这个模式的思想与 C++ 中的拷贝构造函数(Copy Constructor)拷贝赋值运算符(Copy Assignment Operator)非常相似,都是为了复制一个已存在的对象。
  • Java 的实现机制
    • java.lang.Cloneable:这是一个标记接口(Marker Interface),它本身没有任何方法。一个类实现这个接口,只是为了在运行时告诉JVM,这个类的对象是“可以被克隆的”。如果没有实现它而直接调用 clone() 方法,会抛出 CloneNotSupportedException
    • java.lang.Object.clone():这是所有 Java 对象的根类 Object 提供的方法。它是一个 protected 方法,执行的是浅拷贝(Shallow Copy)。要使用它,我们必须在自己的类中:
      1. 实现 Cloneable 接口。
      2. 重写 clone() 方法,并将其访问修饰符改为 public,以便外部调用。

深度剖析:浅拷贝 (Shallow Copy) vs. 深拷贝 (Deep Copy)

这是原型模式中最关键、也最容易出错的地方。

  • **浅拷贝 (Shallow Copy)**:super.clone() 的默认行为。

    • 对于值类型(如 int, double)和不可变引用类型(如 String),会复制其值。
    • 对于可变引用类型(如 Date, ArrayList, 自定义对象),只会复制引用地址,而不会复制引用所指向的对象。这意味着克隆后的对象和原型对象将共享同一个内部对象。修改任何一方的共享对象,另一方也会受到影响。
  • **深拷贝 (Deep Copy)**:

    • 不仅复制对象本身,还会递归地复制其内部所有可变引用类型的成员对象。
    • 克隆后的对象和原型对象是完全独立的,互不影响。
    • 要实现深拷贝,你需要在重写的 clone() 方法中,对所有可变引用类型的成员也手动调用它们的 clone() 方法。

四、怎么做 (How - The Blueprint)

基本步骤:

  1. 创建一个抽象类或接口,作为所有原型的基类。让它实现 Cloneable 接口。
  2. 在该基类中,重写 clone() 方法。
  3. 创建具体的原型类,继承或实现上述基类/接口。
  4. 如果存在可变引用类型的成员变量,务必在 clone() 方法中实现深拷贝逻辑。
  5. (可选但推荐)创建一个原型管理器(Registry/Cache),用于存储和管理一系列原型对象,客户端通过一个键(key)来获取原型并克隆。

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
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
import java.util.HashMap;
import java.util.Map;

// 为了演示深拷贝,我们创建一个可变对象
class Engine implements Cloneable {
private String type;

public Engine(String type) { this.type = type; }
public String getType() { return type; }
public void setType(String type) { this.type = type; }

@Override
public String toString() { return "Engine [type=" + type + "]"; }

// Engine也需要能被克隆
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}


// 1. 抽象原型类,实现 Cloneable
abstract class Vehicle implements Cloneable {
private String id;
protected String type;
private Engine engine; // 可变引用类型成员

public abstract void drive();

public String getId() { return id; }
public void setId(String id) { this.id = id; }
public String getType() { return type; }
public Engine getEngine() { return engine; }
public void setEngine(Engine engine) { this.engine = engine; }

// 2. 重写 clone 方法
@Override
public Object clone() {
Object clone = null;
try {
// --- 浅拷贝 ---
// clone = super.clone();

// --- 深拷贝 ---
Vehicle clonedVehicle = (Vehicle) super.clone(); // 先进行基本浅拷贝
clonedVehicle.engine = (Engine) this.engine.clone(); // 再对可变对象进行拷贝
clone = clonedVehicle;

} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return clone;
}
}

// 3. 具体原型类
class Car extends Vehicle {
public Car() {
this.type = "Car";
setEngine(new Engine("Gasoline"));
}
@Override
public void drive() { System.out.println("Driving a car..."); }
}

class Truck extends Vehicle {
public Truck() {
this.type = "Truck";
setEngine(new Engine("Diesel"));
}
@Override
public void drive() { System.out.println("Driving a truck..."); }
}

// 5. 原型管理器 (推荐实践)
class VehicleCache {
private static Map<String, Vehicle> vehicleMap = new HashMap<>();

public static Vehicle getVehicle(String vehicleId) {
Vehicle cachedVehicle = vehicleMap.get(vehicleId);
// 返回的是原型的一个克隆,而不是原型本身
return (Vehicle) cachedVehicle.clone();
}

// 预加载原型到缓存中
public static void loadCache() {
Car car = new Car();
car.setId("1");
vehicleMap.put(car.getId(), car);

Truck truck = new Truck();
truck.setId("2");
vehicleMap.put(truck.getId(), truck);
}
}


// 客户端代码
public class PrototypePatternDemo {
public static void main(String[] args) {
VehicleCache.loadCache();

// 从缓存中获取克隆对象
Vehicle clonedCar1 = VehicleCache.getVehicle("1");
System.out.println("Cloned Vehicle: " + clonedCar1.getType() + ", Engine: " + clonedCar1.getEngine());

Vehicle clonedCar2 = VehicleCache.getVehicle("1");
System.out.println("Cloned Vehicle: " + clonedCar2.getType() + ", Engine: " + clonedCar2.getEngine());

System.out.println("\n--- 证明是不同实例 ---");
System.out.println("clonedCar1 == clonedCar2 ? " + (clonedCar1 == clonedCar2));

System.out.println("\n--- 演示深拷贝效果 ---");
// 修改第一个克隆车的引擎类型
clonedCar1.getEngine().setType("Electric");

System.out.println("clonedCar1's Engine: " + clonedCar1.getEngine());
System.out.println("clonedCar2's Engine: " + clonedCar2.getEngine());
System.out.println("clonedCar1.engine == clonedCar2.engine ? " + (clonedCar1.getEngine() == clonedCar2.getEngine()));

// 如果是浅拷贝,上面的输出会显示clonedCar2的引擎也变成了Electric,并且engine的比较会是true
// 如果是深拷贝,则不会影响,engine的比较会是false
}
}

五、UML 类图

下面是上述例子的 PlantUML 代码。

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
43
44
45
46
47
48
49
50
51
@startuml
skinparam classAttributeIconSize 0
hide empty members

' 标记接口
interface Cloneable <<interface>>

' 可变成员对象
class Engine implements Cloneable {
- String type
+ void setType(String type)
# Object clone()
}

' 抽象原型
abstract class Vehicle implements Cloneable {
- String id
# String type
- Engine engine ' 引用可变对象
+ {abstract} void drive()
+ Object clone()
}

' 具体原型
class Car extends Vehicle {
+ void drive()
}

class Truck extends Vehicle {
+ void drive()
}

' 原型管理器
class VehicleCache {
- {static} Map<String, Vehicle> vehicleMap
+ {static} Vehicle getVehicle(String id)
+ {static} void loadCache()
}

' 客户端
class PrototypePatternDemo {
+ {static} void main(String[] args)
}

' 关系
Vehicle o-- "1" Engine
VehicleCache o-- "many" Vehicle : caches >
PrototypePatternDemo ..> VehicleCache : uses
VehicleCache ..> Vehicle : creates via clone

@enduml

图示解读:

  • Vehicle 实现了 Cloneable 接口。
  • CarTruck 继承自 Vehicle
  • Vehicle 包含一个 Engine 对象(聚合关系 o--)。
  • VehicleCache 缓存了 Vehicle 的原型实例,并根据请求克隆它们。

六、优缺点分析

优点 (Pros):

  1. 性能提升:当创建新对象的代价(时间、资源)很大时,原型模式通过复制来避免这些开销,性能极佳。
  2. 简化创建过程:客户端无需知道复杂的创建细节,只需调用 clone() 即可。
  3. 增加动态性:可以在运行时动态地添加或删除原型,从而改变应用的行为。客户端可以克隆一个在运行时才决定的对象。
  4. 减少子类:与工厂方法模式相比,在某些场景下可以避免创建与产品类平行的工厂类层次结构。

缺点 (Cons):

  1. 克隆的复杂性:实现深拷贝可能非常复杂,需要对类中所有的可变引用成员进行递归克隆,容易出错。
  2. 对现有类的改造:每个需要支持克隆的类都必须实现 Cloneable 接口并重写 clone() 方法,这可能对已有的类层次结构造成侵入。
  3. clone() 方法的约定问题:Java 中 clone() 的实现机制本身存在一些争议(例如,它与 final 字段的交互不佳),有些开发者(如《Effective Java》作者 Joshua Bloch)建议优先使用拷贝构造函数或静态工厂方法来替代。

七、在 Java 中的应用

  • java.util.ArrayListclone() 方法是一个非常典型的浅拷贝实现。当你克隆一个 ArrayList 时,新列表和旧列表的容量、大小都一样,但它们内部存储的对象引用是相同的。
  • java.util.Datejava.util.Calendarclone() 方法。
  • 许多框架在需要动态创建和配置对象时,会采用原型模式的思想,即使不直接使用 clone() 方法。