C++设计模式之结构型模式:享元模式(Flyweight)
享元模式(Flyweight)是结构型设计模式的一种,它通过共享细粒度对象来减少内存占用和提高性能,特别适合处理大量相似对象的场景(如文字处理中的字符、游戏中的粒子效果)。
一、核心思想与角色
享元模式的核心是“区分内部状态与外部状态,共享内部状态”,从而减少对象数量。其核心角色如下:
角色名称 | 核心职责 |
---|---|
抽象享元(Flyweight) | 定义享元对象的接口,声明接收外部状态的方法。 |
具体享元(ConcreteFlyweight) | 实现抽象享元接口,存储内部状态(可共享),不存储外部状态(需从客户端传入)。 |
非享元(UnsharedConcreteFlyweight) | 不参与共享的享元子类,通常包含不能共享的外部状态。 |
享元工厂(FlyweightFactory) | 管理享元对象池,负责创建和复用享元对象,确保相同内部状态的对象只存在一个实例。 |
客户端(Client) | 维护享元对象的外部状态,通过工厂获取享元对象并使用。 |
核心思想:将对象的状态分为可共享的内部状态(如字符的字体、大小)和不可共享的外部状态(如字符的位置),通过共享内部状态减少对象数量,外部状态由客户端在使用时传入。
二、实现示例(文字处理系统)
假设我们需要实现一个文字处理系统,文档中包含大量字符(如字母、数字),相同字符(如多个’A’)具有相同的字体和大小(内部状态),但位置不同(外部状态)。使用享元模式可大幅减少字符对象数量:
#include <iostream>
#include <string>
#include <map>
#include <vector>// 外部状态:字符位置(不共享)
struct Position {int x; // 横坐标int y; // 纵坐标Position(int x_, int y_) : x(x_), y(y_) {}
};// 1. 抽象享元:字符
class CharacterFlyweight {
public:// 纯虚方法:显示字符(需要外部状态position)virtual void display(const Position& position) const = 0;virtual ~CharacterFlyweight() = default;
};// 2. 具体享元:具体字符(如'A'、'B')
class ConcreteCharacter : public CharacterFlyweight {
private:char symbol; // 字符符号(内部状态)std::string font; // 字体(内部状态)int size; // 大小(内部状态)public:// 构造函数:初始化内部状态(可共享)ConcreteCharacter(char s, const std::string& f, int sz): symbol(s), font(f), size(sz) {}// 显示字符:结合外部状态(位置)void display(const Position& position) const override {std::cout << "字符 '" << symbol << "'(字体:" << font << ",大小:" << size << ")位于 (" << position.x << "," << position.y << ")" << std::endl;}
};// 3. 享元工厂:字符工厂
class CharacterFactory {
private:// 享元池:存储共享的字符对象(key为"符号+字体+大小"的组合)std::map<std::string, CharacterFlyweight*> flyweights;public:// 获取享元对象:存在则复用,不存在则创建CharacterFlyweight* getCharacter(char symbol, const std::string& font, int size) {// 生成唯一键(内部状态组合)std::string key = std::string(1, symbol) + "|" + font + "|" + std::to_string(size);// 检查池中是否存在if (flyweights.find(key) == flyweights.end()) {// 不存在则创建新对象并加入池flyweights[key] = new ConcreteCharacter(symbol, font, size);std::cout << "创建新享元:" << key << std::endl;} else {std::cout << "复用享元:" << key << std::endl;}return flyweights[key];}// 析构函数:释放所有享元对象~CharacterFactory() {for (auto& pair : flyweights) {delete pair.second;}}
};// 客户端代码:文档处理
int main() {// 创建享元工厂CharacterFactory factory;// 文档内容:多个相同字符(共享内部状态)std::vector<std::pair<CharacterFlyweight*, Position>> document;// 添加字符到文档(相同"符号+字体+大小"会复用享元)document.emplace_back(factory.getCharacter('A', "Arial", 12), Position(10, 20));document.emplace_back(factory.getCharacter('A', "Arial", 12), // 复用上面的'A'Position(30, 20));document.emplace_back(factory.getCharacter('B', "Arial", 12), // 新享元Position(50, 20));document.emplace_back(factory.getCharacter('A', "Times", 14), // 新享元(字体和大小不同)Position(10, 40));document.emplace_back(factory.getCharacter('A', "Times", 14), // 复用上面的'A'Position(30, 40));// 显示文档中所有字符(结合外部状态)std::cout << "\n=== 文档内容 ===" << std::endl;for (const auto& item : document) {item.first->display(item.second);}return 0;
}
三、代码解析
-
内部状态与外部状态:
- 内部状态:
ConcreteCharacter
中的symbol
(字符)、font
(字体)、size
(大小),这些属性相同的字符可以共享。 - 外部状态:
Position
中的x
和y
(位置),每个字符的位置不同,无法共享,由客户端在使用时传入。
- 内部状态:
-
抽象享元(CharacterFlyweight):
定义了display()
方法,参数为外部状态Position
,确保所有享元对象都能接收外部状态。 -
具体享元(ConcreteCharacter):
存储内部状态,实现display()
方法,将内部状态与传入的外部状态结合使用(显示字符及其位置)。 -
享元工厂(CharacterFactory):
- 维护一个
flyweights
池(map
容器),键为内部状态的组合(符号|字体|大小
),值为对应的享元对象。 getCharacter()
方法:当请求的字符已存在时复用,不存在时创建新对象并加入池,确保相同内部状态的对象只存在一个。
- 维护一个
-
客户端使用:
客户端通过工厂获取享元对象,维护外部状态(位置),并调用display()
方法时传入外部状态,实现字符的显示。
四、核心优势与适用场景
优势
- 减少内存占用:通过共享相同内部状态的对象,大幅减少系统中的对象数量(如示例中5个字符实际只创建3个享元对象)。
- 提高性能:减少对象创建和销毁的开销,尤其适合大量相似对象的场景。
- 分离状态:明确区分内部状态(可共享)和外部状态(不可共享),使系统设计更清晰。
适用场景
- 存在大量相似对象:如文字处理中的字符、游戏中的粒子(火焰、雨滴)、电商系统中的商品规格。
- 对象的大部分状态可共享:内部状态占比高,外部状态占比低,共享收益明显。
- 需要缓存对象复用:通过工厂池管理对象,避免重复创建。
五、与其他模式的区别
模式 | 核心差异点 |
---|---|
享元模式 | 共享相似对象的内部状态,减少内存占用,关注“对象复用”。 |
单例模式 | 确保一个类只有一个实例,关注“唯一性”,不涉及状态共享。 |
原型模式 | 通过克隆创建对象,关注“快速复制”,不强调状态共享。 |
工厂模式 | 负责对象创建,不涉及对象复用,享元模式中的工厂是特殊的“缓存工厂”。 |
六、实践建议
- 明确状态划分:清晰区分内部状态(不变、可共享)和外部状态(可变、不可共享),这是享元模式的关键。
- 工厂池设计:享元工厂应高效管理对象池(如用
hash map
存储),确保快速查找和复用。 - 外部状态处理:外部状态由客户端管理,避免享元对象存储外部状态导致无法共享。
- 线程安全:多线程环境下,需为享元工厂的
get
方法添加同步机制,避免并发创建重复对象。
享元模式的核心价值在于“通过共享减少对象数量”,当系统中存在大量相似对象且内存占用过高时,它能显著优化资源使用。其关键是合理划分内部状态和外部状态,通过工厂实现对象复用,是处理“大量细粒度对象”场景的最佳设计模式。