深入浅出设计模式【五、原型模式】
一、原型模式介绍
原型模式的核心思想是:使用一个已存在的实例(原型)作为蓝本,通过复制(克隆)这个原型来创建新的对象实例,而不是通过 new 关键字调用构造函数。
这种方式特别适用于以下情况:直接创建一个新对象的成本很高(例如,需要繁琐的初始化、需要从数据库或网络加载大量数据),而新对象与现有对象的区别很小。通过克隆,我们可以绕过这些昂贵的初始化过程。
二、核心概念与意图
-
核心概念:
- 原型 (Prototype): 声明一个用于克隆自身的接口,通常是一个
clone()或copy()方法。 - 具体原型 (Concrete Prototype): 实现原型接口,真正实现克隆自身的操作。
- 客户端 (Client): 向原型对象请求克隆,从而创建一个新的、与之相同的对象。
- 原型 (Prototype): 声明一个用于克隆自身的接口,通常是一个
-
意图:
- 通过复制现有的实例来创建新的实例,而不是通过新建类。
- 避免昂贵的初始化过程,提升创建对象的性能。
- 避免与构建过程相关的代码耦合,使对象创建更加灵活。
三、适用场景剖析
原型模式在以下场景中非常有效:
- 系统需要独立于其产品的创建、构成和表示时: 当要实例化的类是在运行时动态指定时(例如,通过动态加载)。
- 避免构建与产品层次平行的工厂层次: 相比抽象工厂模式,原型模式不需要为每个产品类创建一个对应的工厂类,只需每个产品类实现克隆方法即可。
- 一个类的实例只有几个不同的状态组合: 相比于手动实例化并设置不同的状态,预先克隆并配置好一系列原型可能更方便。当需要匹配这些状态时,直接克隆相应的原型。
- 创建对象的成本高昂: 当对象的创建过程涉及耗时的操作(如复杂的计算、IO操作、数据库查询)时,直接复制一个已有数据完备的对象比重新创建要高效得多。
四、UML 类图解析
以下Mermaid类图清晰地展示了原型模式的结构和角色间的关系:
Prototype: 声明克隆方法的接口。在Java中,这通常就是内置的Cloneable接口,但它是一个标记接口,真正的方法clone()定义在Object类中。ConcretePrototype1和ConcretePrototype2: 实现Prototype接口(即实现Cloneable)的具体类。它们需要重写clone()方法,以提供自身的克隆逻辑。Client: 任何需要创建新原型对象的客户。它持有一个原型实例,并通过调用其clone()方法来获得一个新对象,而不是使用new操作符。
调用流程: 客户端通过调用 prototype.clone() 来获得一个与原型对象相同的新对象。
五、各种实现方式及其优缺点
原型模式的核心在于“复制”,而复制又分为浅拷贝 (Shallow Copy) 和深拷贝 (Deep Copy)。这是实现原型模式时必须仔细考虑的关键点。
1. 使用Java内置的 Cloneable 接口和 Object.clone()
这是最直接的方式,但需要理解其机制。
public class ConcretePrototype implements Cloneable {
private String name;
private int value;
private List<String> items; // 引用类型成员
public ConcretePrototype(String name, int value, List<String> items) {
this.name = name;
this.value = value;
this.items = items;
}
// 重写clone方法
@Override
public ConcretePrototype clone() {
try {
// Object.clone() 是浅拷贝!
return (ConcretePrototype) super.clone();
} catch (CloneNotSupportedException e) {
throw new AssertionError(); // Can't happen
}
}
// ... getters and setters
}
-
浅拷贝 (Shallow Copy):
- 机制:
Object.clone()的默认行为是浅拷贝。它会创建一个新对象,并将原对象的所有字段的值直接复制到新对象。对于基本类型字段(如int value),直接复制其值。对于引用类型字段(如List<String> items),则复制其引用地址,而不是引用所指的对象本身。 - 优点: 简单、快速。
- 缺点: 原对象和克隆对象会共享其引用类型的成员。修改其中一个对象的
items列表,另一个对象的items也会随之改变。这通常不是我们想要的行为,会带来意外的副作用。
- 机制:
-
深拷贝 (Deep Copy):
- 机制: 不仅要复制对象本身,还要递归地复制其所有引用类型字段所指向的对象,直到所有可达对象都被复制。这样原对象和克隆对象就完全独立,没有任何共享内容。
- 实现: 需要在
clone()方法中手动实现。
@Override public ConcretePrototype clone() { ConcretePrototype clone = (ConcretePrototype) super.clone(); // 先进行浅拷贝 // 对引用类型字段进行深拷贝 clone.items = new ArrayList<>(this.items); // 创建一个新的ArrayList,复制原列表中的所有元素 return clone; }- 优点: 克隆对象完全独立,安全无副作用。
- 缺点: 实现相对复杂,尤其当对象引用关系非常深、非常复杂时。性能开销也比浅拷贝大。
2. 使用复制构造函数 (Copy Constructor)
public class ConcretePrototype {
private String name;
private int value;
private List<String> items;
// 普通的构造函数
public ConcretePrototype(String name, int value, List<String> items) {...}
// 复制构造函数
public ConcretePrototype(ConcretePrototype other) {
this.name = other.name;
this.value = other.value;
this.items = new ArrayList<>(other.items); // 深拷贝
}
}
// 客户端使用:ConcretePrototype clone = new ConcretePrototype(original);
- 优点:
- 实现清晰,不需要处理
Cloneable接口和受检异常。 - 可以完全控制拷贝逻辑(浅拷贝或深拷贝)。
- 实现清晰,不需要处理
- 缺点:
- 必须为每个类显式编写复制构造函数。
- 不如
clone()方法那样符合多态性(需要知道具体的类才能调用复制构造函数)。
3. 使用序列化机制实现深拷贝
将对象序列化成字节流,然后再反序列化成一个新的对象。这是一种非常巧妙的实现深拷贝的方法。
import java.io.*;
public class DeepCopyUtil {
@SuppressWarnings("unchecked")
public static <T extends Serializable> T deepCopy(T object) {
try (ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos)) {
oos.writeObject(object);
try (ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bais)) {
return (T) ois.readObject();
}
} catch (IOException | ClassNotFoundException e) {
throw new RuntimeException("Deep copy failed", e);
}
}
}
// 客户端使用:ConcretePrototype clone = DeepCopyUtil.deepCopy(original);
- 优点:
- 自动化实现深拷贝,无需为每个类手动编写代码。
- 非常强大,能处理复杂的对象图。
- 缺点:
- 性能开销较大(IO操作)。
- 要求所有涉及的对象都必须实现
Serializable接口。 - 无法处理瞬态字段(
transientfields)。
六、最佳实践
- 谨慎使用
Cloneable: Java 的Cloneable接口设计存在缺陷(它是一个标记接口,但clone()方法却在Object中),并且默认实现是浅拷贝,容易出错。Joshua Bloch 在《Effective Java》中建议“谨慎地重写clone方法,或者最好提供其他的方式来实现拷贝功能”。 - 优先考虑复制构造函数或静态工厂方法: 这些方式比实现
Cloneable接口更简单、更安全、更灵活。 - 明确拷贝类型: 在设计和文档中明确说明你的
clone()方法是实现浅拷贝还是深拷贝,避免使用者产生误解。 - 考虑不可变对象: 如果对象是不可变的,那么浅拷贝就是安全的,因为其引用指向的对象也是不可变的。这是解决浅拷贝问题的一个根本方法。
七、在开发中的演变和应用
原型模式的思想在现代开发中依然非常重要:
- 与IoC容器结合: 在 Spring框架 中,Bean的作用域(Scope)之一就是
prototype。每次从容器中请求一个作用域为prototype的Bean时,Spring都会创建一个新的实例(可以理解为克隆了一个新的实例)。这与单例作用域(singleton)形成鲜明对比。 - 游戏开发: 在游戏行业中,原型模式被大量使用。敌人、子弹、道具等游戏对象通常有大量重复且创建成本较高。游戏引擎会预先创建好这些对象的原型,在需要时快速克隆,极大地提升了性能。
- 配置对象: 一个复杂的系统配置对象(包含数据库连接、线程池设置、特性开关等)可以被设置为原型。当需要为某个特定任务创建一个稍有不同配置的对象时,克隆原型并只修改少数几个属性是非常高效的方式。
八、真实开发案例(Java语言内部、知名开源框架、工具)
-
ArrayList的clone()方法:ArrayList<String> originalList = new ArrayList<>(); originalList.add("Item1"); @SuppressWarnings("unchecked") ArrayList<String> clonedList = (ArrayList<String>) originalList.clone();ArrayList的clone()方法会创建一个新的ArrayList实例,但它是浅拷贝!新列表中的元素引用与原列表中的元素引用指向相同的对象。 -
Spring Framework的Prototype Scope:
在XML配置中:<bean id="myPrototypeBean" class="com.example.MyBean" scope="prototype"/>或在Java配置中:
@Bean @Scope("prototype") public MyBean myBean() { return new MyBean(); }每次调用
applicationContext.getBean("myPrototypeBean")时,都会返回一个新的实例。 -
JavaScript语言: JavaScript本身是基于原型的语言,它的对象继承机制就是通过原型链(Prototype Chain)来实现的,这是原型模式最极致的应用。
九、总结
| 方面 | 总结 |
|---|---|
| 模式类型 | 创建型设计模式 |
| 核心意图 | 通过复制现有实例(原型)来创建新对象,避免昂贵的初始化开销。 |
| 关键角色 | 原型 (Prototype)、具体原型 (Concrete Prototype)、客户端 (Client) |
| 主要优点 | 1. 性能提升:避免昂贵的初始化操作,性能优于 new。2. 简化创建过程:隐藏对象创建的复杂细节。 3. 动态性:可以在运行时通过改变原型来改变新产品。 |
| 主要缺点 | 1. 深浅拷贝问题:实现复杂的深拷贝可能非常麻烦,且容易出错。 2. 违背原则:某些语言的实现(如Java的 Cloneable)可能违背“面向接口编程”的原则。3. 复杂性:每个类都需要配置一个克隆方法,当类内部引用其他类时,深拷贝的实现会变得复杂。 |
| 适用场景 | 1. 创建对象的成本高昂(资源、时间)。 2. 系统需要独立于对象的创建、构成和表示。 3. 一个对象需要提供给其他调用者使用,但又需要保护其状态不被修改(通过返回一个克隆体)。 |
| 关系与对比 | vs. 工厂模式: 工厂模式关心的是创建什么对象,而原型模式关心的是如何通过复制创建对象。 |
| 核心关注点 | 深浅拷贝的实现是使用原型模式时必须慎重考虑和明确声明的关键点。 |
原型模式是一种“以空间换时间”的经典模式。在现代开发中,虽然直接使用 Cloneable 接口的情况在减少,但其核心思想——通过复制来高效创建对象——被广泛应用于各种框架和特定领域(如游戏、高性能计算)。理解其精髓,特别是深浅拷贝的区别,对于设计出正确、高效的代码至关重要。
本文来自博客园,作者:NeoLshu,转载请注明原文链接:https://www.cnblogs.com/neolshu/p/19120796

浙公网安备 33010602011771号