基于C++的《Head First设计模式》笔记——原型模式

目录

一.专栏介绍

二.一般的拷贝场景

三.拷贝优于构造

四.生成类型未知的对象

五.原型模式的概念,案例与代码

六.原型模式的优点

七.原型模式的缺点

八.原型模式的用途

[九.原型模式 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、网络、计算)
  • 需要多态复制(基类指针指向子类)
  • 想快速复制一个 "一模一样" 的对象
  • 大量重复创建同类对象
相关推荐
故事和你912 小时前
洛谷-入门4-数组3
开发语言·数据结构·c++·算法·动态规划·图论
猹叉叉(学习版)2 小时前
【系统分析师_知识点整理】 8.项目管理
笔记·项目管理·软考·系统分析师
玉树临风ives2 小时前
atcoder ABC 451 题解
c++·算法·atcoder
hssfscv2 小时前
软件设计师 试题三 面向对象——UML事物、关系、图
笔记·学习·uml
南境十里·墨染春水2 小时前
C++传记 详解单例模式(面向对象)
开发语言·c++·单例模式
扶摇接北海1762 小时前
洛谷:B4488 [语言月赛 202602] 甜品食用
数据结构·c++·算法
cui_ruicheng2 小时前
C++智能指针:从 RAII 到 shared_ptr 源码实现
开发语言·c++
xuhaoyu_cpp_java2 小时前
XML学习
xml·java·笔记·学习
共享家95272 小时前
实现简化的高性能并发内存池
开发语言·数据结构·c++·后端