文章目录
-
- 前言
- 一、核心定义
- 二、标准体系结构图
- 三、场景推演
- 四、实战案例
-
- [4.1 需求分析](#4.1 需求分析)
- [4.2 架构图](#4.2 架构图)
-
- [4.2.1 面条代码架构图](#4.2.1 面条代码架构图)
- [4.2.2 原型模式架构图](#4.2.2 原型模式架构图)
- [4.3 类图对比](#4.3 类图对比)
-
- [4.3.1 面条代码类图](#4.3.1 面条代码类图)
- [4.3.2 原型模式类图](#4.3.2 原型模式类图)
- [4.4 时序图](#4.4 时序图)
-
- [4.4.1 面条代码时序图](#4.4.1 面条代码时序图)
- [4.4.2 原型模式时序图](#4.4.2 原型模式时序图)
- [4.5 代码分析](#4.5 代码分析)
-
- [4.5.1 面条代码(if-else/硬编码)](#4.5.1 面条代码(if-else/硬编码))
- [4.5.2 原型模式代码](#4.5.2 原型模式代码)
- 总结
前言
在软件开发和系统重构的过程中,我们经常会遇到创建复杂对象导致性能瓶颈的问题。
例如,当一个对象的初始化需要消耗较多计算资源,或者需要频繁发起网络/数据库请求时,每次都使用 new 关键字从头构建是非常低效的。
如果我们需要大量内容相似但细节略有不同的对象,如何优雅地解决性能与代码冗余的问题?
这就是原型模式大显身手的地方。
本文源码:https://github.com/likerhood/CodeDesignWork 其中原型模式在codedesign3.0系列
一、核心定义
原型模式(Prototype Pattern) 是一种创建型设计模式,它允许你通过复制(克隆)现有对象来生成新对象,而无需使代码依赖于它们所属的类。
其核心逻辑在于:将现有对象作为一个"原型",通过调用该原型的克隆方法(如 Java 中的 clone())来快速生成一个包含相同状态的全新实例。
后续可以根据业务需要,对克隆出来的新实例进行局部属性的修改。
二、标准体系结构图
原型模式通常包含以下几个角色:
- 抽象原型 (Prototype): 声明一个克隆自身的接口(通常是一个
clone()方法)。 - 具体原型 (Concrete Prototype): 实现克隆接口,具体定义如何复制自己。
- 客户端 (Client): 提出创建对象的请求,通过调用原型对象的
clone()方法来获得一个新的对象。
uses
implements
implements
<<interface>>
Prototype
+clone() : Prototype
ConcretePrototype1
-field1
+clone() : ConcretePrototype1
ConcretePrototype2
-field2
+clone() : ConcretePrototype2
Client
-prototype: Prototype
+operation()
三、场景推演
在 Java 中,原型模式的实现通常依赖于 Cloneable 接口和重写 Object 类的 clone() 方法。
示例:带附件的邮件(展示深拷贝)
假设我们有一个复杂的 Email 对象,里面包含一个 Attachment (附件) 对象。
Java
// 1. 附件类 (需要被深拷贝,所以也要实现 Cloneable)
class Attachment implements Cloneable {
private String fileName;
public Attachment(String fileName) {
this.fileName = fileName;
}
public void setFileName(String fileName) {
this.fileName = fileName;
}
public String getFileName() {
return fileName;
}
@Override
protected Attachment clone() throws CloneNotSupportedException {
// 附件类本身的克隆
return (Attachment) super.clone();
}
}
// 2. 具体原型:邮件类
class Email implements Cloneable {
private String title;
private String content;
private Attachment attachment; // 引用类型
public Email(String title, String content, Attachment attachment) {
this.title = title;
this.content = content;
this.attachment = attachment;
// 模拟耗时的初始化过程
try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); }
}
public Attachment getAttachment() {
return attachment;
}
// 核心:实现克隆方法
@Override
public Email clone() {
Email clonedEmail = null;
try {
// 首先进行浅拷贝 (复制基本数据类型和引用地址)
clonedEmail = (Email) super.clone();
// 然后进行深拷贝 (把引用类型的对象也克隆一份全新的)
clonedEmail.attachment = this.attachment.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return clonedEmail;
}
public void display() {
System.out.println("邮件标题: " + title + " | 附件名: " + attachment.getFileName());
}
}
// 3. 客户端调用
public class PrototypeTest {
public static void main(String[] args) {
// 创建原型对象 (耗时)
Attachment attachment = new Attachment("初始设计稿.pdf");
Email originalEmail = new Email("项目启动会议", "请查看附件", attachment);
System.out.println("--- 原型对象 ---");
originalEmail.display();
// 通过克隆创建新对象 (极快,不需要再等1秒)
Email clonedEmail = originalEmail.clone();
System.out.println("\n--- 克隆对象 ---");
clonedEmail.display();
// 验证深拷贝:修改克隆对象的附件名称
clonedEmail.getAttachment().setFileName("最终设计稿.pdf");
System.out.println("\n--- 修改克隆对象的附件后 ---");
System.out.print("原邮件: ");
originalEmail.display(); // 原邮件附件不应被改变
System.out.print("克隆邮件: ");
clonedEmail.display(); // 克隆邮件附件已改变
}
}
注:除了重写 clone() 方法,在实际开发中,深拷贝也经常通过 序列化与反序列化 (Serialization) 或使用 JSON 转换工具 (如 Gson, Jackson) 来实现,这样可以避免繁琐地手动调用每一个子对象的 clone()。
四、实战案例
4.1 需求分析
假设我们需要开发一个在线考试系统。为了防止考生作弊,我们需要为每一位考生生成一份"专属试卷":
- 试卷包含的题目内容是相同的(保证公平性)。
- 每份试卷的题目顺序必须随机打乱。
- 每道选择题的选项顺序(A/B/C/D)必须随机打乱,且对应的正确答案标识也要随之动态映射。
如果使用传统方式,面对一万名考生,系统需要从数据库读取一万次题库,并执行一万次极其繁琐的组卷和洗牌逻辑,这无疑会压垮数据库和应用服务器。
我们需要实现一个试卷生成器:
- 基础物料: 选择题(
ChoiceQuestion)、问答题(AnswerQuestion)。 - 核心动作: 输入考生姓名和学号,输出一份题目打乱、选项打乱的专属试卷。
4.2 架构图
4.2.1 面条代码架构图

痛点:客户端每次调用,Controller 都要从头创建所有题目列表,相当于每次都走一次全量构建(模拟查库),耗时极高。
4.2.2 原型模式架构图

4.3 类图对比
4.3.1 面条代码类图
creates
creates
QuestionBankController
+createPaper(candidate: String, number: String) : String
ChoiceQuestion
-name: String
-option: Map<String, String>
-key: String
AnswerQuestion
-name: String
-key: String
4.3.2 原型模式类图

4.4 时序图
4.4.1 面条代码时序图
QuestionBankController ApiTest QuestionBankController ApiTest createPaper("花花", "1001") new ArrayList<ChoiceQuestion>() new ArrayList<AnswerQuestion>() StringBuilder 拼接 返回花花试卷 createPaper("豆豆", "1002") new ArrayList<ChoiceQuestion>() new ArrayList<AnswerQuestion>() StringBuilder 拼接 返回豆豆试卷
4.4.2 原型模式时序图
TopicRandomUtil QuestionBank (Prototype) QuestionBankController ApiTest TopicRandomUtil QuestionBank (Prototype) QuestionBankController ApiTest 构造函数中完成原型对象的初始化(只查一次库) loop [每道选择题] append(ChoiceQuestion) append(AnswerQuestion) createPaper("花花", "1001") clone() Collections.shuffle(题库) random(options, key) 乱序后的 Topic 返回克隆且乱序的新 QuestionBank 设置考生姓名/学号 返回花花试卷
4.5 代码分析
4.5.1 面条代码(if-else/硬编码)
在 codedesign3.0-1 中,QuestionBankController 的 createPaper 方法包揽了所有的脏活累活。
Java
public class QuestionBankController {
public String createPaper(String candidate, String number){
// 每次调用都会重新 new 集合,模拟了极大的资源开销
List<ChoiceQuestion> choiceQuestionList = new ArrayList<>();
choiceQuestionList.add(new ChoiceQuestion("JAVA所定义的版本中不包括", new HashMap<String, String>() {{ ... }}, "D"));
// ... 极其冗长的题目硬编码录入 ...
// 拼接试卷
StringBuilder detail = new StringBuilder(...);
// ...
return detail.toString();
}
}
问题暴露: 代码极度臃肿。如果在高并发场景下,一千个考生并发请求,由于没有对象复用机制,堆内存中会瞬间产生海量的冗余对象,垃圾回收(GC)压力剧增。
4.5.2 原型模式代码
这是优雅的重构版本。首先,我们将"试卷"抽象为一个 QuestionBank 类,并实现 Cloneable 接口。
最核心的逻辑在于 clone() 方法的重写:
Java
public class QuestionBank implements Cloneable{
// ... 属性省略 ...
@Override
public QuestionBank clone() throws CloneNotSupportedException{
// 1. 浅克隆基础对象
QuestionBank questionBank = (QuestionBank) super.clone();
// 2. 深克隆引用类型(关键点:将题目列表也复制一份,避免各试卷互相影响)
questionBank.choiceQuestionsList = (ArrayList<ChoiceQuestion>) choiceQuestionsList.clone();
questionBank.answerQuestionsList = (ArrayList<AnswerQuestion>) answerQuestionsList.clone();
// 3. 原型模式的高级应用:在克隆过程中顺便修改状态(题目乱序)
Collections.shuffle(choiceQuestionsList);
Collections.shuffle(answerQuestionsList);
// 4. 选项乱序处理
ArrayList<ChoiceQuestion> choiceQuestionsList = questionBank.choiceQuestionsList;
for (ChoiceQuestion choiceQuestion : choiceQuestionsList) {
Topic newTopic = TopicRandomUtil.random(choiceQuestion.getOption(), choiceQuestion.getKey());
choiceQuestion.setOption(newTopic.getOption());
choiceQuestion.setKey(newTopic.getKey());
}
return questionBank;
}
}
而在 QuestionBankController 中,工作量骤减:
Java
public class QuestionBankController {
// 作为原型对象,只在容器/系统启动时初始化一次
private QuestionBank questionBank = new QuestionBank();
public QuestionBankController(){
// ... 一次性加载巨量题库 ...
}
public String createPaper(String candidate, String number) throws CloneNotSupportedException{
// 极速生成试卷,直接在内存中基于二进制流进行拷贝,性能极高
QuestionBank questionBankClone = questionBank.clone();
questionBankClone.setCandidate(candidate);
questionBankClone.setNumber(number);
return questionBankClone.toString();
}
}
总结
通过引入原型模式,我们实现了从"每次重新构建"到"一次构建,无限克隆"的架构跃迁。这带来的直接好处是:
- 性能的大幅提升: 绕过了复杂的构造函数初始化过程,内存级别的对象复制在 JVM 中执行速度极快。
- 代码职责的解耦: Controller 不再负责繁琐的数据装配,它只需持有一个"母版"(Prototype),在需要时调用
clone即可。
工程实践注意点:
在使用原型模式时,务必警惕深拷贝与浅拷贝 的陷阱。在实战代码的 QuestionBank 中,如果只执行 super.clone(),那么所有的试卷将共享同一个题目列表的内存引用。一旦某一试卷打乱了题目,其他所有试卷也会同步被打乱。因此,针对对象内部包含的 ArrayList 或 Map 等引用类型,必须手动调用它们自身的 clone() 方法来实现彻底的深拷贝。
适用场景
- 资源优化场景: 类的初始化需要消耗非常多的资源(数据、硬件资源等)。
- 性能和安全要求的场景: 通过
new产生一个对象需要非常繁琐的数据准备或访问权限,可以使用原型模式提高性能。 - 一个对象多个修改者的场景: 一个对象需要提供给其他对象访问,并且各个调用者可能都需要修改其值。可以考虑使用原型模式拷贝多个对象供调用者使用(比如在某些状态机或规则引擎中保存历史状态)。