C++设计模式结构型模式———享元模式

文章目录

一、引言

享元模式Flyweight)是一种结构型设计模式, 它摒弃了在每个对象中保存所有数据的方式, 通过共享多个对象所共有的相同状态, 让你能在有限的内存容量中载入更多对象。是为了解决面向对象程序设计的性能问题。

所谓享元,就是共享的单元或被共享的对象。其英文名Flyweight,轻量级的意思。这个设计模式就说为了,当我们在需要某个对象的时候,尽量公用以及创建出来的同类对象,从而避免频繁使用new创建同类或相似的对象;在同类对象数量已经很多的情况下,可以达到节省内存占用以及提升程序运行效率的目的。


二、享元模式

从一个典型的范例也就是围棋范例开始特别合适。围棋是一种策略类型的双人棋类游戏。除了棋手,棋盘,只有黑白棋子两个对象。我们先来一个棋子类:

c++ 复制代码
enum class Color {
	Black,
	White
};
struct Position {
	int m_x;
	int m_y;
	Position(int x, int y) :m_x(x), m_y(y) {}
	Position(std::initializer_list<int> init) {
		// 初始化列表中的元素数量必须正确
		if (init.size() != 2) {
			throw std::invalid_argument("Position requires exactly two integers");
		}
		// 获取迭代器
		auto it = init.begin();
		m_x = *it++;
		m_y = *it;
	}
};
class Piece
{
public:
	Piece(Color col, const Position& p) :m_color(col), m_pos(p)
	{}
	void draw()
	{
		if (m_color == Color::Black)
		{
			cout << "在位置:(" << m_pos.m_x << "," << m_pos.m_y << ")处绘制了一个黑色棋子!" << endl;
		}
		else
		{
			cout << "在位置:(" << m_pos.m_x << "," << m_pos.m_y << ")处绘制了一个白色棋子!" << endl;
		}
	}
private:
	Color m_color;
	Position m_pos;
};

下面我们在创建棋子并落位时,需要创建多个对象。

随着棋局的不断进行,不难想象,每落下一颗棋子,就要创建一个Piece对象,程序中将会创建越来越多的Piece对象,而这些Piece对象之间,除了颜色和显示位置不同之外,其他并没有什么不同。这样看起来就没有必要创建这么多的Piece对象,如果只创建一个代表黑色棋子的Piece对象和一个代表白色棋子的Piece对象,那么在绘制棋子的时候,只需要借用(被共享)这两个对象并向其中传递代表棋子的位置信息,这样就可以只用创建两个对象的成本来取代创建越来越多的Piece对象,这就是享元模式的设计思想,代表黑色棋子的Piece对象和代表白色棋子的Piece对象被称为享元对象(被共享的对象)。

通过享元模式,可以对上述代码进行改造。在享元模式中,首先创建一个代表棋子的新抽象类Piece(对刚刚的Piece类改造,去掉其中的位置信息,因为位置信息是可变的,不适合放在被共享的对象中),代码如下:

c++ 复制代码
class Piece	//棋子抽象类
{
public:
	Piece(Color col) :m_color(col)
	{}
	virtual void draw(const Position& pos) = 0;
	virtual	~Piece() {}
private:
	Color m_color;
};

class BlackPiece :public Piece
{
public:  
	BlackPiece() : Piece(Color::Black) {}
	  void draw(const Position& pos)override {
		  cout << this << ":在位置:(" << pos.m_x << "," << pos.m_y << ")处绘制了一个黑色棋子!" << endl;
	  }
};
class WhitePiece :public Piece
{
public:  
	WhitePiece() : Piece(Color::Black) {}
	  void draw(const Position& pos)override {
		  cout <<this << ":在位置:(" << pos.m_x << "," << pos.m_y << ")处绘制了一个白色棋子!" << endl;
	  }
};

然后,引入一个工厂类,该工厂类负责创建并返回黑色和白色棋子对象:

