一、概述
-
目的:该模式主要的设计目的是为了迎合系统大量相似数据的应用而生,减少用于创建和操作相似的细碎对象所花费的成本。大量的对象会消耗高内存,享元模式给出了一个解决方案,即通过共享对象来减少内存负载。
-
作用:运用共享技术来有効地支持大量细粒度对象的复用。它通过共享已经存在的对象来大幅度减少需要创建的对象数量、避免大量相似类的开销,从而提高系统资源的利用率。对于 C++来说就是共用一个内存块,对象指针指向同一个地方)
例如:享元模式可以看成是一个工具箱,而享元对象就是工具箱内的具体的工具,我们在使用工具的时候,不必每回临时的制造工具,而是直接从工具箱里找到工具进行使用,这样就大大节约了制造工具的成本时间和工具占用的空间。
享元模式应用最多就是池技术,String常量池、数据库连接池、缓冲池等等都是享元模式的应用,所以说享元模式是池技术的重要实现方式。
二、两种状态
- 内蕴状态:是对象本身的属性,在生成对象以后一般不会进行改变,比如工具中的属性:名字、大小、重量等,还有就是我们一般需要一个关键性的属性作为其区别于其他对象的key,如工具的话我们可以把名称作为找到工具的唯一标识。
- 外蕴状态:是对象的外部描述,是每个对象的可变部分,比如对工具的使用地点、使用时间、使用人、工作内容的描述,这些属性不属于对象本身,而是根据每回使用情况进行变化的,这就需要制作成接口进行外部调用,而外蕴状态的维护是由调用者维护的,对象内不进行维护。
三、结构
- 抽象享元角色(Flyweight):是所有的具体享元类的基类,为具体享元规范需要实现的公共接口,非享元的外部状态以参数的形式通过方法传入;
- 具体享元角色(Concrete Flyweight):实现抽象享元角色中所规定的接口,必须是可共享的,需要封装享元对象的内部状态;
- 非享元角色(Unshared ConcreteFlyweight):是不可共享的外部状态,它以参数的形式注入具体享元的相关方法中;
- 享元工厂角色(Flyweigh tFactory):负责创建和管理享元角色。当客户对象请求一个享元对象时,享元工厂检査系统中是否存在符合要求的享元对象,如果存在则提供给客户;如果不存在的话,则创建一个新的享元对象。
享元工厂是享元模式的核心,它需要确保系统可以共享相同的对象。一般情况下,享元工厂会维护一个对象列表,当任何组件尝试获取享元类时,如果请求的享元类已经被创建,则直接返回已有的享元类;若没有,则创建一个新的享元对象,并将它加入到维护队列中。
四、实例
#include<iostream>
#include<string>
#include <map>
#include <vector>
#include <mutex>
using namespace std;
/*
享元类包含了树类型的部分状态, 这些成员变量保存的数值对于特定树而言是唯一的。
很多树木之间包含共同的名字、颜色和纹理, 如果在每棵树中都存储这些数据就会浪费大量内存。
因此我们将这些「内在状态」导出到一个单独的对象中, 然后让众多的单个树对象去引用它。
*/
class TreeType
{
private:
string name_;
string color_;
string texture_;
public:
TreeType(string n,string c,string t):name_(n),color_(c),texture_(t){}
void draw(string cavas, double x, double y) {
// 1. 创建特定类型、颜色和纹理的位图
// 2. 在画布坐标(x,y)处绘制位图
return;
}
};
/*
情景对象包含树类型的「外在状态」, 程序中可以创建数十亿个此类对象
因为它们体积很小: 仅有两个浮点坐标类型和一个引用成员变量
*/
class Tree
{
private:
double x_;
double y_;
TreeType* type_;
public:
Tree(double x,double y,TreeType *t):x_(x),y_(y),type_(t){}
void draw(string canvas)
{
return type_->draw(canvas, x_, y_);
}
};
/*
享元工厂: 决定是否复用已有享元或者创建一个新的对象, 同时它也是一个单例模式
*/
class TreeFactory {
private:
TreeFactory(){}
static TreeFactory* instance_;
static mutex mutex_;
共享池, 其中key格式为name_color_texture
map<string, TreeType*>tree_types_;
public:
static TreeFactory* getInstance() {
if (instance_ == nullptr)
{
mutex_.lock();
if (instance_ == nullptr) {
instance_ = new TreeFactory();
}
mutex_.unlock();
}
return instance_;
}
TreeType* getTreeType(string name, string color, string texture) {
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;
}
}
};
//Forest包含数量及其庞大的Tree
class Forest {
private:
vector<Tree> trees_;
public:
void planTree(double x, double y, string name, string color, string texture) {
TreeType* type = TreeFactory::getInstance()->getTreeType(name, color, texture);
Tree tree = Tree(x, y, type);
trees_.push_back(tree);
}
void draw() {
//将 tree_容器中的每一个元素从前往后枚举出来,并用tree来表示,
for(auto tree:trees_)
{
tree.draw("canvas");
}
}
};
int main()
{
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;
//树类型
forest->planTree(x, y, "榕树", "绿色", "");
forest->planTree(x, y, "杉树", "红色", "");
forest->planTree(x, y, "桦树", "白色", "");
}
}
forest->draw();
delete forest;
return 0;
}
五、优缺点
优点:
- 极大减少内存中对象的数量,使得相同对象或相似对象在内存中只保存一份;
- 享元对象的外部状态相对独立,而且不会影响其内部状态,从而使得享元对象可以在不同的环境中被共享。
- 项目3
缺点:
-
享元模式使得系统更加复杂,需要分离出内部状态和外部状态,从而使得程序的逻辑复杂化
-
为了使对象可以共享,享元模式需要将享元对象的状态外部化,而读取外部状态使得运行时间变长。
六、应用场景
-
当一个系统有大量相同或相似的对象,由于这些对象的大量使用,造成内存的大量耗费;使用享元模式可以节约内存空间,提高系统的性能。
-
对象的大部分状态都可以外部化,可以将这些外部状态传入对象中。
-
由于享元模式需要额外维护一个保存享元的数据结构,所以应当在有足够多的享元实例时才值得使用享元模式。
七、对比
享元模式与单例模式的区别
- 享元设计模式是一个类有很多对象,而单例是一个类仅一个对象。
- 享元模式是为了节约内存空间,提升程序性能,而单例模式则主要是出于共享状态的目的。
享元模式和工厂模式、单例模式
- 在区分出不同种类的外部状态后,创建新对象时需要选择不同种类的共享对象,这时就可以使用工厂模式来提供共享对象。
- 在共享对象的维护上,经常会采用单例模式来提供单实例的共享对象。