今天我们来详细解释一下 C# 中的享元工厂模式,我会结合源码级别的实现思路来讲解。
1. 享元模式概述
享元模式是一种结构型设计模式,它通过共享对象来有效地支持大量细粒度的对象,从而减少内存占用和提高性能。
核心思想:
- 内在状态:可以共享的、不变的部分
- 外在状态:不可共享的、变化的部分,由客户端在使用时传入
2. 享元工厂的源码级实现
下面是一个完整的享元工厂实现,包含详细的注释:
csharp
using System;
using System.Collections.Generic;
using System.Threading;
// 1. 享元接口 - 定义对象的操作
public interface ICharacter
{
void Display(int positionX, int positionY, string color);
string GetCharacter();
}
// 2. 具体享元类 - 包含内在状态
public class Character : ICharacter
{
// 内在状态 - 可以共享的部分 (通常用 readonly 保证不变性)
private readonly char _symbol;
private readonly int _fontSize;
private readonly string _fontFamily;
public Character(char symbol, int fontSize = 12, string fontFamily = "Arial")
{
_symbol = symbol;
_fontSize = fontSize;
_fontFamily = fontFamily;
// 模拟创建对象的昂贵操作
Thread.Sleep(10);
Console.WriteLine($"创建字符对象: {_symbol} (字体: {_fontSize}pt {_fontFamily})");
}
// 外在状态作为参数传入
public void Display(int positionX, int positionY, string color)
{
Console.WriteLine($"显示字符 '{_symbol}': 位置({positionX},{positionY}), 颜色:{color}, 字体:{_fontSize}pt {_fontFamily}");
}
public string GetCharacter()
{
return _symbol.ToString();
}
// 重写 Equals 和 GetHashCode 用于字典比较
public override bool Equals(object obj)
{
return obj is Character other &&
_symbol == other._symbol &&
_fontSize == other._fontSize &&
_fontFamily == other._fontFamily;
}
public override int GetHashCode()
{
return HashCode.Combine(_symbol, _fontSize, _fontFamily);
}
}
// 3. 享元工厂 - 核心管理类
public class CharacterFactory
{
// 使用线程安全的字典存储享元对象
private readonly Dictionary<string, ICharacter> _characterPool = new Dictionary<string, ICharacter>();
// 使用双重检查锁保证线程安全
private readonly object _lockObject = new object();
// 工厂方法 - 获取或创建享元对象
public ICharacter GetCharacter(char symbol, int fontSize = 12, string fontFamily = "Arial")
{
// 创建唯一键 - 基于内在状态
string key = $"{symbol}_{fontSize}_{fontFamily}";
// 第一次检查(无锁,性能优化)
if (_characterPool.ContainsKey(key))
{
Console.WriteLine($"✓ 从池中获取现有字符: {symbol}");
return _characterPool[key];
}
// 加锁保证线程安全
lock (_lockObject)
{
// 第二次检查(防止重复创建)
if (_characterPool.ContainsKey(key))
{
return _characterPool[key];
}
// 创建新对象并加入池中
var character = new Character(symbol, fontSize, fontFamily);
_characterPool[key] = character;
Console.WriteLine($"🆕 创建新字符对象并加入池: {symbol}");
return character;
}
}
// 获取池中对象数量
public int GetPoolSize()
{
lock (_lockObject)
{
return _characterPool.Count;
}
}
// 清空对象池(用于测试或重置)
public void ClearPool()
{
lock (_lockObject)
{
_characterPool.Clear();
Console.WriteLine("清空字符池");
}
}
}
// 4. 客户端代码
class Program
{
static void Main(string[] args)
{
Console.WriteLine("=== 享元模式演示 ===\n");
var factory = new CharacterFactory();
// 模拟文档中的字符显示
string documentText = "Hello World! Hello Design Patterns!";
Console.WriteLine($"文档内容: {documentText}");
Console.WriteLine($"文档长度: {documentText.Length} 字符\n");
var random = new Random();
// 显示文档内容,重复使用享元对象
for (int i = 0; i < documentText.Length; i++)
{
char currentChar = documentText[i];
// 跳过空格
if (currentChar == ' ') continue;
// 从工厂获取字符对象(共享内在状态)
var character = factory.GetCharacter(currentChar);
// 传入外在状态(位置和颜色)
int x = i * 20;
int y = random.Next(0, 100);
string color = GetRandomColor(random);
character.Display(x, y, color);
}
Console.WriteLine($"\n📊 统计信息:");
Console.WriteLine($"文档总字符数: {documentText.Length}");
Console.WriteLine($"实际创建的字符对象数: {factory.GetPoolSize()}");
Console.WriteLine($"内存节省: {(1 - (double)factory.GetPoolSize() / documentText.Length) * 100:F1}%");
// 演示重复使用
Console.WriteLine("\n=== 演示重复使用 ===\n");
factory.GetCharacter('H');
factory.GetCharacter('e');
factory.GetCharacter('l');
factory.GetCharacter('l');
factory.GetCharacter('o');
}
private static string GetRandomColor(Random random)
{
string[] colors = { "红色", "蓝色", "绿色", "黑色", "紫色" };
return colors[random.Next(colors.Length)];
}
}
3. 源码关键点解析
3.1 内在状态 vs 外在状态
csharp
// 内在状态 - 在构造函数中设置,对象共享的部分
public Character(char symbol, int fontSize, string fontFamily)
{
_symbol = symbol; // 内在
_fontSize = fontSize; // 内在
_fontFamily = fontFamily; // 内在
}
// 外在状态 - 在方法中传入,每次调用可能不同
public void Display(int positionX, int positionY, string color)
{
// positionX, positionY, color 都是外在状态
}
3.2 工厂的核心逻辑
csharp
public ICharacter GetCharacter(char symbol, int fontSize, string fontFamily)
{
// 关键:基于内在状态创建唯一键
string key = $"{symbol}_{fontSize}_{fontFamily}";
// 检查对象是否已存在
if (_characterPool.ContainsKey(key))
{
return _characterPool[key]; // 返回现有对象
}
// 创建新对象
var character = new Character(symbol, fontSize, fontFamily);
_characterPool[key] = character;
return character;
}
3.3 线程安全实现
csharp
private readonly object _lockObject = new object();
public ICharacter GetCharacter(char symbol, int fontSize, string fontFamily)
{
string key = $"{symbol}_{fontSize}_{fontFamily}";
// 第一次检查(无锁,性能优化)
if (_characterPool.ContainsKey(key))
return _characterPool[key];
lock (_lockObject) // 加锁保证线程安全
{
// 第二次检查(防止重复创建)
if (_characterPool.ContainsKey(key))
return _characterPool[key];
// 创建新对象
var character = new Character(symbol, fontSize, fontFamily);
_characterPool[key] = character;
return character;
}
}
4. 实际应用场景
4.1 游戏开发 - 粒子系统
csharp
public class ParticleFactory
{
private Dictionary<string, ParticleType> _particleTypes = new Dictionary<string, ParticleType>();
public ParticleType GetParticleType(string texture, string color, string behavior)
{
string key = $"{texture}_{color}_{behavior}";
if (!_particleTypes.ContainsKey(key))
{
_particleTypes[key] = new ParticleType(texture, color, behavior);
}
return _particleTypes[key];
}
}
4.2 文档编辑器
csharp
public class FontFactory
{
private Dictionary<string, Font> _fontPool = new Dictionary<string, Font>();
public Font GetFont(string name, int size, bool isBold)
{
string key = $"{name}_{size}_{isBold}";
if (!_fontPool.ContainsKey(key))
{
_fontPool[key] = new Font(name, size, isBold);
}
return _fontPool[key];
}
}
1. 什么是享元模式?(奶茶店的例子)
想象一下你去奶茶店点单:
csharp
// 如果没有享元模式 - 每次点奶茶都完全新做一杯
🍵 奶茶 = new 奶茶("珍珠奶茶", "大杯", "少糖", "加珍珠", "去冰");
🍵 奶茶 = new 奶茶("珍珠奶茶", "大杯", "少糖", "加珍珠", "去冰");
🍵 奶茶 = new 奶茶("珍珠奶茶", "大杯", "少糖", "加珍珠", "去冰");
// 每杯都完全重新制作,太浪费了!
聪明的奶茶店老板想到了好办法:
csharp
// 使用享元模式 - 先准备好基础奶茶
🍵 基础珍珠奶茶 = new 奶茶("珍珠奶茶", "大杯"); // 这是可以共享的
// 然后根据顾客要求添加个性化配料
基础珍珠奶茶.添加个性化("少糖", "加珍珠", "去冰");
基础珍珠奶茶.添加个性化("正常糖", "加椰果", "少冰");
// 共用基础奶茶,只调整个性化部分!
这就是享元模式的核心思想:共享不变的部分,个性化变化的部分。
2. 享元工厂代码示例(游戏角色)
我们来写一个简单的游戏例子:
第一步:定义角色接口
csharp
// 就像定义"游戏角色应该有什么功能"
public interface IGameCharacter
{
void ShowInfo(string playerName, int x, int y); // 玩家名和位置是个性化的
}
第二步:创建具体角色类
csharp
// 具体的游戏角色
public class GameCharacter : IGameCharacter
{
// 内在状态 - 所有同类角色共享的部分(就像角色模板)
private readonly string _characterType; // 角色类型
private readonly string _appearance; // 外观
private readonly string _skills; // 技能
public GameCharacter(string type, string appearance, string skills)
{
_characterType = type;
_appearance = appearance;
_skills = skills;
Console.WriteLine($"🎮 创建角色模板: {type}");
}
// 外在状态 - 每个玩家个性化的部分
public void ShowInfo(string playerName, int x, int y)
{
Console.WriteLine($"👤 玩家 {playerName} 使用 {_characterType}");
Console.WriteLine($" 外观: {_appearance}, 技能: {_skills}");
Console.WriteLine($" 位置: 地图({x}, {y})");
Console.WriteLine("---");
}
}
第三步:创建享元工厂(最重要的部分!)
csharp
// 角色工厂 - 管理所有角色模板
public class CharacterFactory
{
private Dictionary<string, IGameCharacter> _characterPool = new Dictionary<string, IGameCharacter>();
// 获取角色的核心方法
public IGameCharacter GetCharacter(string type, string appearance, string skills)
{
string key = $"{type}_{appearance}_{skills}";
// 如果池子里已经有了,直接返回
if (_characterPool.ContainsKey(key))
{
Console.WriteLine($"✅ 复用现有角色: {type}");
return _characterPool[key];
}
// 如果没有,创建新的并存入池子
Console.WriteLine($"🆕 创建新角色模板: {type}");
var character = new GameCharacter(type, appearance, skills);
_characterPool[key] = character;
return character;
}
// 查看当前有多少种角色模板
public int GetTemplateCount()
{
return _characterPool.Count;
}
}
第四步:使用享元工厂
csharp
class Program
{
static void Main()
{
Console.WriteLine("=== 游戏角色管理系统 ===\n");
var factory = new CharacterFactory();
// 玩家1:选择战士
var warrior = factory.GetCharacter("战士", "重甲大刀", "冲锋、重击");
warrior.ShowInfo("小明", 10, 20);
// 玩家2:也选择战士(复用同一个模板!)
var warrior2 = factory.GetCharacter("战士", "重甲大刀", "冲锋、重击");
warrior2.ShowInfo("小红", 30, 40);
// 玩家3:选择法师(新模板)
var mage = factory.GetCharacter("法师", "法袍法杖", "火球、治疗");
mage.ShowInfo("小刚", 50, 60);
// 玩家4:也选择法师(复用法师模板)
var mage2 = factory.GetCharacter("法师", "法袍法杖", "火球、治疗");
mage2.ShowInfo("小丽", 70, 80);
Console.WriteLine($"\n📊 统计信息:");
Console.WriteLine($"玩家总数: 4人");
Console.WriteLine($"实际创建的角色模板: {factory.GetTemplateCount()}种");
Console.WriteLine($"内存节省: {(1 - 2.0/4) * 100}%"); // 只创建了2个模板,服务了4个玩家
}
}
运行结果:
=== 游戏角色管理系统 ===
🎮 创建角色模板: 战士
👤 玩家 小明 使用 战士
外观: 重甲大刀, 技能: 冲锋、重击
位置: 地图(10, 20)
---
✅ 复用现有角色: 战士
👤 玩家 小红 使用 战士
外观: 重甲大刀, 技能: 冲锋、重击
位置: 地图(30, 40)
---
🎮 创建角色模板: 法师
👤 玩家 小刚 使用 法师
外观: 法袍法杖, 技能: 火球、治疗
位置: 地图(50, 60)
---
✅ 复用现有角色: 法师
👤 玩家 小丽 使用 法师
外观: 法袍法杖, 技能: 火球、治疗
位置: 地图(70, 80)
---
📊 统计信息:
玩家总数: 4人
实际创建的角色模板: 2种
内存节省: 50%
3. 生活中的享元模式例子
例子1:办公楼的打印机
csharp
// 没有享元模式 - 每个员工一台专用打印机
🖨️ 打印机1 = new 打印机("激光打印机", "彩色", "A4");
🖨️ 打印机2 = new 打印机("激光打印机", "彩色", "A4");
🖨️ 打印机3 = new 打印机("激光打印机", "彩色", "A4");
// 使用享元模式 - 共享打印机
🖨️ 共享打印机 = 打印机工厂.获取打印机("激光打印机", "彩色", "A4");
员工1.使用打印机(共享打印机, "员工1的文档");
员工2.使用打印机(共享打印机, "员工2的文档");
例子2:文字处理软件
csharp
// 文档中有1000个字母'A'
// 没有享元模式:创建1000个'A'对象
// 使用享元模式:创建1个'A'模板,重复使用1000次
var 字母A = 字体工厂.获取字符('A', "宋体", 12);
for(int i = 0; i < 1000; i++)
{
字母A.显示(i * 10, 0, "黑色"); // 只改变位置和颜色
}
4. 什么时候使用享元模式?
使用场景:
- ✅ 程序需要创建大量相似对象
- ✅ 这些对象有很多相同部分,只有少量不同
- ✅ 对象创建成本很高(比如加载图片、连接数据库)
- ✅ 内存有限,需要优化
不要使用的情况:
- ❌ 对象之间差异很大,没有可共享的部分
- ❌ 对性能要求不高的小型应用
- ❌ 会增加代码复杂度,得不偿失
5. 总结
记住享元模式的三个关键点:
- 共享不变的部分(内在状态)- 就像角色模板
- 个性化变化的部分(外在状态)- 就像玩家名和位置
- 使用工厂来管理共享 - 避免重复创建
简单比喻:
就像用橡皮图章 - 你只需要一个章(共享模板),但可以在不同位置盖出很多图案(个性化)。
这样理解享元模式是不是很简单?这就是一个"共享经济"的设计模式!
5. 性能优势分析
假设我们有一个包含 1000 个字符的文档,但只使用 26 个字母:
- 不使用享元模式:创建 1000 个字符对象
- 使用享元模式:只创建 26 个字符对象 + 1000 个位置/颜色信息
内存节省:
节省比例 = (1 - 26/1000) × 100% = 97.4%
6. 总结
享元工厂的核心要点:
- 分离状态:明确区分内在状态(可共享)和外在状态(不可共享)
- 对象池管理:使用字典或哈希表来管理和重用对象
- 线程安全:在多线程环境下需要适当的同步机制
- 性能权衡:适用于对象创建成本高、内存有限的场景
- 适用场景:文本编辑器、游戏开发、图形系统等需要大量细粒度对象的场景
这种模式通过共享相同内在状态的对象,显著减少了内存占用和对象创建开销,是优化性能的重要工具。