资讯动态

【设计模式系列 (五) 】原型模式

发布时间:2026/9/30 20:45:19 来源:尧图企业网站定制
⭐️在这个怀疑的年代我们依然需要信仰。个人主页 YYYing.⭐️设计模式系列专栏设计模式系列系列上期内容【设计模式系列 (四) 】建造者模式系列下期内容暂无目录1. 概述2. 结构3. 核心矛盾浅克隆 vs 深克隆4. 实现一浅克隆proto/ex14.1 抽象原型4.2 具体原型4.3 客户端验证共享5. 实现二深克隆proto/ex25.1 这段代码里的两个坑5.2 深克隆的另一种实现序列化6. 为什么 clone() 不直接返回具体类型7. 优缺点与适用环境7.1 典型用法原型管理器8. 面试专题8.1 开场题说说原型模式吧8.2 高频追问清单8.3 什么时候不该用原型结语创建型模式里原型模式是最容易被低估的一个。它的核心只有两个字克隆。本文代码取自 design-pattern-cpp 仓库的design-pattern/proto/使用 C11。为阅读方便片段省略了头文件保护宏。1. 概述WikipediaThe prototype pattern is a creational design pattern in software development. It is used whenthe type of objects to create is determined by a prototypical instance, which is cloned to produce new objects.原型模式是软件开发中的一种创造设计模式。当要创建的对象类型由原型实例确定时使用该实例被克隆以生成新对象。GoFSpecify the kinds of objects to create using a prototypical instance, and create new objects by copying this prototype.使用原型实例指定要创建的对象的种类并通过复制此原型来创建新对象。翻译过来一句话用原型实例指定要创建的对象种类并通过复制这个原型来创建新对象。两个关键词① 原型实例prototypical instance创建对象不再通过new 类名()而是从一个已经存在的实例身上复制。② 克隆clone原型模式解决的问题是创建代价高对象的构造需要查数据库、发网络请求、做大量计算 → 造一个就够剩下的复制运行时才知道要造哪种子类手里只有一个基类指针→ 让它自己 clone 自己比写一堆if/else判断类型再 new 优雅需要一个与现有对象状态相同、后续独立演化的副本 → 比如游戏关卡编辑器里的复制粘贴、撤销栈里的对象快照。2. 结构角色职责Prototype抽象原型声明clone()接口。所有原型必须实现它ConcretePrototype具体原型实现clone()返回自己的一个副本Client客户端调用clone()得到新对象不关心具体类型结构很简单一个虚函数 一次拷贝构造。难的是拷贝到底拷多深。3. 核心矛盾浅克隆 vs 深克隆这是原型模式真正要考的东西。C 默认的拷贝构造是逐成员拷贝逐字节复制对指针成员只复制地址本身成员类型默认拷贝的行为后果基本类型 / 值语义成员复制一份值✅ 两个对象互不影响指针 / 引用成员只复制地址❌ 两个对象指向同一份资源于是有了两个概念浅克隆Shallow Clone只复制值成员和指针本身指针指向的资源仍是共享的。改副本会影响原型两个对象析构时还会重复释放同一块内存。深克隆Deep Clone连指针指向的对象也复制一份副本与原型完全独立。一句话浅克隆共享资源深克隆复制资源。4. 实现一浅克隆proto/ex14.1 抽象原型namespace proto { class Prototyppe // 仓库里的原拼写多了一个 p { public: virtual Prototyppe* clone() 0; }; }4.2 具体原型仓库里用CC_SYNTHESIZE系列宏自动生成 getter/setter先看一眼宏展开才读得懂代码/** 定义受保护的变量并提供get、set方法 */ #define CC_SYNTHESIZE(varType, varName, funName)\ protected: varType varName;\ public: varType get##funName(void) const { return varName; }\ public: void set##funName(varType var){ varName var; }Level的成员里有std::vectorMonster*和Rewards*两个指针成员这正是浅克隆会出问题的地方class Level : public Prototyppe { CC_SYNTHESIZE(std::string, name, Name); CC_SYNTHESIZE(std::string, descrip, Descrip); CC_SYNTHESIZE_CR_GET(std::vectorMonster*, monsters, Monsters); // 返回引用才能 push_back CC_SYNTHESIZE(Rewards*, rew, Rew); CC_SYNTHESIZE_SET(bool, release, Release); // 只给 set不需要 get public: Prototyppe* clone() override { // 使用默认拷贝构造进行浅克隆 Level* copy new Level(*this); copy-setRelease(false); return copy; } Level() { release true; this-rew nullptr; } ~Level() { if (!release) return; // ← 副本不负责释放资源 if (this-rew) delete rew; for (auto one : monsters) delete one; monsters.clear(); } };关键在于release这个标记clone()造出的副本把release置为false于是它析构时直接 return不碰那些共享的Rewards和Monster。这是浅克隆避免 double free的典型权宜写法——资源只有原型自己释放副本只借用。代价也很明显副本一旦自己新增了Monster/Rewards就泄漏了。4.3 客户端验证共享Level* proto new Level(); proto-setName(普通山庄); proto-setDescrip(这是一个比较危险的山庄经常有山蜇出没.); proto-getMonsters().push_back(new Monster(10, 20)); proto-setRew(new Rewards(10)); ​ Level* copy dynamic_castLevel*(proto-clone()); copy-setDescrip(我修改了描述); // 值成员互不影响 std::cout proto: proto-getDescrip() std::endl; // 原描述 std::cout copy: copy-getDescrip() std::endl; // 我修改了描述 ​ copy-getRew()-setGold(30); // 指针成员改副本 改原型 std::cout proto: proto-getRew()-getGold() std::endl; // 30 ← 被带跑了 std::cout copy: copy-getRew()-getGold() std::endl; // 30copy-getRew()-setGold(30)这一行就是浅克隆的罪证只改了副本的奖励原型的金币也跟着变成 30。顺带一提Prototyppe*要dynamic_castLevel*才能拿回具体类型——这个问题下一节会讲怎么优雅解决。5. 实现二深克隆proto/ex2思路绕过默认拷贝构造自己写一套copy()/release()让拷贝构造和赋值运算符共用同一份深拷贝逻辑。class Level : public Prototyppe { CC_SYNTHESIZE(std::string, name, Name); CC_SYNTHESIZE(std::string, descrip, Descrip); CC_SYNTHESIZE_CR_GET(std::vectorMonster*, monsters, Monsters); CC_SYNTHESIZE(Rewards*, rew, Rew); private: void copy(const Level l) { this-name l.name; this-descrip l.descrip; // 值成员直接赋 if (l.rew) { this-rew new Rewards(*l.rew); // ★ 指针成员重新 new 一份 } for (auto m : l.monsters) { this-monsters.push_back(new Monster(*m)); // ★ 容器里的指针同样逐个深拷贝 } } void release() { if (this-rew) { delete rew; this-rew nullptr; } for (auto one : monsters) delete one; monsters.clear(); } public: Prototyppe* clone() override { // 使用拷贝构造进行克隆 return new Level(*this); } Level() { this-rew nullptr; } ~Level() { release(); } // 重写拷贝构造 Level(const Level l) { copy(l); } // 重载赋值运算符 Level operator(const Level l) { release(); // 先释放资源防止覆盖前泄漏 copy(l); // 再复制 return *this; } };三个要点copy()是深拷贝的唯一实现点拷贝构造和赋值运算符都复用它。这样两份逻辑不会走偏改一处即可。operator必须先 release 再 copy否则旧资源会被直接覆盖造成泄漏。clone()里的new Level(*this)走的就是被重写过的拷贝构造所以自动变成深克隆——clone()本身不需要懂深还是浅。此时再跑一遍客户端的验证代码原型打印 10副本打印 30两者彻底独立。5.1 这段代码里的两个坑① 自赋值没有防护Level operator(const Level l) { release(); // 如果 this l这一步已经把资源删了 copy(l); // 再去读已释放的内存 → UB return *this; }标准写法要在release()之前判断if (this l) return *this;② 异常安全copy()进行到一半抛异常比如new失败对象会停在已经 release、只拷了一半的半残状态析构时还可能二次释放。工程里更稳的做法是把裸指针换成智能指针std::unique_ptrRewards、std::vectorstd::unique_ptrMonster拷贝构造交给 RAII 管深克隆只需要在clone()里显式std::make_unique一份。5.2 深克隆的另一种实现序列化先把原型序列化成字节流再从字节流反序列化出新对象。优点不用给每个类手写copy()天然深拷贝对类的新增成员自动生效缺点引入序列化框架、性能差、遇到不可序列化的成员文件描述符、句柄、锁就歇菜。Java 的Object.clone()要求实现Cloneable接口本质上也是同一类思路的产物。C 里没有语言级的 clone 支持只能自己实现。6. 为什么clone()不直接返回具体类型4.3 节提到过仓库里返回Prototyppe*客户端要dynamic_cast才能拿回Level*。其实 C 支持协变返回类型covariant return typeLevel* clone() override { return new Level(*this); } // 合法派生类可以收窄返回类型派生类把返回类型改写为更具体的类型是允许的前提是返回指针或引用这样客户端就不用dynamic_cast了。C 里这是零成本的。但更值得注意的是原型模式的根本价值——多态地复制Prototyppe* p getSomePrototype(); // 运行时才知道是 Level 还是 Boss Prototyppe* copy p-clone(); // 克隆出来的必然是正确的那一种这件事拷贝构造函数做不到拷贝构造是编译期静态绑定的Prototyppe p someLevel;会发生对象切片slicing派生部分被切掉。想在只有一个基类指针的前提下复制出正确的派生类对象只能是虚函数——这就是clone()存在的意义。7. 优缺点与适用环境主要优点创建新对象不必知道具体类型客户端与具体类解耦逃避构造函数的约束——不需要为每种原型写一堆工厂类减少类的数量可以动态地增删原型运行期注册原型即可见下对创建代价高的对象复制比重新构造便宜得多。主要缺点每个类都必须实现 clone()而且深克隆要处理每一层引用关系引用环、共享成员实现复杂深克隆的成本可能接近甚至超过重新构造需要权衡必须小心处理浅克隆带来的资源重复释放问题与所有创建型模式一样克隆细节对客户端透明出问题时排查链更长。适用环境对象之间大同小异差异只是若干属性的取值对象的创建代价大数据准备、网络、复杂计算需要动态加载/注册原型运行期才知道要造什么配合原型管理器。7.1 典型用法原型管理器仓库里的ClassFactory工厂篇讲过的自注册反射机制就是同一思路的另一个形态。如果要做原型管理器通常就是一张表class PrototypeManager { std::mapstd::string, Prototyppe* protos; // 注册表名字 → 原型 public: void add(const std::string key, Prototyppe* p) { protos[key] p; } Prototyppe* create(const std::string key) { return protos[key]-clone(); } // 克隆出副本 };注意注册表里存的是原型样板取出来的一定是clone()的副本而不是原型本身。否则所有客户端会共用同一个对象就退化成全局变量了。8. 面试专题8.1 开场题说说原型模式吧考察点是概念、角色、应用场景按这个顺序答概念原型模式用原型实例指定创建对象的种类并通过拷贝这些原型来创建新对象。它属于创建型模式。角色抽象原型 Prototype 声明clone()具体原型 ConcretePrototype 实现clone()返回自己的副本客户端通过调用clone()得到新对象不关心具体类型。使用场景对象创建代价高要查库、走网络、复杂计算时对象之间只有少量属性不同时运行时才知道要创建哪种子类、手里只有基类指针时。核心难点浅克隆与深克隆的取舍。可扩展把浅/深克隆的区别、和拷贝构造/工厂模式的区别接上见下。8.2 高频追问清单追问答法浅克隆和深克隆的区别必问浅克隆只复制值成员和指针本身指针指向的资源仍是共享的深克隆连指针指向的对象一起复制两者完全独立。C 默认拷贝构造就是浅克隆深克隆怎么实现必问一是自己重写拷贝构造 重载赋值运算符集中到copy()里处理每一层引用二是序列化/反序列化通用但要引入框架且性能差原型模式和拷贝构造函数有什么区别拷贝构造是编译期静态绑定的用基类对象接派生类对象会发生对象切片clone()是虚函数能多态地复制——拿基类指针就能克隆出正确的派生类对象原型模式和工厂模式有什么区别工厂关注造哪个用类名/参数决定原型关注复制哪一个已有的用原型实例决定。工厂要新增产品时通常要加工厂类原型只需要多加一个原型对象甚至运行期动态注册为什么clone()返回基类指针为了让客户端通过基类接口调用实现多态复制。C 支持协变返回类型派生类可以把返回类型收窄为具体类型从而免掉dynamic_cast浅克隆时怎么防止 double free引用计数、智能指针推荐、或像仓库里那样用一个release标记让副本不参与资源释放原型对象的析构谁负责原型本身由创建它的人负责克隆出的副本由客户端负责。所有权关系必须写清楚否则就是上一问的 double free深克隆会遇到什么麻烦引用环A 引用 B、B 又引用 A会导致无限递归多个成员共享同一个对象时深拷贝要保证共享关系也被正确复制而不是各拷一份原型模式有什么缺点每个类都要实现clone()深克隆实现复杂、成本可能不比重新构造低容易踩浅克隆的资源重复释放什么场景不适合用原型对象结构简单、构造开销不大时直接new更直观对象里有不该被复制的资源连接、句柄、锁时深拷贝语义难以定义8.3 什么时候不该用原型判断标准就一条这个对象复制起来真的比重新造便宜吗成员只有几个值类型、构造函数就是几个赋值 → 直接new或拷贝构造clone()是纯成本对象持有外部资源数据库连接、文件句柄、互斥锁→ 深拷贝语义说不清楚浅拷贝又会共享别硬套只想复用一段初始化逻辑 → 那是一个工厂或构造辅助函数的事不是原型模式。原型模式的价值在于把复制一个复杂对象这件事封装成一次虚函数调用。对象不复杂的时候它换不来任何东西。结语我是YYYing后面还有更精彩的内容希望各位能多多关注支持一下主包。无限进步我们下次再见

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