使用原型模式有哪些常见误区?
简化版
原型模式常见误区包括:把 clone() 等同于原型模式、忽略浅拷贝导致共享可变对象、复制了主键和状态字段、用 JSON 或 BeanUtils 盲目深拷贝、简单对象也硬套原型模式。原型模式要围绕业务复制语义设计,而不是机械复制字段。
详细版
常见误区:
- 认为原型模式就是 Java
Cloneable; - 不区分浅拷贝和深拷贝;
- 复制可变集合后多个对象互相影响;
- 把 ID、订单号、状态、创建时间等字段原样复制;
- 用序列化或 JSON 当万能深拷贝;
- 复制外部资源对象,如连接、线程、文件句柄;
- 原型对象被客户端修改,污染后续副本;
- 对象很简单也强行套原型模式;
- 对象结构变化后忘记更新复制逻辑;
- 没有明确哪些字段共享、哪些字段独立。
使用原型模式前,要先定义清楚复制边界。
完整版教学
一、误区一:把 clone 当成原型模式本身
clone() 只是实现复制的一种方式。
原型模式的核心是:
通过复制已有原型创建新对象
即使用拷贝构造器、copy() 方法、序列化,也可以实现原型模式。
不要把设计模式和某个语言 API 绑定死。
二、误区二:浅拷贝可变引用
这是最常见的问题。
copy.permissions = this.permissions;
这样新旧对象共享同一个权限列表。
如果副本修改权限,原型也被修改。
正确做法通常是:
copy.permissions = new ArrayList<>(this.permissions);
如果列表元素本身也是可变对象,还要继续复制元素。
三、误区三:复制业务唯一字段
很多字段不能原样复制:
- 数据库主键;
- 订单号;
- 用户唯一标识;
- 创建时间;
- 支付状态;
- 审批状态;
- 运行时缓存。
复制这些字段可能导致重复主键、状态错乱、审计信息错误。
复制方法必须明确哪些字段重新生成。
四、误区四:迷信自动深拷贝
JSON、序列化、BeanUtils 看起来方便,但不会理解业务。
它们不知道:
- 哪些字段不能复制;
- 哪些字段应该重置;
- 哪些对象可以共享;
- 哪些对象必须深拷贝;
- 哪些字段需要重新初始化。
自动工具可以辅助,但核心业务复制规则最好显式控制。
五、误区五:让客户端拿到并修改原型
如果原型注册表直接返回原型本体:
DocumentTemplate template = registry.get("contract");
template.setTitle("被污染的标题");
后续复制出来的对象都会受到影响。
更安全的方式是注册表直接返回副本:
DocumentTemplate doc = registry.create("contract");
原型本体应尽量只读或被封装保护。
六、误区六:简单对象也硬套原型
如果对象只有两三个字段:
new User(name, age)
直接构造就很清楚。
原型模式适合复杂对象、模板对象、大量相似对象。简单对象硬套会让代码变绕。
七、常见误区与追问
原型误用最常发生在复制边界不明确。假设用户模板包含 id=101、3个可变标签和1个共享的只读配置,复制后通常应重置 id、深拷贝标签、保留只读配置引用。把所有字段统一浅拷贝或统一序列化深拷贝,都可能破坏业务身份与共享语义。
| 检查维度 | 判定依据 |
|---|---|
| 业务身份字段 | 重置或重新生成 |
| 可变子对象 | 按独立性要求复制 |
心法:先给每个字段标注“共享、复制、重置”,再写复制代码。
- 误区:深拷贝一定比浅拷贝正确。 不可变配置和共享服务本就应共享,盲目深拷贝会制造错误语义。
- 追问:怎样发现漏拷贝的可变字段? 修改副本字段并断言原型不变,同时覆盖嵌套集合和元素对象。
- 误区:复制数据库实体可以保留主键。 把副本当新增实体保存时,原主键可能造成更新旧记录或唯一键冲突。
- 追问:原型注册表能返回内部原型吗? 不应直接暴露,通常返回副本以防调用者污染后续创建结果。
- 追问:什么对象不值得用原型? 构造便宜、字段少且没有模板状态的对象,直接构造更清晰。
八、加强记忆
原型模式最怕“复制得太随意”:浅拷贝共享可变对象,深拷贝乱复制业务唯一字段,工具类隐藏复制语义。正确做法是先定义复制边界,再选择 copy()、拷贝构造器或其他实现方式。