当前位置: 首页 > news >正文

【设计模式精讲】8.原型模式(Prototype)

【设计模式精讲】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)。换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 : string

+clone() : Shape*

Rectangle

-width : int

-height : int

+clone() : Rectangle

Circle

-radius : int

+clone() : Circle

clone 返回类型协变\nnew Rectangle(*this)

角色极其精简(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 一节。

http://www.cnnetsun.cn/news/4344064.html

相关文章:

  • 安卓4老电视没有输入法?从APK安装到ADB的完整解决指南
  • Cesium三维淹没分析:热力图可视化水深分布实践
  • 27届大模型面试准备(七十):大模型推理服务的负载均衡与智能请求路由
  • 0x28通信控制服务测试用例设计:从需求拆解到落地实践
  • 武汉国家开放大学怎么报名?靠谱教育机构怎么选?华祺教育优势详解
  • 基于微信小程序的餐厅预约系统设计与实现源码+文档+讲解视频
  • 深度学习+CNN 深度学习大白菜病害检测系统预测模型完整项目源码+训练脚本+评估指标【AI毕设】
  • Codex多Agent加密:你的AI编程Agent正在变成监察黑洞
  • STM32F103C8T6驱动WS2812B灯带:基于PWM+DMA的完整方案
  • 2026年AI岗位能力要求与工程实践:从RAG到模型部署全解析
  • 工厂必须设置的安全标识有哪些?分别放在什么位置?
  • 一张图彻底看懂5G RF前端:从PA、ET、FEMiD、Duplexer到Antenna Tuner,为什么中间能损失4~5dB?
  • AI生成测试用例实战:提示词工程与结构化输出设计
  • SpringBoot+Vue入校申报审批系统:从设计到部署全解析
  • Zotero AI插件批量生成文献精读笔记实战指南
  • 数据科学家私藏!5个Python冷门库,告别重复劳动,效率翻10倍
  • Spring Boot+Vue全栈实战:构建自动化网站监控系统
  • 技术经纪人如何提升匹配效率与专业能力?
  • Live2D动画项目工程化全流程:从原画拆分到Web集成的“和弦”实践
  • Python实战:打造象棋打谱与AI分析桌面小软件
  • CNC刀具直径精准测量全攻略:从工具选择到实战流程
  • Windows USB插拔记录清理指南:注册表、日志与一键脚本
  • UDS 0x28通信控制服务测试用例设计:从协议规范到CANoe自动化验证
  • C#基于KEPServerEx的OPC UA客户端开发实战:从配置到排错
  • 【C++算法】动态规划背包问题 -> 01背包
  • 美团前端移动端笔试复盘:核心考点与手写代码实战思路
  • 飞牛NAS内网穿透实战:零公网IP实现远程访问
  • 东芝REGZA ZX电视:如何通过画质引擎与Mini LED技术实现沉浸式观影
  • 开源象棋引擎核心原理与二次开发实战解析
  • HIS系统毕业设计实战:SSM框架+RABC权限管理全解析