【设计模式精讲】8.原型模式(Prototype)
【摘要】:复制一个多态对象,最直觉的写法
Shape s = *proto;会发生对象切片——副本丢掉派生部分只剩基类骨架。本文从这次「复制后行为消失」的事故讲起,给出原型模式:把克隆动作委派给对象自己,clone()虚函数 + 拷贝构造配合,让「只认识接口」的代码也能造出正确类型的副本。C++ 实现沿 Refactoring Guru 的几何图形示例展开传统写法,进阶覆盖智能指针返回、CRTP 消除重复 clone、深浅拷贝抉择与「拷贝构造即内建原型」的辨析;文末考察 Boost 指针容器的new_clone体系与 Protobuf 的Clone(),并给出注册表形态的工程用法。
【关键词】:原型、clone、拷贝构造、对象切片、深拷贝、CRTP
1. 复制出来的图形「忘了」自己是谁
画板程序里,工具箱预置了配置好的图形,拖出来用:
// 说明性片段(省略图形类定义)
// ❌ 按基类复制:发生对象切片
Shape* proto = toolbox->get("circle");
Shape copy = *proto; // 只复制了基类部分!
copy.draw(); // 画出来的是「哑图」
copy 静态类型是 Shape,无论 *proto 运行时是什么,拷贝构造只切走基类子对象——派生类新增的半径、填充、特效全部丢失。C++ 特有的这个坑叫 对象切片(object slicing)。换 new?new Shape(*proto) 同样只构造基类;要让副本保持真实类型,创建者必须 在编译期就知道具体类——可工具箱只知道 Shape*。两难的本质:「复制一个对象」这个动作,需要具体类的知识,而持有者只有抽象接口。把复制委派给对象自己,问题即解——这就是原型模式。
2. 模式意图与定义
- 一句话定义(GoF 原文意图):Specify the kinds of objects to create using a prototypical instance, and create new objects by copying this prototype——用原型实例指定创建对象的种类,并通过复制这些原型创建新对象。RG 亦称「克隆模式(Clone)」:使你能够复制已有对象,而又无需使代码依赖它们所属的类。
- 解决的问题:RG 指出从外部复制的两重障碍——私有成员从外部不可见;必须知道对象的具体类才能 new 副本,而很多时候调用方只持有接口。把克隆收回对象内部(「绝大部分语言允许对象访问同类对象的私有成员」,C++ 亦然),两个障碍同时消失。
原型的第二个用途同样重要(GoF 动机):图形编辑器的工具箱里,每种工具持有一个 预配置好的原型实例(四分音符原型、移动工具原型),用户点击工具添加对象时只是 clone() 一下——「预生成原型」可以代替子类构造,运行时还能增删注册新种类,这是它比工厂更动态的一面。RG 的类比是细胞有丝分裂:原始细胞推动生成一对完全相同的细胞。
3. UML 图 + 结构说明
角色极其精简(RG 基本实现三角色):
- 原型(Prototype)接口:声明
clone(),绝大多数情况只有这一个方法; - 具体原型(Concrete Prototype):实现
clone()——调用自己的 拷贝构造 造一个同类新对象并返回;有极端情况要处理(关联对象要不要连带复制、递归依赖如何梳理),这正是深浅拷贝的抉择点(第 5 节); - 客户端:可复制任何实现原型接口的对象,无需知道其类。
C++ 的一个语言红利:clone() 的返回类型可以写 协变返回类型(Rectangle::clone() 返回 Rectangle*),比 GoF 伪代码更精确;另一个语言事实要警惕:拷贝构造本身就是 C++ 内建的原型机制,但它是静态绑定的——clone() 的存在只为补上「多态复制」这一块。
4. 传统 C++ 写法(C++11 之前)
按 RG 几何图形示例的传统 C++ 实现:基类提供「原型构造函数」(从同类对象复制成员),子类 clone() 调用它:
// C++98/03 写法
#include <iostream>
#include <string>
class Shape {
public:
Shape() : m_x(0), m_y(0) {}
Shape(const Shape& s) // 原型构造:复制基类成员
: m_x(s.m_x), m_y(s.m_y),
m_color(s.m_color) {}
virtual ~Shape() {}
void move(int x, int y) {
m_x = x;
m_y = y;
}
virtual void draw() const = 0;
// 协变返回:子类可返回更精确类型
virtual Shape* clone() const = 0;
protected:
int m_x, m_y;
std::string m_color;
};
class Rectangle : public Shape {
public:
Rectangle() : m_w(0), m_h(0) {}
Rectangle(const Rectangle& r)
: Shape(r), // 先复制基类部分
m_w(r.m_w), m_h(r.h()) {}
virtual void draw() const {
std::cout << "矩形 @" << m_x << ","
<< m_y << "\n";
}
virtual Rectangle* clone() const {
return new Rectangle(*this);
}
int h() const { return m_h; }
private:
int m_w, m_h;
};
class Circle : public Shape {
public:
Circle() : m_r(0) {}
Circle(const Circle& c)
: Shape(c), m_r(c.m_r) {}
virtual void draw() const {
std::cout << "圆 r=" << m_r << "\n";
}
virtual Circle* clone() const {
return new Circle(*this);
}
private:
int m_r;
};
客户端只见抽象:
// 说明性片段(接上例)
Shape* proto = new Circle(); // 实际来自工具箱
proto->move(10, 10);
Shape* copy = proto->clone(); // ✅ 真·Circle
copy->draw(); // 画的是圆,不是哑图
delete proto;
delete copy;
与传统写法配套的是 RG 的 原型注册表:预置一批配置好的原型,名称 → 原型 存进 map,create(name) 查表后 clone()——工具新增图形种类不用写新工厂,注册一个原型即可,甚至可以运行时替换。
5. 现代 C++ 进阶写法
升级一:智能指针交货 + 深浅拷贝抉择。clone() 返回 std::unique_ptr<Shape>,所有权随即清晰;拷贝构造照旧承担成员复制:
// 节选(Shape/Rectangle 定义同上,略)
#include <memory>
struct ModernShape {
virtual ~ModernShape() = default;
virtual void draw() const = 0;
virtual std::unique_ptr<ModernShape>
clone() const = 0;
};
struct ModernCircle : ModernShape {
void draw() const override {}
std::unique_ptr<ModernShape> clone()
const override {
// 调拷贝构造,隐式转基类指针
return std::make_unique<ModernCircle>(
*this);
}
};
深浅拷贝的抉择在智能指针时代更尖锐:成员若是 unique_ptr<Brush>,拷贝构造被隐式删除,必须手写「深拷贝」(m_brush(other.m_brush->clone()))或改 shared_ptr 共享;成员若是 std::string/std::vector 值语义类型,默认拷贝构造就是正确深拷贝。经验法则:值语义成员交给默认,多态成员逐个 clone,裸指针成员先还债。
升级二:CRTP 消灭重复的 clone。每个子类写一遍 clone() 语义相同又枯燥,Curiously Recurring Template Pattern 把它上提:
// 节选(ModernShape 定义同上,略)
#include <memory>
#include <type_traits>
template <typename Derived,
typename Base = ModernShape>
struct Cloneable : Base {
std::unique_ptr<Base> clone()
const override {
// 静态断言保证可拷贝构造
static_assert(
std::is_constructible<Derived,
const Derived&>::value,
"需要拷贝构造函数");
return std::make_unique<Derived>(
static_cast<const Derived&>(
*this));
}
};
struct ModernRect
: Cloneable<ModernRect> {
void draw() const override {}
};
// ModernRect 已自动获得 clone()
一点说明:static_assert 检查(C++11 <type_traits>,进阶标注)让「忘了写拷贝构造」在实例化时报错而不是运行时崩。代价是每个子类多一层模板基类,调试栈稍深,团队需熟悉 CRTP。
升级三:注册表 + 工厂合流。把第 5 篇的自注册工厂表搬过来,Maker 存「返回原型」而非「直接构造」——工厂每次 clone() 原型交货,正好把 RG「原型注册表」与「工厂不必每次 new、可返回已有对象」两个说法接通:原型即「被工厂复用的预生成对象」。这套合流在插件架构里尤其常见:宿主只认接口与注册表,插件自带原型注册进来,双方无需链接期共享任何具体类型。
6. 优缺点与适用场景
- ✅ 优点(GoF 后果清单 + RG):隐藏具体产品类,客户端只认
clone();运行时增减「种类」——注册一个原型即可上线新图形,比其他创建型模式更动态;以变值代替变类——高度动态的系统里,复制一个配置好的对象再微调,比定义新类轻得多;还能显著降低「相似对象反复初始化」的成本(重配置昂贵时克隆现成的)。 - ❌ 缺点:每个类都要正确实现
clone()/拷贝构造,深浅拷贝决策随成员结构演化反复回来找你(RG 提醒:克隆关联对象、梳理递归依赖是隐藏工作量);循环引用的对象图克隆需要格外小心;与 C++ 内建的拷贝构造并存,「两条复制路径」容易让人误用其一(切片 vs 正确克隆)。 - 🎯 适用场景:调用方只有接口、需要多态复制(工具箱、撤销栈快照、缓存模板);对象初始化昂贵且新对象与某个现成对象高度相似;种类需要在运行时动态增减。能靠
clone解决的,就不必为每类建工厂。
「拷贝构造还是 clone」再给一个可操作的判断流程。第一问:副本的使用方知道具体类型吗? 知道(同函数、同模块内)用拷贝构造,简洁零成本;不知道(跨接口、跨层传递)必须 clone。第二问:对象图里有多态成员吗? 有,则默认拷贝构造只会切基类,必须自定义并逐成员 clone(或改 shared_ptr 明示共享)。第三问:该类型会被「按名字批量复制」吗? 会,就值得补一个原型注册表,让调用方彻底摆脱具体类。三问都是「否」,恭喜你根本不需要原型——模式是给多态复制的问题准备的,不是给「想少写构造函数」准备的。回答完三问再动手,能少写一半本不该存在的克隆代码,也能让留下的那一半经得起成员结构演化的拷问。
〔辨析〕原型 vs 工厂方法(第 5 篇):RG 的对照最精辟——工厂方法基于继承(子类重写决定造什么)但无需初始化步骤;原型不基于继承(复制即创建)但需要「对被复制对象的复杂初始化」;原型常用来实现抽象工厂里的创建方法。原型 vs 拷贝构造:拷贝构造是静态绑定的内建复制,原型是 多态 复制——差异只在「编译期已知类型」与否,前者够用就别上后者。
7. 开源项目中的身影
Boost:指针容器把克隆做成了基础设施。boost::ptr_vector<T> 等 Boost.PointerContainer 面对的问题正是本文第 1 节的坑:容器存多态裸指针,拷贝容器时必须逐个按真实类型复制。Boost 的方案是自由函数 new_clone()/delete_clone() + 克隆分配器(Clone Allocator) 策略:
// 说明性片段(依赖 Boost,示意)
#include <boost/ptr_container/ptr_vector.hpp>
boost::ptr_vector<Shape> shapes;
shapes.push_back(new Circle()); // 容器接管
// 拷贝容器:逐个 new_clone,无切片
boost::ptr_vector<Shape> copy = shapes;
点评:默认的 heap_clone_allocator 经 ADL 找到你类型所在名字空间的 new_clone(const T&) 重载——没有虚函数也实现了多态克隆,代价是忘写重载时报错晦涩(Boost 文档自己都标注了这个编译错误的提示);view_clone_allocator 则切换为「不拥有、不克隆」的视图语义,同一个容器模板两种策略,思路值得收藏。
Protobuf:Clone() 是序列化框架的原型标配。Google Protobuf 的 Message 接口提供 virtual Message* Clone() const(新版返回 MessageLite),生成的每个消息类各自实现「按字段深拷贝」——收到消息后要分发多份处理、要落盘快照,都是 Clone() 一下。它示范了原型的工程形态:字段含指针/字符串的深拷贝语义由代码生成器统一保证,人工维护「每个类写对克隆」这个最大风险点被生成器消掉。现代 C++ 工程里「代码生成 + 原型」是常见搭档。
AOSP/图形栈的呼应:GoF 当年点名的 Known Uses(音乐编辑器的图符工具、macintosh 的 MacDraw 类应用)在今天演化为各类「模板对象」体系——Android 的 Canvas 变换、Chromium 的 PaintFilter 树都以「复制现成配置好的对象」替代「从零构造」。原型在开源世界的存在感不如工厂 loudly,但只要出现 clone()/Copy()/Duplicate() 方法名,背后几乎都是它。
本篇小结
多态世界里「复制」不是赋值那么简单:对象切片随时等你,具体类名又常常不在手上。原型模式把克隆动作还给对象自己——clone() 虚函数调自己的拷贝构造,协变返回保住精确类型,注册表让「种类」运行时可增减;现代 C++ 再用 unique_ptr 交货、CRTP 去重、值语义成员交给默认拷贝。经验法则收尾:值语义成员交给默认,多态成员逐个 clone,裸指针成员先还债;编译期已知类型就用拷贝构造,只有接口就上原型。 至此创建型五模式集齐,下一篇横向对比五种「造对象」的分工与选择决策树。
本文模式定义、几何图形示例与结构说明参考了 Refactoring Guru《设计模式》中文版「原型」一章,意图译文、运行时增减种类等后果参考了 GoF《Design Patterns》第 3 章 Prototype 一节。
&spm=1001.2101.3001.5002&articleId=164194163&d=1&t=3&u=3009c7cc39a44b11802fb27bc2b24b24)
449

被折叠的 条评论
为什么被折叠?



