【设计模式精讲】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>classShape{public:Shape():m_x(0),m_y(0){}Shape(constShape&s)// 原型构造:复制基类成员:m_x(s.m_x),m_y(s.m_y),m_color(s.m_color){}virtual~Shape(){}voidmove(intx,inty){m_x=x;m_y=y;}virtualvoiddraw()const=0;// 协变返回:子类可返回更精确类型virtualShape*clone()const=0;protected:intm_x,m_y;std::string m_color;};classRectangle:publicShape{public:Rectangle():m_w(0),m_h(0){}Rectangle(constRectangle&r):Shape(r),// 先复制基类部分m_w(r.m_w),m_h(r.h()){}virtualvoiddraw()const{std::cout<<"矩形 @"<<m_x<<","<<m_y<<"\n";}virtualRectangle*clone()const{returnnewRectangle(*this);}inth()const{returnm_h;}private:intm_w,m_h;};classCircle:publicShape{public:Circle():m_r(0){}Circle(constCircle&c):Shape(c),m_r(c.m_r){}virtualvoiddraw()const{std::cout<<"圆 r="<<m_r<<"\n";}virtualCircle*clone()const{returnnewCircle(*this);}private:intm_r;};客户端只见抽象:
// 说明性片段(接上例)Shape*proto=newCircle();// 实际来自工具箱proto->move(10,10);Shape*copy=proto->clone();// ✅ 真·Circlecopy->draw();// 画的是圆,不是哑图deleteproto;deletecopy;与传统写法配套的是 RG 的原型注册表:预置一批配置好的原型,名称 → 原型存进 map,create(name)查表后clone()——工具新增图形种类不用写新工厂,注册一个原型即可,甚至可以运行时替换。
5. 现代 C++ 进阶写法
升级一:智能指针交货 + 深浅拷贝抉择。clone()返回std::unique_ptr<Shape>,所有权随即清晰;拷贝构造照旧承担成员复制:
// 节选(Shape/Rectangle 定义同上,略)#include<memory>structModernShape{virtual~ModernShape()=default;virtualvoiddraw()const=0;virtualstd::unique_ptr<ModernShape>clone()const=0;};structModernCircle:ModernShape{voiddraw()constoverride{}std::unique_ptr<ModernShape>clone()constoverride{// 调拷贝构造,隐式转基类指针returnstd::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<typenameDerived,typenameBase=ModernShape>structCloneable:Base{std::unique_ptr<Base>clone()constoverride{// 静态断言保证可拷贝构造static_assert(std::is_constructible<Derived,constDerived&>::value,"需要拷贝构造函数");returnstd::make_unique<Derived>(static_cast<constDerived&>(*this));}};structModernRect:Cloneable<ModernRect>{voiddraw()constoverride{}};// 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(newCircle());// 容器接管// 拷贝容器:逐个 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 一节。