Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

03_原型模式 Prototype Pattern

1. 基本信息

中文名称: 原型模式

英文名称: Prototype Pattern

模式类型: 创建型设计模式

Rust 中常见实现方式: Clone trait、clone()clone_from()#[derive(Clone)]、手动实现 Clone

原型模式的核心是:通过复制已有对象来创建新对象,而不是每次都从零开始构造对象。

在 Rust 中,原型模式通常和 Clone 机制直接对应。只要一个类型实现了 Clone,就可以基于已有实例创建出一个新的实例。

示例代码


2. 模式核心思想

原型模式的核心思想是:

当创建一个对象的过程比较复杂,或者希望保留已有对象的大部分状态时,可以先准备一个“原型对象”,之后通过复制这个原型对象来创建新对象。

也就是说,原型模式关注的不是“如何 new 一个对象”,而是:

如何基于已有对象快速创建相似对象。

例如:

已有一个默认配置对象
已有一个模板文档对象
已有一个游戏角色基础对象
已有一个网络请求模板对象
已有一个任务配置模板对象

后续只需要在原型基础上稍作修改,就可以得到新的对象。


3. 这个模式解决什么问题

原型模式主要解决的是:对象创建成本高,或者对象初始化过程复杂时,如何更方便地创建相似对象。

在实际开发中,有些对象的构造过程可能比较繁琐:

字段很多
默认值复杂
初始化步骤多
需要读取配置文件
需要预先计算一些数据
大部分新对象都和已有对象相似

如果每次都重新构造,会导致代码重复,也容易漏掉某些初始化字段。

原型模式通过复制已有对象,可以减少重复构造逻辑。

它适合解决的问题是:

如何复用已有对象的状态?
如何减少重复初始化代码?
如何快速生成相似对象?
如何把对象创建过程从复杂构造改为复制已有模板?

4. 不使用这个模式会怎样

如果不使用原型模式,可能会出现以下问题:

每次创建对象都要重复填写大量字段。
构造逻辑散落在多个地方,难以维护。
默认配置一旦修改,需要同步修改很多创建代码。
创建复杂对象时容易遗漏字段。
对象之间大部分内容相同,却仍然重复初始化。

例如一个配置对象有十几个字段,但多数情况下只改其中一两个字段。如果每次都完整创建新对象,代码会变得很冗长。

原型模式的作用就是:保留已有对象的大部分内容,只针对差异部分进行修改。


5. 传统面向对象中的实现思路

在传统面向对象语言中,原型模式通常会定义一个原型接口,例如:

Prototype
    └── clone()

具体对象实现这个接口,然后通过 clone() 方法复制自身。

传统结构一般是:

原型接口:声明 clone 方法
具体原型类:实现 clone 方法
客户端:通过 clone 复制已有对象

这种写法的核心是:

调用方不直接依赖具体构造过程,而是通过复制已有对象来获得新对象。

在 Java、C++ 等语言中,原型模式还经常会讨论:

浅拷贝
深拷贝
对象复制后的独立性
引用字段是否共享

因为如果对象中包含指针、引用、集合或嵌套对象,复制时就需要考虑:新对象是否真正独立,还是仍然共享内部数据。


6. Rust 中的实现思路

6.1 Rust 中通常怎么实现

在 Rust 中,原型模式通常通过 Clone trait 实现。

Rust 标准库已经提供了统一的复制接口:

Clone trait
    └── clone()

对于字段都支持克隆的结构体,可以直接使用:

#[derive(Clone)]

这样编译器会自动为该类型生成 clone() 方法。

如果结构体中某些字段需要特殊处理,也可以手动实现 Clone

Rust 中的原型模式可以理解为:

已有对象
→ 调用 clone()
→ 生成一个新的对象
→ 根据需要修改部分字段

因此,Rust 不需要像传统 OOP 那样专门设计一个 Prototype 接口,因为 Clone 本身已经承担了这个角色。


6.2 和传统 OOP 写法相比有什么不同

和传统面向对象语言相比,Rust 的原型模式有几个明显特点。

第一,Rust 已经把复制能力标准化为 Clone trait。 传统 OOP 中可能需要自己定义 Prototype 接口,而 Rust 中通常直接实现或派生 Clone

第二,Rust 区分 CopyClone。 这是 Rust 和很多传统语言的重要区别。

Copy:隐式复制,适合简单栈上数据,例如整数、布尔值、字符等
Clone:显式复制,需要调用 clone(),可能有一定成本

