深入浅出享元模式:从图形编辑器看对象复用的艺术
在软件开发中,当系统需要处理大量相似对象时,内存占用往往成为性能瓶颈。想象一下,在一个图形编辑器中,如果用户绘制了成千上万的圆形,每个圆形都作为独立对象存在,即使它们的形状完全相同,也会造成极大的内存浪费。这时候,享元模式(Flyweight Pattern)就成为了拯救内存的利器。本文将结合图形编辑器的实战场景,深入解析享元模式的设计思想与实现方式,揭示如何通过对象复用优化系统资源占用。
一、享元模式的核心思想
享元模式的核心在于运用共享技术有效支持大量细粒度对象,其关键是区分对象的两种状态:
内部状态(Intrinsic State):对象可共享的不变部分,存储在享元对象内部,对所有同类对象一致。例如图形编辑器中,"圆形""矩形"这类图形类型就是内部状态,不会因绘制位置变化而改变。
外部状态(Extrinsic State):对象依赖的可变部分,由客户端保存,使用时传递给享元对象。例如图形的坐标位置、绘制时的临时标记(是否首次使用)等,这些状态会随使用场景动态变化。
通过分离两种状态,享元模式能让多个对象共享同一个享元实例,仅通过外部状态的差异来体现不同的使用场景,从而大幅减少系统中的对象数量,降低内存开销。
享元模式通用UML图
享元模式的经典结构包含以下核心角色,其UML类图如下:

Flyweight(抽象享元):定义享元对象的接口,声明接收外部状态的方法。
ConcreteFlyweight(具体享元):实现抽象享元接口,存储内部状态,可被共享。
UnsharedConcreteFlyweight(非共享具体享元):不参与共享的享元实现,通常因包含不可共享的状态。
FlyweightFactory(享元工厂):管理享元对象的创建与缓存,确保合理复用共享实例。
Client(客户端):维护外部状态,通过工厂获取享元对象并调用其方法。
Extrinsic State(外部状态):客户端传递给享元的可变状态,不被享元对象存储。
二、图形编辑器中的享元模式设计
我们以一个支持多种图形绘制的编辑器为例实现享元模式,需求如下:
- 支持圆形(CIRCLE)、矩形(RECTANGLE)、三角形(TRIANGLE)的共享复用,减少重复对象创建;
- 新增星形(STAR)作为非共享类型,每次绘制都需创建新实例(模拟特殊图形无需复用的场景);
- 输出时区分"首次创建""复用共享""非共享创建"三种状态,清晰展示享元模式的运行逻辑。
1. 核心组件设计
根据享元模式的经典结构,需设计以下核心组件:
- 抽象享元接口(Shape):定义图形的统一绘制方法,接收外部状态(位置信息);
- 具体享元类:分为"共享型"和"非共享型",分别实现复用逻辑和独立创建逻辑;
- 享元工厂(ShapeFactory):负责管理共享对象的创建与缓存,同时处理非共享对象的即时创建。
2. 案例UML图
本图形编辑器案例的UML类图如下:

