简介
享元模式(Flyweight Pattern)是一种结构型设计模式,它主要解决的问题是创建大量相似对象时的内存开销问题。该模式通过共享具有相同状态的对象来减少内存使用量。
享元模式的思想是:当需要创建一个新对象时,首先检查是否已经存在具有相同状态的对象。如果存在,则返回已经存在的对象,否则创建一个新的对象。因此,如果要创建多个具有相同状态的对象,可以重复使用相同的对象,从而减少内存开销。
通过运用共享技术来有效地支持大量细粒度对象的复用。它通过共享已经存在的对象来大幅度减少需要创建 的对象数量、避免大量相似对象的开销,从而提高系统资源的利用率。
享元模式结构
享元(Flyweight )模式中存在以下两种状态:
1. 内部状态,即不会随着环境的改变而改变的可共享部分。
2. 外部状态,指随环境改变而改变的不可以共享的部分。享元模式的实现要领就是区分应用中的这两 种状态,并将外部状态外部化。
享元模式的主要有以下角色:
享元模式只是一种优化。在应用该模式之前,你要确定程序中存在与大量类似对象同时占用内存相关的内存消耗问题,并且确保该问题无法使用其他更好的方式来解决。
**享元(Flyweight)**类包含原始对象中部分能在多个对象中共享的状态。同一享元对象可在许多不同情景中使用。享元中存储的状态被称为“内在状态”。传递给享元方法的状态被称为“外在状态”。
**情景(Context)**类包含原始对象中各不相同的外在状态。情景与享元对象组合在一起就能表示原始对象的全部状态。
通常情况下,原始对象的行为会保留在享元类中。因此调用享元方法必须提供部分外在状态作为参数。但你也可将行为移动到情景类中,然后将连入的享元作为单纯的数据对象。
**客户端(Client)**负责计算或存储享元的外在状态。在客户端看来,享元是一种可在运行时进行配置的模板对象,具体的配置方式为向其方法中传入一些情景数据参数。
**享元工厂(Flyweight Factory)**会对已有享元的缓存池进行管理。有了工厂后,客户端就无需直接创建享元,它们只需调用工厂并向其传递目标享元的一些内在状态即可。工厂会根据参数在之前已创建的享元中进行查找,如果找到满足条件的享元就将其返回;如果没有找到就根据参数新建享元。
代码实现
#ifndef FLYWEIGHT_H_
#define FLYWEIGHT_H_
#include <string>
// 享元类包含了树类型的部分状态, 这些成员变量保存的数值对于特定树而言是唯一的。
// 很多树木之间包含共同的名字、颜色和纹理, 如果在每棵树中都存储这些数据就会浪费大量内存。
// 因此我们将这些「内在状态」导出到一个单独的对象中, 然后让众多的单个树对象去引用它。
class TreeType {
public:
TreeType(std::string n, std::string c, std::string t) :
name_(n), color_(c), texture_(t) {}
void draw(std::string canvas, double x, double y) {
// 1. 创建特定类型、颜色和纹理的位图
// 2. 在画布坐标(x,y)处绘制位图
return;
}
private:
std::string name_;
std::string color_;
std::string texture_;
};
#endif // FLYWEIGHT_H_
#ifndef CONTEXT_H_
#define CONTEXT_H_
#include <string>
#include "Flyweight.h"
// 情景对象包含树类型的「外在状态」, 程序中可以创建数十亿个此类对象, 因为它们体积很小: 仅有两个浮点坐标类型和一个引用成员变量
class Tree {
public:
Tree(double x, double y, TreeType* t) : x_(x), y_(y), type_(t) {}
void draw(std::string canvas) {
return type_->draw(canvas, x_, y_);
}
private:
double x_;
double y_;
TreeType* type_;
};
#endif // CONTEXT_H_
#ifndef FLYWEIGHT_FACTORY_H_
#define FLYWEIGHT_FACTORY_H_
#include <map>
#include <string>
#include <mutex>
#include "Flyweight.h"
// 享元工厂: 决定是否复用已有享元或者创建一个新的对象, 同时它也是一个单例模式
class TreeFactory {
public:
static TreeFactory* getInstance() {
if (instance_ == nullptr) {
mutex_.lock();
if (instance_ == nullptr) {
instance_ = new TreeFactory();
}
mutex_.unlock();
}
return instance_;
}
TreeType* getTreeType(std::string name, std::string color, std::string texture) {
std::string key = name + "_" + color + "_" + texture;
auto iter = tree_types_.find(key);
if (iter == tree_types_.end()) {
// 新的tree type
TreeType* new_tree_type = new TreeType(name, color, texture);
tree_types_[key] = new_tree_type;
return new_tree_type;
} else {
// 已存在的tree type
return iter->second;
}
}
private:
TreeFactory() {}
static TreeFactory* instance_;
static std::mutex mutex_;
// 共享池, 其中key格式为name_color_texture
std::map<std::string, TreeType*> tree_types_;
};
#endif // FLYWEIGHT_FACTORY_H_
#include "FlyweightFactory.h"
TreeFactory* TreeFactory::instance_ = nullptr;
std::mutex TreeFactory::mutex_;
#ifndef CLIENT_H_
#define CLIENT_H_
#include <vector>
#include <iostream>
#include <string>
#include "FlyweightFactory.h"
#include "Context.h"
// Forest包含数量及其庞大的Tree
class Forest {
public:
void planTree(double x, double y, std::string name, std::string color, std::string texture) {
TreeType* type = TreeFactory::getInstance()->getTreeType(name, color, texture);
Tree tree = Tree(x, y, type);
trees_.push_back(tree);
}
void draw() {
for (auto tree : trees_) {
tree.draw("canvas");
}
}
private:
std::vector<Tree> trees_;
};
#endif // CLIENT_H_
#include "Client.h"
int main() {
system("chcp 65001");
Forest* forest = new Forest();
// 在forest中种植很多棵树
for (int i = 0; i < 500; i++) {
for (int j = 0; j < 500; j++) {
double x = i;
double y = j;
// 树类型1: 红色的杉树
forest->planTree(x, y, "杉树", "红色", "");
// 树类型2: 绿色的榕树
forest->planTree(x, y, "榕树", "绿色", "");
// 树类型3: 白色的桦树
forest->planTree(x, y, "桦树", "白色", "");
}
}
forest->draw();
system("pause");
delete forest;
}
优缺点
优点:
- 减少内存使用:由于享元模式共享对象,因此可以减少内存使用。
- 提高性能:创建和销毁对象会占用大量的CPU时间和内存空间。因此,使用享元模式可以提高性能。
- 代码简洁:享元模式可以使代码更简洁,因为该模式使用相同的对象来处理多个请求,而不需要创建大量的对象。
缺点:
- 共享对象可能会对线程安全造成问题。如果不正确地实现共享机制,则可能导致多个线程对同一对象进行更改,从而导致竞争条件。
- 需要牺牲一定的时间和空间,来实现对象共享和控制机制。这意味着,当对象之间没有重复性时,使用享元模式可能会导致额外的开销。
使用场景
- 当需要创建大量相似对象时可以使用享元模式,例如游戏中的道具、棋子等。
- 当对象需要被共享时,可以使用享元模式,例如线程池中的线程。
- 当系统的内存资源相对有限时可以考虑使用享元模式,以减少内存的使用。
- 当需要减少对象的创建次数、降低系统开销时可以使用享元模式。