c++ 复制代码
class PieceFactory//创建棋子的工厂
{
public:
	~PieceFactory() {}
	std::shared_ptr<Piece> getFlyWeight(Color col) {
		// 检查缓存中是否已经存在对应颜色的棋子
		auto it = m_flyweightMap.find(col);
		if (it != m_flyweightMap.end()) {
			return it->second; // 返回已存在的棋子
		}

		// 创建新棋子并存入缓存
		std::shared_ptr<Piece> piece;
		if (col == Color::Black) {
			piece = std::make_shared<BlackPiece>();
		}
		else if (col == Color::White) {
			piece = std::make_shared<WhitePiece>();
		}

		// 存入映射表
		m_flyweightMap[col] = piece;
		return piece;
	}
private:
	map<Color, std::shared_ptr<Piece>> m_flyweightMap; // 使用智能指针
};

我们调用试一下:

c++ 复制代码
PieceFactory factory;

auto blackPiece = factory.getFlyWeight(Color::Black);
blackPiece->draw(Position(1, 2));

auto whitePiece = factory.getFlyWeight(Color::White);
whitePiece->draw(Position(3, 4));

// 再次获取黑色棋子,应该返回相同的对象
auto anotherBlackPiece = factory.getFlyWeight(Color::Black);
anotherBlackPiece->draw(Position(8, 8));

/*
01351284:在位置:(1,2)处绘制了一个黑色棋子!
0134CACC:在位置:(3,4)处绘制了一个白色棋子!
01351284:在位置:(8,8)处绘制了一个黑色棋子!
*/

这个享元模式的范例的核心的代码是pieceFactory类中的getFlyWeight成员函数代码。当在main主函数中第一次调用该成员函数时,该成员函数会根据传递进来的棋子枚举值参数来创建相应的棋子对象(黑色棋子对象或白色棋子对象),并将棋子对象(享元对象)保存到map容器。因此blackPieceanotherBlackPiece的地址是相同的。

可以看到,在一盘棋局中,只创建了两个棋子对象并将这两个棋子对象作为共享对象,通过给它们传递不同的位置信息以便在不同的位置绘制棋子。相比于第一种写法(每落一个棋子都要创建一个棋子对象),可以节省大量因创建多个类似(仅仅是位置或颜色不同)的棋子对象导致的对内存的不必要的消耗,这就是利用享元模式的好处。

享元模式结构

享元模式一般包含三个角色:

  • 抽象享元类Flyweight ):通常是一个接口或者抽象类。在该类中声明各种享元类的方法,在子类中实现这些方法,外部状态可以作为参数传递到这些方法中来。这里的抽象享元类指Piece类,而方法是指draw方法,外部状态(棋子的位置信息)通过draw方法的形参即可传递进来。
  • 具体享元类ConcreteFlyweight ):抽象享元类的子类,用这些类创建的对象就是享元对象,有时候也可以考虑以单件类实现享元对象。这里指BlackPieceWhitePiece类。
  • 享元工厂类FlyweightFactory ):用于创建并管理享元对象,在该类中存在一个享元池(一般使用map这种存储键值对的容器来实现),享元对象会放人其中。当用户请求一个享元对象时,该工厂返回一个已经创建的享元对象或者如果用户请求的享元对象不存在,则新创建一个并将该享元对象放人享元池,然后将该享元对象返回给请求者。其实享元工厂对象也可以考虑以单件的形式实现。这里指PieceFactory类。

引入享元模式的定义 :运用共享技术有效地支持大量细粒度的对象(的复用)。这意味着只使用少量的对象并复用它们就能够达到使用大量相似对象同样的效果。享元模式中一般包含了对简单工厂模式的使用,该工厂(pieceFactory类)一般用于创建享元对象并把这些享元对象保存在一个容器(map容器)中,这个容器也称为享元池(专门保存一个或多个享元对象),其中保存了一个白棋对象和一个黑棋对象。


三、总结

