目录
[九.原型模式 VS 工厂模式](#九.原型模式 VS 工厂模式)
一.专栏介绍
本专栏是我学习《head first》设计模式的笔记。这本书中是用Java语言为基础的,我将用C++语言重写一遍,并且详细讲述其中的设计模式,涉及是什么,为什么,怎么做,自己的心得等等。希望阅读者在读完我的这个专题后,也能在开发中灵活且正确的使用,或者在面对面试官时,能够自信地说自己熟悉常用设计模式。
本章将开始原型模式(Prototype Pattern)的学习。
二.一般的拷贝场景
在C++中,我们拷贝一个对象时直接调用拷贝构造,移动拷贝构造,赋值运算符重载或移动赋值运算符重载即可。下面是一个简单的Person类并且附带一个_name属性。代码如下:
cpp
#pragma once
#include <string>
using namespace std;
class Person
{
public:
Person(const char name[], size_t len) :
_name(new char[len + 1]), _len(len)
{
memcpy(_name, name, _len);
_name[_len] = '\0';
}
~Person()
{
delete[] _name;
}
// 拷贝构造
Person(const Person& person)
{
_len = person._len;
_name = new char[_len + 1];
memcpy(_name, person._name, _len);
}
// 赋值运算符重载
Person& operator=(const Person& person)
{
if (this == &person) return *this;
delete[] _name;
_len = person._len;
_name = new char[_len + 1];
memcpy(_name, person._name, _len);
return *this;
}
// 移动拷贝构造
Person(Person&& person) noexcept
{
_name = person._name;
_len = person._len;
person._name = nullptr;
person._len = 0;
}
// 移动赋值运算符重载
Person& operator=(Person&& person) noexcept
{
if (this == &person)
return *this;
delete[] _name;
_name = person._name;
_len = person._len;
person._name = nullptr;
person._len = 0;
return *this;
}
private:
char* _name;
size_t _len;
};
这是只有一个类的案例,不涉及继承多态,那么我们的拷贝构造,移动拷贝构造,赋值运算符重载和移动赋值运算符重载就够用了。实现它们是为了深拷贝,避免默认生成的四个拷贝函数是浅拷贝。
三.拷贝优于构造
在上面的例子中,拷贝Person的消耗和new Person的消耗是几乎一样的,因为Person类的构造函数只涉及了内存操作,构造函数和四个拷贝构造里有一样的内存申请操作。
那如果构造函数不只有内存操作呢,这才是大部分业务中构造函数的样子。也就是构造函数中还会包含磁盘io(比如读取Json文件),包含网络io,包含各种计算,这几样都是非常耗时的,相比内存操作可能直接慢了好几个数量级。
四个拷贝函数中就一般不涉及网络io,磁盘io,各种计算等等,就单纯是内存的申请和初始化。所以说拷贝优于构造 。这是原型模式的思想之一。
四.生成类型未知的对象
我们再说一个例子,这个例子就涉及到继承和多态。我们用基类指针去拷贝一个对象,我们甚至不知道我们拷贝出了哪一个具体的子类对象。
那我们为什么要用原型模式,而不是直接使用四个拷贝函数呢?
四个拷贝函数不支持多态。
我们验证的代码如下:
cpp
// 基类
class Animal
{};
// 子类
class Dog : public Animal {};
// 你用基类指针指向子类对象
Animal* animal = new Dog();
// 现在你想克隆它,用拷贝构造
Animal* clone = new Animal(*animal);
结果:得到的不是新 Dog,而是一个 Animal!对象切割(Object Slicing)发生了!子类信息全部丢失!
换句话说,四个拷贝函数甚至不能虚函数化:

那么,这时候原型模式就登场了。
五.原型模式的概念,案例与代码
用原型实例指定创建对象的种类,并且通过这些原型创建新的对象。
比如在一个游戏中,有各种技能:
-
火球术
-
冰冻术
-
闪电术
每个技能创建成本极高:
-
要加载特效
-
要读配置
-
要算伤害公式
-
要绑定音效
而这些技能被很多游戏角色所使用 ,只是可能技能的具体属性,比如伤害值不一样。所以我们希望可以选中一个技能的原型对象,然后复制它即可得到一样的新技能,最后改一下伤害值就可以了。(继续说的话,那就是还要将这个技能配置给一个角色,这里就是策略模式的思想。)
代码如下:
prototype.h:
cpp
#pragma once
#include <string>
#include <stdio.h>
#include <iostream>
using namespace std;
class Skill
{
public:
Skill(string name, size_t demage, string effect, string sound):
_name(name), _demage(demage), _effect(effect), _sound(sound)
{}
virtual ~Skill() = default;
// 命令模式基类中关键函数,多态实现构造对应的原型对象
virtual Skill* clone() = 0;
// 打印输出技能信息
void print()
{
printf("技能名:%s,伤害值:%zd,特效:%s,音效:%s\n",
_name.c_str(), _demage, _effect.c_str(), _sound.c_str());
}
// 不同游戏角色可能伤害值不一样,支持修改
void setDemage(size_t demage)
{
_demage = demage;
}
protected:
string _name; // 技能名字
size_t _demage; // 伤害值
string _effect; // 特效(加载成本高)
string _sound; // 音效(加载成本高)
};
// ------------------------------
// 具体技能1:火球术
// ------------------------------
class FireBall : public Skill
{
public:
FireBall() : Skill("火球术", 50, "火焰爆炸", "fire.wav")
{
cout << "[加载] 火球术创建完成(耗时:加载贴图+粒子+音效)\n";
}
// 克隆:直接调用拷贝构造(深拷贝)
Skill* clone() override
{
return new FireBall(*this);
}
// 拷贝构造(进行深拷贝)
FireBall(const FireBall& other) : Skill(other)
{
cout << "[克隆] 复制了一个火球术\n";
}
};
// ------------------------------
// 具体技能2:冰冻术
// ------------------------------
class IceBall : public Skill
{
public:
IceBall() : Skill("冰冻术", 40, "冰霜冻结", "ice.wav")
{
cout << "[加载] 冰冻术创建完成(耗时:加载贴图+粒子+音效)\n";
}
Skill* clone() override
{
return new IceBall(*this);
}
IceBall(const IceBall& other) : Skill(other)
{
cout << "[克隆] 复制了一个冰冻术\n";
}
};
main.cpp:
cpp
#include "prototype.h"
int main()
{
Skill* firePrototype = new FireBall(); // 火球术原型
Skill* icePrototype = new IceBall(); // 冰冻术原型
// 克隆一个火球术和两个冰冻术
Skill* fileSkill1 = firePrototype->clone();
fileSkill1->setDemage(88);
Skill* iceSkill1 = icePrototype->clone();
Skill* iceSkill2 = icePrototype->clone();
iceSkill2->setDemage(99);
cout << endl << "验证其中一个子类指针:" << endl;
if (dynamic_cast<FireBall*>(fileSkill1))
cout << "fileSkill1指向了子类对象,clone正确" << endl << endl;
else
cout << "clone不正确" << endl << endl;
// 打印输出
fileSkill1->print();
iceSkill1->print();
iceSkill2->print();
delete fileSkill1;
delete iceSkill1;
delete iceSkill2;
return 0;
}
运行输出:

总结一下:基类中有一个clone()这个纯虚函数,子类中实现它,clone()里是调用了子类自己的拷贝构造函数,返回一个构造出的新对象的指针。这样就完成了多态情形下的拷贝,相当于通过clone()来搭桥。
这里还有缓存的思想,火球术原型和冰球术原型就是一份缓存,它们在构造时可能会进行缓慢的磁盘io,网络io等来加载音频文件,特效文件到内存中,之后克隆的过程就没有这些io了,仅有内存的申请和初始化。
六.原型模式的优点
-
解决多态克隆痛点 :通过虚
clone()实现动态绑定,彻底避免拷贝构造的对象切片问题,完整保留子类类型与状态,支持面向接口的安全克隆。 -
大幅优化创建性能:复杂对象仅需 1 次完整初始化(IO / 计算 / 资源加载),后续克隆仅做内存拷贝,避免重复的高开销初始化,极速生成实例。
-
高内聚低耦合,易扩展 :调用者无需知晓对象构造细节、子类类型,仅通过统一
clone()接口即可生成实例;新增子类仅需实现clone(),完全符合开闭原则,扩展无侵入。
七.原型模式的缺点
- 对象的复制有时候相当复杂,四个拷贝函数的代码可能会比较难写。
八.原型模式的用途
- 在一个复杂的类层次中,当系统必须创建许多类型的新对象时,应该考虑原型。比如上面我们的游戏技能的例子,这个例子中所谓复杂的类层次,就是涉及到了继承多态。
九.原型模式 VS 工厂模式
-
工厂模式:从零创建对象(new)
-
原型模式:复制已有对象(clone)
用工厂模式:
- 对象轻量
- 构造简单
- 每次创建需要不同参数 / 不同初始化
- 需要统一管理创建逻辑
用原型模式:
- 对象构造昂贵(加载资源、IO、网络、计算)
- 需要多态复制(基类指针指向子类)
- 想快速复制一个 "一模一样" 的对象
- 大量重复创建同类对象