新谈设计模式 Chapter 05 — 原型模式 Prototype

张开发
2026/4/3 16:57:53 15 分钟阅读
新谈设计模式 Chapter 05 — 原型模式 Prototype
Chapter 05 — 原型模式 Prototype灵魂速记复印机——照着原件复制一份省得从头再造。秒懂类比你有一份精心排版的简历模板。每次投不同公司你不是从头写一份新的而是复印一份改几个字。原型模式就是这个复印动作——通过克隆已有对象来创建新对象。问题引入// 灾难现场你拿到一个对象想复制一份voidduplicate(Shape*shape){// 问题1shape 是 CircleRectangleTriangle你不知道// 问题2就算知道有些属性是 private 的你访问不了// 问题3你依赖了具体类型违反了面向接口编程if(auto*cdynamic_castCircle*(shape)){auto*copynewCircle(*c);// 要写一堆 dynamic_cast}elseif(auto*rdynamic_castRectangle*(shape)){auto*copynewRectangle(*r);}// 每加一种形状这里就要加一段……}你需要的是不管什么类型调一个方法就能得到一个一模一样的副本。模式结构┌───────────────┐ │ Prototype │ ├───────────────┤ │ clone() │ ← 核心自我复制的能力 └───────┬───────┘ │ 实现 ┌────┴─────┐ │ │ ┌──┴──┐ ┌───┴────┐ │CircleP│ │RectangleP│ │clone()│ │ clone() │ └──────┘ └─────────┘精髓每个对象自己知道怎么复制自己因为自己能访问自己的 private 成员。C 实现#includeiostream#includememory#includestring#includeunordered_map// 原型接口 classShape{public:virtual~Shape()default;virtualstd::unique_ptrShapeclone()const0;// 核心方法virtualvoiddraw()const0;voidsetColor(conststd::stringcolor){color_color;}std::stringcolor()const{returncolor_;}protected:std::string color_black;};// 具体原型 classCircle:publicShape{public:Circle(doubleradius):radius_(radius){}// 拷贝构造 clone —— 自己复制自己Circle(constCircleother)default;std::unique_ptrShapeclone()constoverride{returnstd::make_uniqueCircle(*this);// 调用拷贝构造}voiddraw()constoverride{std::cout○ Circle(rradius_, colorcolor_)\n;}private:doubleradius_;};classRectangle:publicShape{public:Rectangle(doublew,doubleh):width_(w),height_(h){}Rectangle(constRectangleother)default;std::unique_ptrShapeclone()constoverride{returnstd::make_uniqueRectangle(*this);}voiddraw()constoverride{std::cout□ Rectangle(width_×height_, colorcolor_)\n;}private:doublewidth_,height_;};// 原型注册表可选方便管理模板 classShapeRegistry{public:voidaddPrototype(conststd::stringname,std::unique_ptrShapeproto){prototypes_[name]std::move(proto);}std::unique_ptrShapecreate(conststd::stringname)const{autoitprototypes_.find(name);if(it!prototypes_.end()){returnit-second-clone();// 从模板克隆}returnnullptr;}private:std::unordered_mapstd::string,std::unique_ptrShapeprototypes_;};intmain(){// 基本用法克隆对象 autooriginalstd::make_uniqueCircle(5.0);original-setColor(red);autocopyoriginal-clone();// 复制不需要知道具体类型std::cout原件;original-draw();std::cout副本;copy-draw();// 修改副本不影响原件copy-setColor(blue);std::cout改色后;copy-draw();std::cout原件;original-draw();// 还是 redstd::cout\n 使用注册表 \n;// 注册表用法预存模板 ShapeRegistry registry;autocircleTemplatestd::make_uniqueCircle(10.0);circleTemplate-setColor(green);registry.addPrototype(big-green-circle,std::move(circleTemplate));autorectTemplatestd::make_uniqueRectangle(100,50);rectTemplate-setColor(yellow);registry.addPrototype(banner,std::move(rectTemplate));// 从模板快速创建autos1registry.create(big-green-circle);autos2registry.create(banner);s1-draw();s2-draw();}输出原件○ Circle(r5, colorred) 副本○ Circle(r5, colorred) 改色后○ Circle(r5, colorblue) 原件○ Circle(r5, colorred) 使用注册表 ○ Circle(r10, colorgreen) □ Rectangle(100×50, coloryellow)深拷贝 vs 浅拷贝这是原型模式最容易翻车的地方。C 的默认拷贝构造是逐成员拷贝memberwise copy。对于值类型int、std::string这没问题。但如果成员是指针classDocument{std::string title_;std::vectorPage*pages_;// 裸指针// 默认拷贝构造 → 浅拷贝// 新 Document 的 pages_ 和原来的指向同一批 Page 对象// 改一个另一个也变了。析构时还会 double-free。};解决方案如果成员涉及堆上资源必须手写拷贝构造做深拷贝。用智能指针可以让意图更明确classDocument{std::string title_;std::vectorstd::unique_ptrPagepages_;// 手写拷贝构造 —— 深拷贝每一个 PageDocument(constDocumentother):title_(other.title_){pages_.reserve(other.pages_.size());for(constautopage:other.pages_){pages_.push_back(page-clone());// 递归克隆}}// 注意声明了拷贝构造后编译器不再隐式生成移动构造。// 如果需要移动语义要显式写 defaultDocument(Document)default;Documentoperator(Document)default;// 拷贝赋值也得跟着写Rule of FiveDocumentoperator(constDocumentother){if(this!other){title_other.title_;pages_.clear();pages_.reserve(other.pages_.size());for(constautopage:other.pages_){pages_.push_back(page-clone());}}return*this;}};这里体现了 C 的Rule of Five如果你手写了析构函数、拷贝构造、拷贝赋值、移动构造、移动赋值中的任何一个通常五个都需要考虑。原型模式和这条规则天然有关联——一旦你要clone()就必须认真对待拷贝语义。什么时候用✅ 适合❌ 别用创建成本高需要读数据库/文件/网络创建成本低new 一下就行需要克隆运行时才知道类型的对象编译期就知道类型对象有很多配置想保存模板对象很简单没有复杂状态想避免大量子类避免工厂爆炸对象没有拷贝语义防混淆Prototype vs Factory MethodPrototypeFactory Method创建方式复制已有对象从头 new 新对象需要子类不需要工厂子类需要为每种产品写工厂子类灵活度运行时配置模板编译期确定类型类比复印工厂流水线一句话分清Prototype 是照着抄Factory 是从头造。Prototype vs 拷贝构造Prototype (clone)直接拷贝构造多态✅ 不需要知道具体类型❌ 必须知道具体类型返回类型基类指针/unique_ptr具体类型适用场景通过接口操作时知道具体类时clone()本质是对拷贝构造的多态封装。现代 C 小贴士C 有天然的原型支持——拷贝构造函数。但问题是拷贝构造不支持多态所以需要clone()来包装。如果你用 C20 的 Concepts可以约束 clone 接口templatetypenameTconceptCloneablerequires(constTt){{t.clone()}-std::convertible_tostd::unique_ptrShape;};// 说明这里约束的是 convertible_tounique_ptrShape 而非 unique_ptrT。// 原因C 的 unique_ptr 不支持协变返回类型covariant return types——// clone() 在类层次结构中只能返回 unique_ptrShape基类无法返回 unique_ptrCircle派生类。// 若写成 convertible_tounique_ptrTCloneableCircle 会失败// unique_ptrShape 无法隐式转换为 unique_ptrCircle。// 如需编译期强类型克隆可结合 CRTP 实现超出本章范围了解即可。templateCloneable Tstd::unique_ptrShapeduplicateAndModify(constToriginal){autocopyoriginal.clone();// 返回 unique_ptrShape// ... 修改 copyreturncopy;}五大创建型模式总结学完了创建型五兄弟来个总结模式一句话核心手段Singleton只要一个静态实例 私有构造Factory Method子类决定造什么继承 重写Abstract Factory造一整套配套的工厂接口 产品族Builder一步步配出来链式调用 分步构建Prototype照着现有的抄clone() 拷贝构造

更多文章