05 原型(创建型)
从头造一个对象太贵?那就照着现成的拷一个。讲清原型模式的“以复制代替新建”思路、Go 结构体值拷贝的天然优势,以及深拷贝 vs 浅拷贝这个能坑死人的头号陷阱。
05 原型 · Prototype(创建型)
创建型的最后一位。前四个模式都在琢磨“怎么把一个对象造出来”,原型模式偏不:别造了,照着现成的拷一个不就完了?
一、这是什么:复印一份再改,别重写
你要写一份合同。是打开空白文档从头敲,还是找一份去年的旧合同,复制一份,改几个关键字段?显然后者快得多——绝大部分条款是现成的,你只需在副本上动几处。
原型模式就是这个思路:系统里已经有一个配置好的对象(原型),需要新对象时,不从零构造,而是克隆这个原型,再在副本上做少量修改。
配套代码克隆图形:有一个配置好的 Circle,要新圆时直接 Clone() 出一份,改改位置就用,不用重新设置颜色、半径等一堆属性。
二、解决什么痛:新建太贵,或者你不想认识它的类
原型模式主要止两种痛:
- “造一个”的成本很高。 有些对象初始化要查数据库、跑复杂计算、读网络、解析大文件。如果你手头已经有一个初始化好的同类对象,拷贝内存几乎总比重新初始化便宜得多。游戏里刷一片一模一样的小怪,与其每只都重新加载模型和属性,不如以一只为模板疯狂复制。
- 你想要副本,却不想依赖它的具体类。 你手上是个抽象的
Shape,想复制它。但它到底是Circle还是Rectangle?你不知道,也不想用一堆if type == Circle去判断再分别 new。让对象自己提供Clone(),复制这件事就交给它自己——它最清楚怎么拷贝自己。
原型隔离的“会变的东西”,是“具体要复制哪个类”。 客户端只对着 Clone() 喊一声,至于复制出的是圆是方,由对象自身负责。这一点和工厂方法异曲同工,只是手段从“new”换成了“copy”。
三、怎么实现:Go 结构体天生会拷贝
这是 Go 占尽便宜的一个模式。因为 Go 的结构体赋值(cp := *c)本身就是逐字段的值拷贝,对只含值类型字段的结构体来说,一行就完成了克隆:
type Shape interface {
Clone() Shape
Info() string
SetPosition(x, y int)
}
type Circle struct {
Color string
X, Y int
Radius int
}
// Clone 返回自身的一份拷贝。Go 中 *c 解引用再赋值,
// 就是逐字段值拷贝——对全值类型字段的结构体,这即是深拷贝。
func (c *Circle) Clone() Shape {
cp := *c // ← 一行搞定
return &cp
}用起来,改副本完全不影响原型:
original := &Circle{Color: "红色", X: 10, Y: 20, Radius: 5}
cloned := original.Clone()
cloned.SetPosition(100, 200) // 只动副本
// original 依旧是 (10,20),纹丝不动Java 里就没这么轻松了,得实现 Cloneable 接口、覆写 clone()、处理受检异常——而且默认还是浅拷贝,坑一堆(见下)。
四、有哪些坑:深拷贝 vs 浅拷贝,头号杀手
这是原型模式最危险、最必须讲清的一节。上面 cp := *c 之所以是完整拷贝,全靠 Circle 的字段都是值类型(string、int)。一旦结构体里含有指针、slice、map、channel 这类引用类型,cp := *c 就只拷贝了“引用”本身——副本和原型会共享底层数据:
type Config struct {
Name string
Tags []string // ← 引用类型!
}
func (c *Config) Clone() *Config {
cp := *c // 浅拷贝:cp.Tags 和 c.Tags 指向同一个底层数组
return &cp
}
orig := &Config{Name: "prod", Tags: []string{"a", "b"}}
clone := orig.Clone()
clone.Tags[0] = "HACKED" // 想改副本...
// 结果 orig.Tags[0] 也变成了 "HACKED"!两个对象共享同一个数组。正确的深拷贝必须手动把引用类型字段也复制一份:
func (c *Config) Clone() *Config {
cp := *c
cp.Tags = append([]string(nil), c.Tags...) // 复制一份新的底层数组
return &cp
}这是原型模式踩坑率数一数二的地方。 记死一句话:cp := *c 只拷贝“第一层”。 结构体里凡是指针、slice、map,浅拷贝后都和原型共享同一份底层数据,改一个动全家。深拷贝要不要做、做到几层,取决于你的对象结构——嵌套引用越深,越要小心,必要时逐层手写或借助序列化(如 json.Marshal 再 Unmarshal,虽慢但省心)。循环引用更是深拷贝的经典噩梦,遇到要专门处理。
另外两个小坑:
- Java 的
Cloneable是反面教材。 《Effective Java》专门开一条批判它:clone()机制设计得别扭(受检异常、Object.clone()的浅拷贝默认、和构造函数不一致)。Java 社区更推荐用拷贝构造函数或拷贝工厂代替Cloneable。这说明:即便是 GoF 钦定的模式,具体语言的实现也可能是个坑,别盲从。 - 别为了用而用。 如果你的对象构造成本很低,直接 new 一个新的、干干净净,比“克隆再修改”更清晰,也没有共享数据的风险。原型的价值,建立在“新建确实贵”或“确实要摆脱具体类”之上。
五、开源里怎么用
- Go 的
Clone()/Copy()家族:proto.Clone()(Protocol Buffers 深拷贝一个 message)、很多库给复杂配置对象提供的DeepCopy()。Kubernetes 尤其典型——它的 API 对象(Pod、Deployment 等)几乎每个都自动生成了DeepCopy()方法(zz_generated.deepcopy.go),因为 K8s 内部大量传递对象副本以避免并发修改共享状态,这是原型模式在超大型项目里的工业级应用。 - Go 的值语义:
time.Time、net/url.URL等被设计成值类型,赋值即安全拷贝——语言层面把“原型”变成了默认行为。 - JavaScript
structuredClone():浏览器/Node 内置的深拷贝函数,一行完成复杂对象(含嵌套、循环引用)的深克隆,是原型思路的现代标配。 - 各类编辑器/设计工具的“复制粘贴”:复制一个图层、一个组件、一个节点,底层几乎都是“克隆原型对象再改属性”,和配套代码的图形克隆一模一样。
创建型五连,收工。 回头看这一组的主线:它们全在管一件事——别让“创建对象”这个动作,把你的业务代码绑死在具体类型上。 单例管数量,工厂管“造哪个”,抽象工厂管“造哪族”,建造者管“怎么拼”,原型管“照着拷”。
下一章进入结构型:对象已经造好了,该研究怎么把它们拼装成更大的结构,还不别扭。第一个出场的是最实用的老黄牛——适配器。