享元模式 (Flyweight Pattern) 深度解析
享元模式 (Flyweight Pattern) 深度解析
没问题,我完全理解了。之前的处理确实在整合时丢失了部分深度,且没有严格遵循“一篇一篇输出”的节奏。
现在,我将严格按照您的要求:保持原文深度、优化排版结构、增加 Java/Python 代码示例,为您重新输出享元模式 (Flyweight Pattern) 的完整深度解析笔记。
🚀 享元模式 (Flyweight Pattern) 深度解析
1. 模式动机与背景
面向对象技术可以很好地解决一些灵活性或可扩展性问题,但在很多情况下需要在系统中增加类和对象的个数。
- 问题场景:当系统中存在大量相同或相似的细粒度对象时,如果为每个实例都创建独立的对象,将导致内存占用极高,运行代价过大,甚至导致性能急剧下降(例如:文档编辑器中的字符对象、游戏中的粒子效果)。
- 解决方案:享元模式通过共享技术实现相同或相似对象的重用。
- 核心思想:
- 内部状态 (Intrinsic State):存储在享元对象内部,不会随环境改变而改变,因此可以共享(例如:字符的字体、颜色)。
- 外部状态 (Extrinsic State):随环境改变而改变,不可以共享,必须由客户端保存,并在使用时传入享元对象(例如:字符在文档中的位置)。
通过区分这两种状态,我们可以将大量细粒度对象的内部状态剥离出来共享,从而将内存中对象的数量从“百万级”降低到“几十个”。
2. 模式定义
享元模式 (Flyweight Pattern):
运用共享技术有效地支持大量细粒度对象的复用。系统只使用少量的对象,而这些对象都很相似,状态变化很小,可以实现对象的多次复用。
由于享元模式要求能够共享的对象必须是细粒度对象,因此它又称为轻量级模式,它是一种对象结构型模式。
3. 模式结构与角色
享元模式的核心在于享元工厂和享元池。
| 角色名称 | 职责描述 |
|---|---|
| Flyweight (抽象享元类) | 通常是一个接口或抽象类,声明了具体享元类的公共方法,这些方法可以向外界提供对象的内部状态,同时也可以通过这些方法设置外部状态。 |
| ConcreteFlyweight (具体享元类) | 实现了抽象享元接口,为内部状态增加存储空间。ConcreteFlyweight 对象必须是可共享的。它所存储的状态必须是内部的,即它不随环境的改变而改变。 |
| UnsharedConcreteFlyweight (非共享具体享元类) | 并非所有的抽象享元类的子类都需要被共享。不能被共享的子类通常设计为非共享具体享元类(例如,复合享元模式中的组合对象)。 |
| FlyweightFactory (享元工厂类) | 负责创建和管理享元对象。它主要用来确保合理地共享享元,当用户请求一个享元时,工厂提供一个已创建的实例或者创建一个(如果不存在的话)。 |
时序图逻辑
- Client 请求 FlyweightFactory。
- Factory 检查享元池(Pool)。
- 如果有对应的 Key,直接返回 Pool 中的对象。
- 如果没有,创建一个新的 ConcreteFlyweight,存入 Pool,然后返回。
4. 代码深度解析(多语言实现)
为了更好地理解“内部状态”和“外部状态”的分离,我们以绘制圆形为例。
- 内部状态(共享):圆的颜色(Color)。
- 外部状态(不共享):圆的坐标(X, Y)和半径(Radius)。
4.1. Java 代码示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
import java.util.HashMap;
// 1. 抽象享元类 (Flyweight)
public interface Shape {
// draw 方法接收外部状态作为参数
void draw(int x, int y, int radius);
}
// 2. 具体享元类 (ConcreteFlyweight)
public class Circle implements Shape {
// 内部状态:颜色 (在对象创建后不再改变,可共享)
private String color;
public Circle(String color){
this.color = color;
}
@Override
public void draw(int x, int y, int radius) {
// 这里的 x, y, radius 是外部状态,由客户端在运行时传入
System.out.println("Circle: Draw() [Color : " + color
+ ", x : " + x + ", y :" + y + ", radius :" + radius + "]");
}
}
// 3. 享元工厂类 (FlyweightFactory)
public class ShapeFactory {
// 享元池:使用 HashMap 存储已创建的 Shape 对象
private static final HashMap<String, Shape> circleMap = new HashMap<>();
public static Shape getCircle(String color) {
// 尝试从缓存池获取
Circle circle = (Circle)circleMap.get(color);
if(circle == null) {
// 如果不存在,则创建新对象并放入池中
circle = new Circle(color);
circleMap.put(color, circle);
System.out.println("Creating circle of color : " + color);
} else {
System.out.println("Reusing existing circle of color : " + color);
}
return circle;
}
}
// 4. 客户端调用
public class FlyweightPatternDemo {
private static final String colors[] = { "Red", "Green", "Blue" };
public static void main(String[] args) {
// 模拟绘制 10 个圆,但实际上只会创建 3 个对象 (Red, Green, Blue)
for(int i=0; i < 10; ++i) {
String color = colors[(int)(Math.random()*colors.length)];
// 获取享元对象
Circle circle = (Circle)ShapeFactory.getCircle(color);
// 传入外部状态 (随机坐标)
circle.draw(getRandomX(), getRandomY(), 100);
}
}
private static int getRandomX() { return (int)(Math.random()*100 ); }
private static int getRandomY() { return (int)(Math.random()*100); }
}
4.2. Python 代码示例
Python 中利用字典来实现享元池非常直观。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
import random
# 1. 抽象享元类 (Conceptually)
class Flyweight:
def operation(self, extrinsic_state):
pass
# 2. 具体享元类
class ConcreteFlyweight(Flyweight):
def __init__(self, intrinsic_state):
# 内部状态:可共享
self.intrinsic_state = intrinsic_state
def operation(self, extrinsic_state):
# 外部状态:由客户端传入
print(f"Flyweight [{self.intrinsic_state}] executing with external state: {extrinsic_state}")
# 3. 享元工厂类
class FlyweightFactory:
_flyweights = {}
@staticmethod
def get_flyweight(key):
if key not in FlyweightFactory._flyweights:
print(f"--> Creating new Flyweight for key: {key}")
FlyweightFactory._flyweights[key] = ConcreteFlyweight(key)
else:
print(f"--> Reusing existing Flyweight for key: {key}")
return FlyweightFactory._flyweights[key]
@staticmethod
def get_count():
return len(FlyweightFactory._flyweights)
# 4. 客户端代码
if __name__ == "__main__":
factory = FlyweightFactory()
# 模拟请求多个对象
keys = ["A", "B", "A", "C", "B", "A"]
for key in keys:
flyweight = factory.get_flyweight(key)
# 传入外部状态 (例如随机数)
flyweight.operation(random.randint(1, 100))
print(f"\nTotal objects created in memory: {factory.get_count()}")
# 输出结果将显示实际内存中只有 3 个对象 (A, B, C)
4.3. C++ 代码逻辑分析 (基于原文)
原文中的 C++ 代码展示了标准的指针管理和 Map 容器使用。
FlyweightFactory: 内部维护map<string, Flyweight*> m_mpFlyweight。getFlyweight(string str)方法先查找 iterator,如果itr == end()则new ConcreteFlyweight,否则直接返回itr->second。ConcreteFlyweight: 构造函数接收intrinsicState。operation()方法执行具体逻辑。- 内存管理注意: 在 C++ 中使用享元模式,需要特别注意享元工厂析构时对池中所有对象的内存释放,防止内存泄漏。
5. 模式分析与优缺点
5.1. 优点
- 极大减少内存中对象的数量:使得相同对象或相似对象在内存中只保存一份。
- 外部状态独立:外部状态相对独立,而且不会影响其内部状态,从而使得享元对象可以在不同的环境中被共享。
5.2. 缺点
- 系统复杂度增加:需要分离出内部状态和外部状态,这使得程序的逻辑复杂化。
- 运行时间变长:为了使对象可以共享,享元模式需要将享元对象的状态外部化,而读取外部状态使得运行时间变长(时间换空间)。
- 线程安全问题:如果多个线程同时修改共享对象的外部状态注入过程,需要注意并发控制(虽然内部状态通常是不可变的)。
6. 适用环境
在以下情况下强烈建议使用享元模式:
- 一个系统有大量相同或者相似的对象,由于这类对象的大量使用,造成内存的大量耗费。
- 对象的大部分状态都可以外部化,可以将这些外部状态传入对象中。
- 系统不依赖于这些对象的身份,这些对象是不可分辨的(即
obj1 == obj2对逻辑无影响)。 - 需要缓冲池的场景。
7. 模式应用实例
- Java String 常量池:Java 中的字符串如果内容相同,通常指向常量池中的同一个对象。
- 文本编辑器:文档中包含成千上万个字符对象(如 ‘a’, ‘b’, ‘c’),每个字符对象的内部状态(字形、编码)是共享的,而外部状态(位置、大小、颜色)由文档结构维护。
- 数据库连接池:虽然连接池更侧重于复用连接资源(避免频繁握手),但其核心思想也是减少重对象的创建,通过池化技术实现复用,与享元模式有异曲同工之妙。
8. 模式扩展:单纯享元 vs 复合享元
- 单纯享元模式:
- 所有的享元对象都是可以共享的。
- 即所有抽象享元类的子类都可共享,不存在非共享具体享元类。
- 复合享元模式 (Composite Flyweight):
- 将一些单纯享元使用组合模式 (Composite Pattern) 加以组合。
- 形成的复合享元对象本身不能共享,但是它们可以分解成单纯享元对象,而后者则可以共享。
- 例如:一个“单词”对象可以由多个“字母”享元对象组合而成。
总结:享元模式是用时间换空间的经典模式。它通过复杂的工厂逻辑和状态分离,换取了内存占用的极大降低。在处理大规模细粒度对象的系统中,它是必不可少的优化手段。
本文由作者按照 CC BY 4.0 进行授权