原型模式有哪些角色?调用流程是什么?
简化版
原型模式通常有三个角色:Prototype 定义复制接口,ConcretePrototype 实现具体复制逻辑,Client 通过原型对象创建新对象。调用流程是客户端拿到一个原型对象,调用复制方法得到新对象,再按需修改差异字段。
详细版
原型模式的典型角色:
| 角色 | 作用 |
|---|---|
| Prototype | 抽象原型,定义 clone() 或 copy() |
| ConcretePrototype | 具体原型,实现复制逻辑 |
| Client | 使用原型复制新对象 |
| PrototypeRegistry | 可选角色,集中管理多个原型对象 |
典型流程:
Client 获取原型对象
Client 调用 prototype.copy()
ConcretePrototype 创建并返回副本
Client 修改副本的差异字段
如果系统有多种原型,可以使用注册表:
registry.get("vip-user").copy()
registry.get("normal-user").copy()
原型模式让客户端不必关心复杂对象如何初始化,只需要复制合适的模板。
完整版教学
一、Prototype:定义复制能力
Prototype 是抽象接口。
interface Prototype<T> {
T copy();
}
用 copy() 比直接暴露 Java 的 clone() 更清晰,因为 clone() 在 Java 里有一些历史包袱,例如访问权限、Cloneable 标记接口和异常处理。
二、ConcretePrototype:实现复制逻辑
具体原型类负责创建自身副本。
class DocumentTemplate implements Prototype<DocumentTemplate> {
private String title;
private List<String> sections;
public DocumentTemplate copy() {
DocumentTemplate copy = new DocumentTemplate();
copy.title = this.title;
copy.sections = new ArrayList<>(this.sections);
return copy;
}
}
复制逻辑由对象自己掌握,因为对象最清楚哪些字段要复制、哪些字段要重新生成、哪些字段可以共享。
三、Client:使用原型但不关心构造细节
客户端只使用复制结果:
DocumentTemplate base = registry.get("tech-doc");
DocumentTemplate doc = base.copy();
doc.setTitle("原型模式面试题");
客户端不需要知道默认章节、样式、权限等字段如何初始化。
这能减少重复初始化代码,也能隐藏复杂构造细节。
四、PrototypeRegistry:集中管理原型
如果只有一个原型对象,客户端直接持有即可。
如果原型很多,可以使用注册表:
class PrototypeRegistry {
private final Map<String, Prototype<?>> prototypes = new HashMap<>();
void register(String key, Prototype<?> prototype) {
prototypes.put(key, prototype);
}
Prototype<?> get(String key) {
return prototypes.get(key);
}
}
注册表让客户端通过名称获取模板,而不是到处创建原型对象。
五、调用流程中的关键边界
复制时要明确三件事:
- 副本是否是新对象;
- 可变引用是否需要深拷贝;
- ID、创建时间、状态等字段是否应该重新生成。
例如复制订单模板时,商品配置可以复制,但订单号不能复制。
这类字段如果不处理,会产生严重业务问题。
六、常见误区与追问
角色流程可用注册表场景验证:先登记2个配置完整的原型,客户端按 key 取出时得到副本,而不是内部模板本身。ConcretePrototype 最了解自身字段,应承担复制规则;Registry 只负责选择和保存,不应偷偷决定每个字段的深浅复制。
| 检查维度 | 判定依据 |
|---|---|
| Prototype | 声明复制契约 |
| Registry | 按键选择模板并返回副本 |
记忆钩子:原型负责“怎么复制”,注册表负责“复制哪个”。
- 误区:原型模式必须有注册表。 注册表是常见配套角色,不是模式成立的必要条件。
- 追问:谁负责初始化原型? 启动配置、工厂或管理服务都可以,关键是初始化与复制职责分离。
- 误区:Client 应拿到原型后自行补齐深拷贝。 这会让复制规则散落,具体原型应封装完整契约。
- 追问:注册表并发读取安全吗? 模板不可变且映射安全发布时读取较简单,动态更新则需并发控制。
- 追问:如何防止原型被污染? 注册时保存受控模板,获取时只返回副本,并限制外部直接修改内部对象。
七、加强记忆
原型模式的角色可以记成“接口定义复制,具体类负责复制,客户端拿副本使用”。如果原型很多,再加一个注册表统一管理。流程简单,但复制边界要谨慎,尤其是可变引用、唯一标识和业务状态字段。