享元对象最重要的就是对"内部状态"和"外部状态"做出了明确的区分,这是享元对象能够被共享的关键所在。

  • 内部状态 :存储在享元对象内部的,一直不会发生改变的状态,这种状态可以被共享。例如,对于BlackPiece类对象,它的内部状态就是黑棋子,它一直代表黑棋子,不会发生改变。而对于WhitePiece类对象,它的内部状态就是白棋子,它一直代表白棋子,不会发生改变。内部状态一般可以作为享元类的成员变量而存在。
  • 外部状态 :随着着外部环境和各种动作因素的改变而发生改变的状态,这种状态不可以被共享。例如,黑棋子或者白棋子的位置信息就属于外部状态。当一个享元对象被创建之后,这种外部状态可以在需要的时候传到享元对象内部,例如,在需要绘制棋子的时候,通过调用BlackPieceWhitePiece类的draw成员函数并向该成员函数传递位置信息作为参数以达到在不同位置绘制棋子的目的。

这样一来,将内部状态相同的对象保存在享元池中以备随时取出以共享(复用)。当将共享的对象取出后,为其传递不同的外部状态,从而达到生成多个相似对象的效果,而实际上这些相似的对象在内存中只保存一份。在确定享元类的内部状态时,要仔细思考,只将真正需要在多个地方共享的成员变量作为享元对象的内部状态。

千万需要注意的是,不要将享元模式与对象池、连接池、线程池等混为一谈,虽然这几种技术都可以看成是对象的复用,但对象池、连接池、线程池等技术中的"复用"和享元模式中的"复用"并不相同。采用各种池技术时所讲的复用主要目的是节省时间和提高效率,例如,使用完的对象、连接、线程放人到池中而不是通过delete等释放掉,下次需要创建新对象、连接或线程时可以直接从池中取出来再次使用而不是使用诸如new等重新创建,但在每一时刻,池中的每个对象、连接、线程都会被一个使用者独占而不会在多处使用,当使用完毕后,再次放回到池中,其他使用者才可以取出来重复使用。而享元模式中的复用指的是享元对象在存在期间被所有使用者共享使用,从而达到节省内存空间的目的。

如果程序中有很多相似对象, 那么将可以节省大量内存。但是可能需要牺牲执行速度来换取内存, 因为他人每次调用享元方法时都需要重新计算部分情景数据。

  • 可以使用享元模式实现组合模式树的共享叶节点以节省内存。
  • 享元展示了如何生成大量的小型对象, 外观模式则展示了如何用一个对象来代表整个子系统。
  • 如果能将对象的所有共享状态简化为一个享元对象, 那么享元就和单例模式类似了。 但这两个模式有两个根本性的不同。
    1. 只会有一个单例实体, 但是享元类可以有多个实体, 各实体的内在状态也可以不同。
    2. 单例对象可以是可变的。 享元对象是不可变的。

总之,设计享元模式时,要重点考虑哪些是对象的内部状态,哪些是对象的外部状态,内部状态固定不变,外部状态可变,通过共享不变的部分达到享元模式的目的一一减少对象数量、节省内存、提高程序运行效率。

相关推荐
檀越剑指大厂10 分钟前
【Python系列】Python中的`any`函数:检查“至少有一个”条件满足
开发语言·python
Crazy learner13 分钟前
C 和 C++ 动态库的跨语言调用原理
c语言·c++
I_Am_Me_1 小时前
【JavaEE初阶】线程安全问题
开发语言·python
运维&陈同学1 小时前
【Elasticsearch05】企业级日志分析系统ELK之集群工作原理
运维·开发语言·后端·python·elasticsearch·自动化·jenkins·哈希算法
金士顿3 小时前
MFC 文档模板 每个文档模板需要实例化吧
c++·mfc
ZVAyIVqt0UFji4 小时前
go-zero负载均衡实现原理
运维·开发语言·后端·golang·负载均衡
loop lee4 小时前
Nginx - 负载均衡及其配置(Balance)
java·开发语言·github
SomeB1oody5 小时前
【Rust自学】4.1. 所有权:栈内存 vs. 堆内存
开发语言·后端·rust
toto4125 小时前
线程安全与线程不安全
java·开发语言·安全
水木流年追梦5 小时前
【python因果库实战10】为何需要因果分析
开发语言·python