也就是说,在 Rust 中,如果一个对象复制可能有成本,通常需要显式写出 clone()。这让复制行为更加明确。

第三,Rust 的所有权机制会让“复制”和“移动”区分得非常清楚。 在 Rust 中,赋值默认可能是移动所有权,而不是复制对象。只有实现 Copy 的类型才会自动复制;其他类型如果想复制,必须显式调用 clone()

第四,Rust 更强调复制成本的可见性。 比如 StringVecHashMap 等类型调用 clone() 时,通常会复制堆上的数据。这可能带来内存分配和数据拷贝成本。Rust 通过显式 clone() 让开发者意识到这里发生了复制。

第五,Rust 中的原型模式通常不需要继承体系。 传统原型模式可能依赖父类接口、虚函数或多态复制,而 Rust 通常通过 trait、泛型和具体类型的 Clone 实现来完成。


6.3 Rust 中是否有更自然的替代写法

在 Rust 中,很多时候不需要完整照搬传统原型模式,而是根据对象复杂度选择不同方式。

如果只是简单复制对象,可以使用:

Clone

如果只是复制简单标量类型,可以使用:

Copy

如果想共享大对象,避免完整复制,可以考虑:

Rc<T>
Arc<T>

如果想实现“写时复制”,可以考虑:

Cow<T>

如果对象字段很多,但只想改其中几个字段,可以结合结构体更新语法:

基于已有对象复制大部分字段,只修改少数字段

所以 Rust 中的原型模式不一定总是“深拷贝一个对象”,还要根据需求判断:

是真的需要复制一份?
还是只需要共享?
是否允许修改?
是否需要线程安全?
复制成本是否可以接受?

7. Rust 中涉及的语言特性

原型模式在 Rust 中主要涉及以下语言特性:

Clone trait
Copy trait
#[derive(Clone)]
手动实现 Clone
所有权移动
结构体更新语法
Rc / Arc 共享所有权
Cow 写时复制

这些特性分别解决的问题是:

Clone:显式复制对象
Copy:低成本隐式复制
derive(Clone):自动生成克隆逻辑
手动实现 Clone:控制复制细节
所有权:区分移动和复制
Rc / Arc:避免重复复制大对象
Cow:需要修改时才复制

其中最核心的是 Clone

可以这样理解:

原型模式关心“从已有对象创建新对象”
Rust 的 Clone 负责“如何复制已有对象”
所有权系统负责“复制、移动和共享之间的边界”

8. 性能与工程代价

原型模式的性能关键在于:复制到底有多贵。

在 Rust 中,调用 clone() 并不一定是轻量操作。它的成本取决于对象内部结构。

如果对象只包含简单类型,例如数字、布尔值、枚举小值,那么复制成本通常很低。

如果对象包含:

String
Vec
HashMap
BTreeMap
大型嵌套结构
堆上数据
文件缓存
图像数据
网络缓冲区

那么 clone() 可能会触发堆内存分配和大量数据复制。

因此,在 Rust 中使用原型模式时,需要注意以下几点:

不要无意识地 clone 大对象。
clone() 是显式操作,看到它就应该想到可能有复制成本。
对大对象可以考虑 Rc 或 Arc 共享数据。
对读多写少的数据可以考虑 Cow。
性能敏感场景下,需要评估 clone 是否导致额外内存分配。

从工程角度看,原型模式可以减少复杂对象构造代码,但也可能隐藏复制成本。

一句话总结:

原型模式可以降低创建复杂对象的代码成本,但在 Rust 中必须关注 clone 带来的内存和性能成本。

9. 典型应用场景

原型模式适合用于以下场景:

对象创建过程复杂,但多个对象大部分字段相同。
需要基于默认配置创建多个相似配置。
需要复制模板对象并进行局部修改。
游戏开发中基于基础角色复制不同实例。
文档编辑器中复制页面、图形或组件。
编译器或解释器中复制 AST 节点或中间表示。
Web 请求中复用请求模板。
测试代码中快速生成相似测试数据。

在真实 Rust 项目中,原型模式常见于:

配置管理
测试数据构造
命令行参数模板
任务配置复制
缓存数据复用
数据结构快照
状态备份

例如,在批量编译实验中,如果每个 crate 的任务配置大体相同,只是 crate 名称、路径、版本号不同,就可以先构造一个基础任务配置,再基于这个配置复制并修改部分字段。


10. 和相似模式的区别