Shape :抽象享元接口,定义draw()方法接收外部状态Position。
SharedConcreteShape :共享具体享元,存储内部状态shapeType,实现复用逻辑。
UnsharedConcreteShape:非共享具体享元,对应星形图形,每次创建新实例。
ShapeFactory :享元工厂,通过getShape()管理共享对象缓存(圆形/矩形/三角形)和非共享对象创建(星形)。
Position:外部状态载体,存储图形坐标信息。
Client :包含main()和processCommand(),负责输入处理和外部状态管理。
3. 代码实现详解
(1)基础类型定义
首先定义图形类型枚举和位置类,位置类作为外部状态载体,传递图形的绘制坐标:
cpp
#include <iostream>
#include <unordered_map>
#include <string>
using namespace std;
// 图形类型枚举:包含共享类型(圆形/矩形/三角形)和非共享类型(星形)
enum ShapeType {
CIRCLE, RECTANGLE, TRIANGLE, STAR // STAR为非共享享元
};
// 类型转字符串:统一输出格式
string shapeTypeToString(ShapeType type) {
switch (type) {
case CIRCLE: return "CIRCLE";
case RECTANGLE: return "RECTANGLE";
case TRIANGLE: return "TRIANGLE";
case STAR: return "STAR";
default: return "UNKNOWN";
}
}
// 位置类:存储图形坐标(外部状态)
class Position {
private:
int x, y;
public:
Position(int x, int y) : x(x), y(y) {}
int getX() const { return x; }
int getY() const { return y; }
};
(2)抽象享元接口
抽象接口定义所有图形的统一行为,确保共享与非共享类型都遵循相同的调用规范:
cpp
// 抽象享元接口:定义图形绘制方法
class Shape {
public:
// 绘制方法:接收外部状态(位置)
virtual void draw(const Position &position) = 0;
// 虚析构函数:确保子类资源正确释放
virtual ~Shape() {}
};
(3)具体享元实现
区分"共享型"和"非共享型"具体享元,分别处理复用逻辑和独立创建逻辑:
cpp
// 共享具体享元:被工厂缓存,支持复用
class SharedConcreteShape : public Shape {
private:
ShapeType shapeType; // 内部状态:图形类型(固定不变)
bool isFirstTime; // 外部状态衍生:标记是否首次绘制(控制输出文案)
public:
// 构造函数:初始化内部状态
SharedConcreteShape(ShapeType type) : shapeType(type), isFirstTime(true) {}
// 绘制实现:结合内部状态和外部状态(位置)输出结果
void draw(const Position &position) override {
cout << shapeTypeToString(shapeType)
<< (isFirstTime ? " drawn" : " shared") // 首次绘制/复用标记
<< " at (" << position.getX() << ", " << position.getY() << ")\n";
}
// 外部状态修改:复用后更新首次标记
void setFirstTime(bool firstTime) { isFirstTime = firstTime; }
};
// 非共享具体享元:不被缓存,每次使用创建新实例
class UnsharedConcreteShape : public Shape {
private:
ShapeType shapeType; // 仅存储类型,无需复用标记
public:
// 构造函数:初始化内部状态(非共享对象可扩展独有的状态,如颜色、大小等)
UnsharedConcreteShape(ShapeType type) : shapeType(type) {}
// 绘制实现:每次都是新实例,输出"非共享创建"标记
void draw(const Position &position) override {
cout << shapeTypeToString(shapeType)
<< " created (unshared) at (" << position.getX() << ", " << position.getY() << ")\n";
}
};
(4)享元工厂实现
工厂是享元模式的核心,负责管理共享对象的缓存与复用,同时处理非共享对象的即时创建,避免客户端直接依赖具体实现:
cpp
// 享元工厂:区分共享/非共享类型,控制对象创建逻辑
class ShapeFactory {
private:
// 缓存共享享元:key为图形类型,value为共享对象实例
unordered_map<ShapeType, Shape*> sharedShapes;
public:
// 获取图形对象:根据类型判断是否复用
Shape* getShape(ShapeType type) {
// 共享类型逻辑:存在则复用,不存在则创建并缓存
if (type == CIRCLE || type == RECTANGLE || type == TRIANGLE) {
if (sharedShapes.find(type) == sharedShapes.end()) {
// 首次请求:创建共享对象并加入缓存
sharedShapes[type] = new SharedConcreteShape(type);
}
return sharedShapes[type];
}
// 非共享类型逻辑:每次请求都创建新实例,不缓存
else if (type == STAR) {
return new UnsharedConcreteShape(type);
}
return nullptr; // 无效类型:返回空指针
}
// 析构函数:释放缓存的共享对象(避免内存泄漏)
~ShapeFactory() {
for (auto &entry : sharedShapes) {
delete entry.second;
}
}
};
(5)客户端实现
客户端负责处理用户输入(命令)、维护外部状态(位置),并通过工厂获取享元对象,无需直接创建或管理对象生命周期:
cpp
// 命令处理:解析输入并调用享元对象
void processCommand(ShapeFactory &factory) {
string shapeTypeStr;
int x, y;
// 读取输入:格式为"图形类型 横坐标 纵坐标"(如"CIRCLE 10 20")
if (!(cin >> shapeTypeStr >> x >> y)) {
exit(0); // 输入结束或异常:退出程序
}
// 转换图形类型(字符串→枚举)
ShapeType type;
if (shapeTypeStr == "CIRCLE") type = CIRCLE;
else if (shapeTypeStr == "RECTANGLE") type = RECTANGLE;
else if (shapeTypeStr == "TRIANGLE") type = TRIANGLE;
else if (shapeTypeStr == "STAR") type = STAR;
else {
cerr << "Invalid shape type: " << shapeTypeStr << endl;
return;
}
// 通过工厂获取图形对象并绘制
Shape* shape = factory.getShape(type);
if (shape) {
shape->draw(Position(x, y));
// 特殊处理:共享对象复用后更新首次标记,非共享对象使用后释放
if (type != STAR) {
dynamic_cast<SharedConcreteShape*>(shape)->setFirstTime(false);
} else {
delete shape; // 非共享对象:使用后立即释放,避免内存泄漏
}
}
}
// 主函数:初始化工厂并循环处理命令
int main() {
ShapeFactory factory;
while (true) {
processCommand(factory);
}
return 0;
}
三、享元模式的运行机制
为了直观展示享元模式的效果,我们输入一组测试命令,观察输出结果:
测试输入
CIRCLE 10 20 // 首次请求圆形:创建共享对象
CIRCLE 30 40 // 再次请求圆形:复用共享对象
STAR 50 60 // 首次请求星形:创建非共享对象
STAR 70 80 // 再次请求星形:创建新的非共享对象
RECTANGLE 90 100// 首次请求矩形:创建共享对象
RECTANGLE 110 120// 再次请求矩形:复用共享对象
输出结果
CIRCLE drawn at (10, 20) // 首次创建共享对象
CIRCLE shared at (30, 40) // 复用已缓存的共享对象
STAR created (unshared) at (50, 60)// 创建非共享对象
STAR created (unshared) at (70, 80)// 再次创建新的非共享对象
RECTANGLE drawn at (90, 100) // 首次创建共享对象
RECTANGLE shared at (110, 120) // 复用已缓存的共享对象
从结果可见:
共享类型(圆形、矩形)仅首次创建时标记"drawn",后续复用标记"shared",实现了对象复用;
非共享类型(星形)每次请求都标记"created (unshared)",每次都是新实例,符合非共享场景需求。
四、享元模式的适用场景与优缺点
适用场景
- 大量相似对象场景:系统中存在大量结构相似、仅部分状态不同的对象(如图形编辑器中的图形、文本编辑器中的字符);
- 内存敏感场景:对象数量过多导致内存占用过高,需要通过复用减少对象创建(如游戏中的粒子效果、海量数据展示中的重复组件);
- 外部状态可分离场景:对象的状态可分为内部(共享)和外部(可变)两部分,且外部状态可由客户端独立维护。
优点
- 降低内存占用:通过对象复用,减少系统中对象的总数量,缓解内存压力;
- 提高性能:减少对象创建和销毁的频繁操作,降低系统开销;
- 增强扩展性:新增共享类型时,只需扩展具体享元类和工厂逻辑,符合开闭原则。
缺点
- 增加系统复杂度:需要分离内部状态和外部状态,且需通过工厂管理对象,增加了代码设计难度;
- 客户端负担加重:客户端需负责维护外部状态,且需理解享元模式的使用逻辑;
- 线程安全风险:共享对象在多线程环境下可能存在并发访问问题,需额外处理同步逻辑。
五、总结
享元模式的本质是"以空间换时间"的优化思想------通过缓存共享对象减少重复创建,从而降低内存占用并提升系统效率。其核心在于清晰区分对象的"不变部分"(内部状态)和"可变部分"(外部状态),并通过工厂模式统一管理对象的创建与复用,避免客户端直接与具体实现耦合。
在实际开发中,享元模式不仅可用于图形编辑器、文本处理等场景,还广泛应用于数据库连接池、线程池、缓存系统等组件中。理解其"共享"的设计思想,根据业务场景灵活选择共享与非共享类型,才能真正发挥其优化系统资源的价值。正如《大话设计模式》中所强调的,设计模式的核心是"找出程序中变化的地方,并将变化封装起来",享元模式正是通过封装对象的不变状态,让可变状态灵活适配不同场景,最终实现高效、优雅的代码设计。
本文中相关概念参考自《大话设计模式》一书,该书以通俗易懂的方式讲解了设计模式与设计原则的核心思想,推荐大家深入阅读,场景案例及代码参考来自程序员刷题学习网站卡码网。