原型模式
发表于更新于
设计模式原型模式
Rif设计模式笔记:原型模式 (Prototype Pattern)
一、一句话概括
通过复制一个已经存在的实例(原型)来创建新的对象,而无需关心创建的细节。简单说,就是 “别用 new 了,用 clone() 吧”。
二、为什么需要它 (Why - The Pain Point)
在软件开发中,我们常常会遇到以下痛点:
- 创建成本高昂:一个对象的初始化过程可能非常复杂和耗时。例如,需要查询数据库、进行RPC调用、读取大文件、进行复杂的计算等。如果需要频繁创建这类对象,系统的性能会受到严重影响。
- 避免与具体类耦合:假设你正在开发一个框架,它需要能创建和处理用户提供的各种对象。你的框架代码不应该硬编码具体的用户类名(例如
new UserDefinedClass()),否则框架的通用性就无从谈起。
- 简化对象状态的复制:当需要一个与现有对象状态完全相同或大部分相同的新对象时,从头开始设置所有属性会非常繁琐。直接复制一个现有对象的状态会便捷得多。
核心问题:如何高效、灵活地创建复杂对象,同时又不让创建过程与系统的其他部分紧密耦合?
三、它是什么 (What - The Solution)
原型模式提供了一种优雅的解决方案:克隆。
它将创建新对象的过程,从传统的“构造函数逐步构建”转变为“从一个原型对象拷贝”。这个原型对象就像一个模板,后续所有的新对象都是它的一个副本。
核心思想:
- **原型接口 (Prototype Interface)**:定义一个用于克隆自身的接口。在 Java 中,语言层面已经提供了
java.lang.Cloneable 标记接口和 java.lang.Object 类中的 clone() 方法。
- **具体原型 (Concrete Prototype)**:实现克隆接口的类。它负责实现具体的克隆逻辑。
- **客户端 (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)。要使用它,我们必须在自己的类中:
- 实现
Cloneable 接口。
- 重写
clone() 方法,并将其访问修饰符改为 public,以便外部调用。
深度剖析:浅拷贝 (Shallow Copy) vs. 深拷贝 (Deep Copy)
这是原型模式中最关键、也最容易出错的地方。
四、怎么做 (How - The Blueprint)
基本步骤:
- 创建一个抽象类或接口,作为所有原型的基类。让它实现
Cloneable 接口。
- 在该基类中,重写
clone() 方法。
- 创建具体的原型类,继承或实现上述基类/接口。
- 如果存在可变引用类型的成员变量,务必在
clone() 方法中实现深拷贝逻辑。
- (可选但推荐)创建一个原型管理器(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 + "]"; }
@Override protected Object clone() throws CloneNotSupportedException { return super.clone(); } }
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; }
@Override public Object clone() { Object clone = null; try {
Vehicle clonedVehicle = (Vehicle) super.clone(); clonedVehicle.engine = (Engine) this.engine.clone(); clone = clonedVehicle;
} catch (CloneNotSupportedException e) { e.printStackTrace(); } return clone; } }
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..."); } }
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()));
} }
|
五、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 接口。
Car 和 Truck 继承自 Vehicle。
Vehicle 包含一个 Engine 对象(聚合关系 o--)。
VehicleCache 缓存了 Vehicle 的原型实例,并根据请求克隆它们。
六、优缺点分析
优点 (Pros):
- 性能提升:当创建新对象的代价(时间、资源)很大时,原型模式通过复制来避免这些开销,性能极佳。
- 简化创建过程:客户端无需知道复杂的创建细节,只需调用
clone() 即可。
- 增加动态性:可以在运行时动态地添加或删除原型,从而改变应用的行为。客户端可以克隆一个在运行时才决定的对象。
- 减少子类:与工厂方法模式相比,在某些场景下可以避免创建与产品类平行的工厂类层次结构。
缺点 (Cons):
- 克隆的复杂性:实现深拷贝可能非常复杂,需要对类中所有的可变引用成员进行递归克隆,容易出错。
- 对现有类的改造:每个需要支持克隆的类都必须实现
Cloneable 接口并重写 clone() 方法,这可能对已有的类层次结构造成侵入。
clone() 方法的约定问题:Java 中 clone() 的实现机制本身存在一些争议(例如,它与 final 字段的交互不佳),有些开发者(如《Effective Java》作者 Joshua Bloch)建议优先使用拷贝构造函数或静态工厂方法来替代。
七、在 Java 中的应用
java.util.ArrayList 的 clone() 方法是一个非常典型的浅拷贝实现。当你克隆一个 ArrayList 时,新列表和旧列表的容量、大小都一样,但它们内部存储的对象引用是相同的。
java.util.Date 和 java.util.Calendar 的 clone() 方法。
- 许多框架在需要动态创建和配置对象时,会采用原型模式的思想,即使不直接使用
clone() 方法。