原型模式容易和工厂模式、构建者模式、备忘录模式、享元模式混淆。

10.1 原型模式和工厂模式

二者都属于创建型模式,但关注点不同。

工厂模式关注:根据条件创建不同类型的对象。
原型模式关注:通过复制已有对象创建相似对象。

工厂模式更像是:

我要什么类型,你帮我创建。

原型模式更像是:

我已经有一个模板,你帮我复制一份。

10.2 原型模式和构建者模式

构建者模式关注复杂对象的分步骤构造。

原型模式关注基于已有对象复制。

构建者模式:一步一步设置字段,最后 build。
原型模式:先有一个现成对象,再 clone 一份。

如果对象需要从零开始灵活配置,构建者模式更合适。

如果对象大部分内容相同,只改少数字段,原型模式更合适。


10.3 原型模式和备忘录模式

二者都可能涉及对象状态复制,但目的不同。

原型模式:复制对象是为了创建新对象。
备忘录模式:保存对象状态是为了之后恢复。

例如:

复制一个配置模板创建新配置:原型模式
保存编辑器当前状态用于撤销:备忘录模式

10.4 原型模式和享元模式

二者都和对象复用有关,但方向不同。

原型模式:复制已有对象,得到新对象。
享元模式:共享已有对象,避免创建太多对象。

原型模式可能增加对象数量,享元模式则是减少重复对象。


11. 使用该模式的优点

原型模式的优点主要有:

可以减少复杂对象的重复创建代码。
可以基于已有对象快速生成相似对象。
可以保留对象的大部分已有状态,只修改局部字段。
对测试数据和默认配置非常方便。
在 Rust 中通过 Clone 实现非常自然。
可以避免把复杂构造逻辑散落在多个地方。

从工程角度看,原型模式让“创建一个相似对象”变得简单,尤其适合对象字段较多、默认配置较复杂的情况。


12. 使用该模式的代价

原型模式的代价主要包括:

clone() 可能带来额外内存分配。
大对象复制成本可能较高。
如果复制后仍然共享内部资源,可能需要关注共享修改问题。
如果手动实现 Clone,逻辑可能比较复杂。
过度 clone 可能掩盖所有权设计问题。
不清楚深拷贝和共享之间的区别时,容易写出性能较差的代码。

在 Rust 中,滥用 clone() 是初学者常见问题。

有时候调用 clone() 只是为了绕过所有权或借用检查,但这并不一定是最好的设计。更合理的方式可能是重新设计数据所有权,或者使用引用、RcArc 等共享机制。


13. 什么时候不应该使用

以下情况不适合使用原型模式:

对象创建过程很简单,直接构造即可。
对象很大,复制成本很高。
实际上只需要共享数据,而不是复制数据。
复制语义不清晰,容易产生误解。
clone() 只是为了绕过所有权问题。
对象包含不可复制资源,例如文件句柄、网络连接、锁等。

尤其要注意最后一点。

不是所有对象都适合复制。例如文件句柄、线程句柄、网络连接、数据库连接等资源型对象,复制语义可能不明确。即使可以封装,也需要非常谨慎。

在 Rust 中,可以遵循一个简单原则:

如果复制的是数据,Clone 通常合理。
如果复制的是资源句柄,要谨慎设计复制语义。

14. 一个简单例子思路

可以设计这样一个例子来理解原型模式:

场景:
有一个服务器配置模板,其中包含 host、port、timeout、retry、use_tls、log_level 等字段。

需求:
不同环境下需要创建多个相似配置,例如开发环境、测试环境、生产环境。

处理流程:
1. 先创建一个默认配置对象。
2. 基于默认配置 clone 一份。
3. 修改其中少数字段,例如端口号或日志等级。
4. 得到新的配置对象。

这个例子可以体现原型模式的完整流程:

默认配置模板
→ 复制一份
→ 修改少数字段
→ 得到新配置

适合练习的 Rust 特性包括:

Clone
#[derive(Clone)]
结构体更新语法
所有权移动
String 和 Vec 的 clone 成本

15. 总结一句话

原型模式的本质是:

通过复制已有对象来创建新对象,适合对象创建过程复杂、但新对象与已有对象大部分内容相同的场景。

在 Rust 中,原型模式通常不需要单独设计复杂的原型接口,而是直接依赖 Clone trait。使用时要重点区分 CloneCopy、所有权移动和共享引用,并注意大对象复制可能带来的性能成本。