一、模式介绍
定义
享元模式又称缓存、Cache、Flyweight,是一种结构型设计模式。享元模式摒弃了在每个对象中保存所有数据的方式, 运用共享技术,共享多个对象所共有的相同状态, 能在有限的内存容量中载入更多对象,以支持存在大量细粒度对象且内存不足的场景。
适用场景
- 仅在程序必须支持大量对象且没有足够的内存容量时使用享元模式。
- 程序需要生成数量巨大的相似对象
- 目标内存不足
- 对象中包含可抽取且能在多个对象间共享的重复状态。
二、问题及动机
在软件系统采用纯粹对象方案的问题在于大量细粒度的对象会很快充斥在系统中,从而带来很高的运行时代价——主要指内存需求方面的代价。如何在避免大量细粒度对象问题的同时,让外部客户程序仍然能够透明地使用面向对象的方式来进行操作?
如下以一款游戏开发作为例子说明该问题。该款游戏中会使用到大量的子弹、导弹、和爆炸弹片在整幅地图上穿梭,以提供更好的游戏体验。因为游戏中存在大量的粒子,每个粒子(子弹、导弹、弹片)都包含完整数据的独立对象。当游戏中大量新建并载入粒子时,如果电脑内存不足将引起程序奔溃。
三、情景示例
- 将特殊(外在可变)状态及重复(内在不可变)状态分离。
- 内在状态。对象的常量数据称为内在状态,其他对象只能读取但不能修改其数值,如游戏中每个粒子的颜色和精灵图。仅存储内在状态的对象称为享元。
- 外在状态。对象中能被其他对象修改的状态称为外在状态,如粒子的坐标、移动矢量和速度。
- 不在对象中存储外在状态,而是将其传递给依赖于它的一个特殊方法。程序只在对象中保存内在状态,以方便在不同情景下重用。如在游戏代码实现中,从粒子类抽出外在状态,使用三个不同的对象表示子弹、导弹、和弹片以表示游戏中的所有粒子。
- 创建独立的情景类来存储外在状态和对享元对象的引用。如在上述例子中,
可以通过创建多个数组成员变量存储每个粒子的坐标、方向矢量、速度大小,创建一个数组存储指向代表粒子的特定享元的引用,但该做法需要数组保持同步,才能够使用同一索引来获取关于某个粒子的所有数据。更好的方法是创建独立的情景类来存储外在状态和对享元对象的引用。 在该方法中, 容器类只需包含一个数组。
四、模式结构
- 享元模式只是一种优化。在应用该模式之前,需要确定程序中存在与大量类似对象同时占用内存相关的内存消耗问题,并且确保该问题无法使用其他更好的方式来解决.
- 享元(Flyweight) 类包含原始对象中部分能在多个对象中共享的状态。同一享元对象可在许多不同情景中使用。享元中存储的状态被称为 “内在状态”。传递给享元方法的状态被称为 “外在状态”。
- 情景(Context) 类包含原始对象中各不相同的外在状态。情景与享元对象组合在一起就能表示原始对象的全部状态。
- 通常情况下,原始对象的行为会保留在享元类中。因此调用享元方法必须提供部分外在状态作为参数。但也可将行为移动到情景类中,然后将连入的享元作为单纯的数据对象。
- 客户端(Client) 负责计算或存储享元的外在状态。在客户端看来,享元是一种可在运行时进行配置的模板对象,具体的配置方式为向其方法中传入一些情景数据参数。
- 享元工厂(Flyweight Factory) 会对已有享元的缓存池进行管理。有了工厂后,客户端就无需直接创建享元,它们只需调用工厂并向其传递目标享元的一些内在状态即可。工厂会根据参数在之前已创建的享元中进行查找,如果找到满足条件的享元就将其返回;如果没有找到就根据参数新建享元。
五、示例代码
/**
* 享元设计
*
* 目的: 通过共享多个对象之间的公共状态而不保存每个对象中的所有数据,以将更多的对象放入可用的RAM中
*/
struct SharedState
{
std::string brand_;
std::string model_;
std::string color_;
SharedState(const std::string &brand, const std::string &model, const std::string &color)
: brand_(brand), model_(model), color_(color)
{
}
friend std::ostream &operator<<(std::ostream &os, const SharedState &ss)
{
return os << "[ " << ss.brand_ << " , " << ss.model_ << " , " << ss.color_ << " ]";
}
};
struct UniqueState
{
std::string owner_;
std::string plates_;
UniqueState(const std::string &owner, const std::string &plates)
: owner_(owner), plates_(plates)
{
}
friend std::ostream &operator<<(std::ostream &os, const UniqueState &us)
{
return os << "[ " << us.owner_ << " , " << us.plates_ << " ]";
}
};
/**
* Flyweight存储属于多个实际业务实体的公共状态部分(也称为内在状态)。
* 通过成员函数的参数接受状态的其余部分(外部状态,每个实体都是惟一的)。
*/
class Flyweight
{
private:
SharedState *shared_state_;
public:
Flyweight(const SharedState *shared_state) : shared_state_(new SharedState(*shared_state))
{
}
Flyweight(const Flyweight &other) : shared_state_(new SharedState(*other.shared_state_))
{
}
~Flyweight()
{
delete shared_state_;
}
SharedState *shared_state() const
{
return shared_state_;
}
void Operation(const UniqueState &unique_state) const
{
std::cout << "Flyweight: Displaying shared (" << *shared_state_ << ") and unique (" << unique_state << ") state.\n";
}
};
/**
* Flyweight Factory创建和管理Flyweight对象,确保flyweight的用法和样例:当客户请求享元时,
* 工厂要么返回一个现有实例(如果有的话),要么创建一个新实例。
*/
class FlyweightFactory
{
/**
* @var Flyweight[]
*/
private:
std::unordered_map<std::string, Flyweight> flyweights_;
/**
* 返回对应状态的享元的键名。
*/
std::string GetKey(const SharedState &ss) const
{
return ss.brand_ + "_" + ss.model_ + "_" + ss.color_;
}
public:
FlyweightFactory(std::initializer_list<SharedState> share_states)
{
for (const SharedState &ss : share_states)
{
this->flyweights_.insert(std::make_pair<std::string, Flyweight>(this->GetKey(ss), Flyweight(&ss)));
}
}
/**
* 返回具有给定状态的Flyweight或创建一个新Flyweight。
*/
Flyweight GetFlyweight(const SharedState &shared_state)
{
std::string key = this->GetKey(shared_state);
if (this->flyweights_.find(key) == this->flyweights_.end())
{
std::cout << "FlyweightFactory: Can't find a flyweight, creating new one.\n";
this->flyweights_.insert(std::make_pair(key, Flyweight(&shared_state)));
}
else
{
std::cout << "FlyweightFactory: Reusing existing flyweight.\n";
}
return this->flyweights_.at(key);
}
void ListFlyweights() const
{
size_t count = this->flyweights_.size();
std::cout << "\nFlyweightFactory: I have " << count << " flyweights:\n";
for (std::pair<std::string, Flyweight> pair : this->flyweights_)
{
std::cout << pair.first << "\n";
}
}
};
// ...
void AddCarToPoliceDatabase(
FlyweightFactory &ff, const std::string &plates, const std::string &owner,
const std::string &brand, const std::string &model, const std::string &color)
{
std::cout << "\nClient: Adding a car to database.\n";
const Flyweight &flyweight = ff.GetFlyweight({brand, model, color});
// 客户端代码存储或计算外部状态并将其传递给享元对象的成员函数。
flyweight.Operation({owner, plates});
}
/**
* 客户端代码通常在应用程序的初始化阶段创建一堆预先填充的权重值。
*/
int main()
{
FlyweightFactory *factory = new FlyweightFactory({{"Chevrolet", "Camaro2018", "pink"}, {"Mercedes Benz", "C300", "black"}, {"Mercedes Benz", "C500", "red"}, {"BMW", "M5", "red"}, {"BMW", "X6", "white"}});
factory->ListFlyweights();
AddCarToPoliceDatabase(*factory,
"CL234IR",
"James Doe",
"BMW",
"M5",
"red");
AddCarToPoliceDatabase(*factory,
"CL234IR",
"James Doe",
"BMW",
"X1",
"red");
factory->ListFlyweights();
delete factory;
return 0;
}
六、优缺点
优点
- 当程序中有很多相似对象, 可以节省大量内存。
缺点
- 需要牺牲执行速度来换取内存, 因为每次调用享元方法时都需要重新计算部分情景数据。
- 代码会变得更加复杂。
七、模式关系
- 可以使用享元模式实现组合模式树的共享叶节点以节省内存。
- 享元展示了如何生成大量的小型对象, 外观模式则展示了如何用一个对象来代表整个子系统。
- 单例只有一个单例实体, 但是享元类可以有多个实体, 各实体的内在状态也可以不同。
- 单例对象可以是可变的。 享元对象是不可变的。