享元模式C++
享元模式(Flyweight Pattern)是一种结构型设计模式,它通过共享技术有效地支持大量细粒度对象的复用,从而减少内存消耗和对象创建开销。这种模式特别适合处理大量相似对象,这些对象中包含可共享的"内部状态"和不可共享的"外部状态"。
享元模式的核心角色
- Flyweight(享元接口):定义享元对象的接口,接受并作用于外部状态
- ConcreteFlyweight(具体享元):实现享元接口,存储内部状态
- FlyweightFactory(享元工厂):管理享元对象,负责创建和复用享元
- Client(客户端):维护享元对象的外部状态,通过工厂获取享元
C++实现示例
以下以"文字处理软件"为例实现享元模式,文档中的字符具有可共享的内部状态(字符值、字体等)和不可共享的外部状态(位置、颜色等):
#include <iostream>
#include <string>
#include <unordered_map>
#include <memory>
#include <vector>// 享元接口:字符享元
class CharacterFlyweight {
public:virtual ~CharacterFlyweight() = default;// 绘制字符,接收外部状态virtual void draw(int x, int y, const std::string& color) const = 0;virtual char getSymbol() const = 0;
};// 具体享元:具体字符
class ConcreteCharacter : public CharacterFlyweight {
private:char symbol; // 字符值(内部状态,可共享)std::string font; // 字体(内部状态,可共享)int size; // 字号(内部状态,可共享)public:ConcreteCharacter(char sym, std::string fnt, int sz): symbol(sym), font(std::move(fnt)), size(sz) {}// 绘制字符,结合外部状态(位置和颜色)void draw(int x, int y, const std::string& color) const override {std::cout << "绘制字符 '" << symbol << "' 在位置 (" << x << "," << y << "),颜色: " << color << ",字体: " << font << ",字号: " << size << std::endl;}char getSymbol() const override {return symbol;}
};// 享元工厂:字符工厂
class CharacterFactory {
private:// 缓存享元对象,键为"字符-字体-字号"的组合std::unordered_map<std::string, std::shared_ptr<CharacterFlyweight>> flyweights;// 生成缓存键std::string getKey(char symbol, const std::string& font, int size) {return std::string(1, symbol) + "|" + font + "|" + std::to_string(size);}public:// 获取或创建享元对象std::shared_ptr<CharacterFlyweight> getCharacter(char symbol, const std::string& font, int size) {std::string key = getKey(symbol, font, size);// 如果不存在,则创建并缓存if (flyweights.find(key) == flyweights.end()) {flyweights[key] = std::make_shared<ConcreteCharacter>(symbol, font, size);std::cout << "创建新的享元对象: " << key << std::endl;} else {std::cout << "复用享元对象: " << key << std::endl;}return flyweights[key];}// 获取缓存的享元数量size_t getFlyweightCount() const {return flyweights.size();}
};// 字符上下文:包含外部状态
struct CharacterContext {std::shared_ptr<CharacterFlyweight> flyweight;int x; // 位置X(外部状态)int y; // 位置Y(外部状态)std::string color; // 颜色(外部状态)void draw() const {flyweight->draw(x, y, color);}
};// 文档类:使用享元的客户端
class Document {
private:CharacterFactory factory;std::vector<CharacterContext> characters;public:void addCharacter(char symbol, const std::string& font, int size,int x, int y, const std::string& color) {// 从工厂获取享元auto flyweight = factory.getCharacter(symbol, font, size);// 存储外部状态和享元引用characters.push_back({flyweight, x, y, color});}void render() const {std::cout << "\n开始渲染文档..." << std::endl;for (const auto& ctx : characters) {ctx.draw();}std::cout << "文档渲染完成" << std::endl;}void showStats() const {std::cout << "\n文档统计: " << std::endl;std::cout << "总字符数: " << characters.size() << std::endl;std::cout << "享元对象数: " << factory.getFlyweightCount() << std::endl;}
};int main() {Document doc;// 添加文档内容(大量重复字符)doc.addCharacter('H', "Arial", 12, 10, 20, "black");doc.addCharacter('e', "Arial", 12, 20, 20, "black");doc.addCharacter('l', "Arial", 12, 30, 20, "black");doc.addCharacter('l', "Arial", 12, 40, 20, "black");doc.addCharacter('o', "Arial", 12, 50, 20, "black");doc.addCharacter('W', "Arial", 12, 10, 40, "blue");doc.addCharacter('o', "Arial", 12, 20, 40, "blue");doc.addCharacter('r', "Arial", 12, 30, 40, "blue");doc.addCharacter('l', "Arial", 12, 40, 40, "blue");doc.addCharacter('d', "Arial", 12, 50, 40, "blue");doc.addCharacter('!', "Times New Roman", 14, 60, 30, "red");// 显示统计信息doc.showStats();// 渲染文档doc.render();return 0;
}
代码解析
-
状态划分:
- 内部状态:字符值(
symbol
)、字体(font
)、字号(size
),这些属性相同的字符可以共享 - 外部状态:位置(
x
、y
)、颜色(color
),这些属性因字符位置不同而变化,不可共享
- 内部状态:字符值(
-
核心组件:
ConcreteCharacter
:具体享元类,存储内部状态,实现draw()
方法时结合外部状态完成绘制CharacterFactory
:享元工厂,通过键值(字符-字体-字号组合)缓存和复用享元对象CharacterContext
:存储外部状态和对应的享元引用,将内部状态和外部状态结合使用
-
优化效果:文档中大量重复的字符(如示例中的多个’l’和’o’)只需要创建一个享元对象,显著减少了对象数量(示例中11个字符仅创建8个享元)。
享元模式的优缺点
优点:
- 大幅减少对象数量,降低内存消耗
- 提高系统性能,减少对象创建和销毁的开销
- 将内部状态与外部状态分离,便于管理和复用
缺点:
- 增加了系统复杂度,需要分离内部状态和外部状态
- 外部状态的管理可能带来额外开销
- 可能导致线程安全问题(需妥善处理共享的内部状态)
适用场景
- 当系统中存在大量相似对象,且这些对象消耗大量内存时
- 当对象的大部分状态可以外部化(即可以作为外部状态传递)时
- 当需要缓冲池管理对象时
常见应用:
- 文字处理软件(如Word)中的字符对象
- 游戏中的粒子效果(共享粒子类型,外部状态为位置、速度等)
- 数据库连接池(共享连接对象,外部状态为用户会话)
- 缓存系统(如图片缓存、数据缓存)
享元模式的关键是合理划分内部状态和外部状态:内部状态是不变的、可共享的,外部状态是可变的、依赖于上下文的。通过这种划分,实现了对象的高效复用。