享元模式
介绍
所谓“享元”, 顾名思义就是被共享的单元. 享元模式的意图是复用对象, 节省内存, 前提是享元对象是不可变对象.
它使用共享的对象来有效地支持大量细粒度的对象. 具体来说,享元模式尽量减少内存使用,方法是共享相同数据的对象,避免重复创建相同的对象.
具体来讲, 当一个系统中存在大量重复对象的时候, 如果这些重复的对象是不可变对象, 我们就可以利用享元模式将对象设计成享元, 在内存中只保留一份实例, 供多处代码引用. 这样可以减少内存中对象的数量, 起到节省内存的目的.
定义
假设我们要开发一个国际象棋游戏,chessboard 上一共有32个棋子。如果对每个棋子都创建一个对象,那会产生大量相似的对象并浪费内存。
使用享元模式,可以把棋子分为两类状态:
内部状态:棋子的名称(king, queen, pawn 等),颜色(黑色或白色)。这部分对每种棋子只需要创建一次。
外部状态:棋子的当前位置。这需要每个棋子都单独保存。
那么我们可以创建一个 ChessPiece 类代表内部状态,里面有棋子的名称和颜色属性。然后创建一个 ChessPieceUnit 类代表每个棋子,里面包含一个 ChessPiece 引用和当前位置坐标。
在游戏中,每次需要一个棋子时,可以从 ChessPiece 缓存池中获得,避免重复创建内部状态相同的 ChessPiece,然后绑定其外部状态,如当前位置。
这样就可以大量节省内存,只需要创建很少的内部状态对象,而复用它们来代表具体的每个棋子。
共同部分 设定好就不再改变
// ChessPiece为棋子对象的基类(共同部分)
class ChessPiece {
public:
virtual ~ChessPiece() {}
virtual std::string render() = 0;
};
// 具体棋子类,继承自ChessPiece基类(共同部分)
class Queen : public ChessPiece {
public:
std::string render() override {
return "渲染Queen";
}
};
class King : public ChessPiece {
public:
std::string render() override {
return "渲染King" ;
}
};
不同部分
// 棋子对象(不同部分之一 - 棋子类型)
struct ChessPieceUnit {
ChessPiece* piece;
int positionX; // 棋子位置(不同部分之二)
int positionY;
void print() {
std::cout << piece->render() << " " << positionX << " " << positionY << std::endl;
}
};
// 享元工厂类,使用单例模式
class ChessPieceFactory {
public:
static ChessPieceFactory* getInstance() {
static ChessPieceFactory instance;
return &instance;
}
ChessPiece* getChessPiece(std::string type) {
if (!pieces.count(type)) {
if (type == "Queen")
pieces[type] = new Queen();
else if (type == "King")
pieces[type] = new King();
}
return pieces[type];
}
private:
std::unordered_map<std::string, ChessPiece*> pieces;
ChessPieceFactory() {}
};
调用
// 创建棋子对象
ChessPieceUnit queen1;
auto factory = ChessPieceFactory::getInstance();
queen1.piece = factory->getChessPiece("Queen");
queen1.positionX = 3;
queen1.positionY = 5;
ChessPieceUnit king1;
king1.piece = factory->getChessPiece("King");
king1.positionX = 4;
king1.positionY = 8;
// 渲染棋子
queen1.print();
king1.print();
// 修改位置
queen1.positionX = 2;
queen1.positionY = 8;
king1.positionX = 5;
king1.positionY = 2;
// 渲染棋子
queen1.print();
king1.print();
效果
./bin/design/flyWeight
渲染Queen 3 5
渲染King 4 8
渲染Queen 2 8
渲染King 5 2
说明:
- ChessPiece类中定义渲染接口是棋子的共同功能
- Queen和King类继承自ChessPiece,是具体棋子类型的不同之处
- ChessPieceUnit结构体中包含具体棋子对象和位置坐标,是每个棋子不同的状态
这样通过共享内存中相同的ChessPiece对象,避免重复创建,即实现了享元模式。
回顾
1.享元模式的原理
所谓“享元”,顾名思义就是被共享的单元。享元模式的意图是复用对象,节省内存,前提是享元对象是不可变对象。具体来讲,当一个系统中存在大量重复对象的时候,我们就可以利用享元模式,将对象设计成享元,在内存中只保留一份实例,供多处代码引用,这样可以减少内存中对象的数量,以起到节省内存的目的。实际上,不仅仅相同对象可以设计成享元,对于相似对象,我们也可以将这些对象中相同的部分(字段),提取出来设计成享元,让这些大量相似对象引用这些享元。
2.享元模式的实现
享元模式的代码实现非常简单,主要是通过工厂模式,在工厂类中,通过一个Map或者List来缓存已经创建好的享元对象,以达到复用的目的。
使用享元模式的好处:
- 减少内存占用,降低系统资源消耗
- 提高系统性能
- 适用于系统有大量相似对象的情况
其缺点是增加了系统的复杂度,需要区分内部状态和外部状态,这使得程序的逻辑复杂化。