【设计模式精讲】8.原型模式(Prototype)
发布时间:2026/9/1 4:52:44 作者:尧图编辑部 阅读量:1,286
)
【设计模式精讲】8.原型模式Prototype【摘要】复制一个多态对象最直觉的写法Shape s *proto;会发生对象切片——副本丢掉派生部分只剩基类骨架。本文从这次「复制后行为消失」的事故讲起给出原型模式把克隆动作委派给对象自己clone()虚函数 拷贝构造配合让「只认识接口」的代码也能造出正确类型的副本。C 实现沿 Refactoring Guru 的几何图形示例展开传统写法进阶覆盖智能指针返回、CRTP 消除重复 clone、深浅拷贝抉择与「拷贝构造即内建原型」的辨析文末考察 Boost 指针容器的new_clone体系与 Protobuf 的Clone()并给出注册表形态的工程用法。【关键词】原型、clone、拷贝构造、对象切片、深拷贝、CRTP1. 复制出来的图形「忘了」自己是谁画板程序里工具箱预置了配置好的图形拖出来用// 说明性片段省略图形类定义// ❌ 按基类复制发生对象切片Shape*prototoolbox-get(circle);Shape copy*proto;// 只复制了基类部分copy.draw();// 画出来的是「哑图」copy静态类型是Shape无论*proto运行时是什么拷贝构造只切走基类子对象——派生类新增的半径、填充、特效全部丢失。C 特有的这个坑叫对象切片object slicing。换newnew 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 图 结构说明«abstract»Shape#x : int#y : int#color : stringclone() : Shape*Rectangle-width : int-height : intclone() : RectangleCircle-radius : intclone() : Circleclone 返回类型协变\nnew Rectangle(*this)角色极其精简RG 基本实现三角色原型Prototype接口声明clone()绝大多数情况只有这一个方法具体原型Concrete Prototype实现clone()——调用自己的拷贝构造造一个同类新对象并返回有极端情况要处理关联对象要不要连带复制、递归依赖如何梳理这正是深浅拷贝的抉择点第 5 节客户端可复制任何实现原型接口的对象无需知道其类。C 的一个语言红利clone()的返回类型可以写协变返回类型Rectangle::clone()返回Rectangle*比 GoF 伪代码更精确另一个语言事实要警惕拷贝构造本身就是 C 内建的原型机制但它是静态绑定的——clone()的存在只为补上「多态复制」这一块。4. 传统 C 写法C11 之前按 RG 几何图形示例的传统 C 实现基类提供「原型构造函数」从同类对象复制成员子类clone()调用它// C98/03 写法#includeiostream#includestringclassShape{public:Shape():m_x(0),m_y(0){}Shape(constShapes)// 原型构造复制基类成员:m_x(s.m_x),m_y(s.m_y),m_color(s.m_color){}virtual~Shape(){}voidmove(intx,inty){m_xx;m_yy;}virtualvoiddraw()const0;// 协变返回子类可返回更精确类型virtualShape*clone()const0;protected:intm_x,m_y;std::string m_color;};classRectangle:publicShape{public:Rectangle():m_w(0),m_h(0){}Rectangle(constRectangler):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(constCirclec):Shape(c),m_r(c.m_r){}virtualvoiddraw()const{std::cout圆 rm_r\n;}virtualCircle*clone()const{returnnewCircle(*this);}private:intm_r;};客户端只见抽象// 说明性片段接上例Shape*protonewCircle();// 实际来自工具箱proto-move(10,10);Shape*copyproto-clone();// ✅ 真·Circlecopy-draw();// 画的是圆不是哑图deleteproto;deletecopy;与传统写法配套的是 RG 的原型注册表预置一批配置好的原型名称 → 原型存进 mapcreate(name)查表后clone()——工具新增图形种类不用写新工厂注册一个原型即可甚至可以运行时替换。5. 现代 C 进阶写法升级一智能指针交货 深浅拷贝抉择。clone()返回std::unique_ptrShape所有权随即清晰拷贝构造照旧承担成员复制// 节选Shape/Rectangle 定义同上略#includememorystructModernShape{virtual~ModernShape()default;virtualvoiddraw()const0;virtualstd::unique_ptrModernShapeclone()const0;};structModernCircle:ModernShape{voiddraw()constoverride{}std::unique_ptrModernShapeclone()constoverride{// 调拷贝构造隐式转基类指针returnstd::make_uniqueModernCircle(*this);}};深浅拷贝的抉择在智能指针时代更尖锐成员若是unique_ptrBrush拷贝构造被隐式删除必须手写「深拷贝」m_brush(other.m_brush-clone())或改shared_ptr共享成员若是std::string/std::vector值语义类型默认拷贝构造就是正确深拷贝。经验法则值语义成员交给默认多态成员逐个clone裸指针成员先还债。升级二CRTP 消灭重复的 clone。每个子类写一遍clone()语义相同又枯燥Curiously Recurring Template Pattern 把它上提// 节选ModernShape 定义同上略#includememory#includetype_traitstemplatetypenameDerived,typenameBaseModernShapestructCloneable:Base{std::unique_ptrBaseclone()constoverride{// 静态断言保证可拷贝构造static_assert(std::is_constructibleDerived,constDerived::value,需要拷贝构造函数);returnstd::make_uniqueDerived(static_castconstDerived(*this));}};structModernRect:CloneableModernRect{voiddraw()constoverride{}};// ModernRect 已自动获得 clone()一点说明static_assert检查C11type_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_vectorT等 Boost.PointerContainer 面对的问题正是本文第 1 节的坑容器存多态裸指针拷贝容器时必须逐个按真实类型复制。Boost 的方案是自由函数new_clone()/delete_clone()克隆分配器Clone Allocator策略// 说明性片段依赖 Boost示意#includeboost/ptr_container/ptr_vector.hppboost::ptr_vectorShapeshapes;shapes.push_back(newCircle());// 容器接管// 拷贝容器逐个 new_clone无切片boost::ptr_vectorShapecopyshapes;点评默认的heap_clone_allocator经 ADL 找到你类型所在名字空间的new_clone(const T)重载——没有虚函数也实现了多态克隆代价是忘写重载时报错晦涩Boost 文档自己都标注了这个编译错误的提示view_clone_allocator则切换为「不拥有、不克隆」的视图语义同一个容器模板两种策略思路值得收藏。ProtobufClone()是序列化框架的原型标配。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 一节。