享元模式
大约 12 分钟约 3604 字
享元模式
简介
享元(Flyweight)通过共享对象来最小化内存使用。当系统需要大量相似对象时,享元模式将对象的内在状态(可共享)与外在状态(不可共享)分离,显著降低内存消耗。
享元模式的名字来源于拳击中的"蝇量级",暗示其目标是让对象像蝇量级选手一样轻量。这个模式的核心洞察是:当系统中存在大量相似对象时,这些对象的很多属性(状态)是相同或可以共享的。如果将这些共享状态提取出来,让多个对象引用同一个共享对象,就能大幅减少内存使用。
享元模式在游戏开发中应用最为广泛 —— 一片森林中可能有成千上万棵树,但树的模型(纹理、形状)只有几种,只有位置、大小等属性是每棵树独有的。通过共享树的模型,可以显著减少内存占用。
特点
结构分析
UML 类图
+--------------------+
| FlyweightFactory | <-- 享元工厂
+--------------------+
| -_pool: Dictionary |
| +GetFlyweight(key) |
+--------------------+
|
v
+--------------------+
| IFlyweight | <-- 享元接口
+--------------------+
| +Operation(state) |
+--------------------+
^
|
+--------------------+
| ConcreteFlyweight | <-- 具体享元
+--------------------+
| -intrinsicState | 内在状态(共享)
| +Operation(exState)|
+--------------------+
+--------------------+
| UnsharedConcrete | <-- 非共享享元
+--------------------+
| -allState | 所有状态(不共享)
+--------------------+内在状态 vs 外在状态
内在状态 (Intrinsic) — 可共享 外在状态 (Extrinsic) — 不可共享
+---------------------------+ +---------------------------+
| 存储在享元对象内部 | | 由客户端存储或传入 |
| 不随环境变化 | | 随环境变化 |
| 可被多个对象共享 | | 每个对象独立 |
| 示例: 字体、纹理、颜色 | | 示例: 位置、大小、坐标 |
+---------------------------+ +---------------------------+实现
文本渲染享元
// 享元对象 — 字符样式(内在状态)
public class TextStyle : IEquatable<TextStyle>
{
public string FontFamily { get; }
public int Size { get; }
public string Color { get; }
public bool Bold { get; }
public TextStyle(string fontFamily, int size, string color, bool bold)
{
FontFamily = fontFamily; Size = size; Color = color; Bold = bold;
}
public bool Equals(TextStyle? other) =>
other != null && FontFamily == other.FontFamily && Size == other.Size
&& Color == other.Color && Bold == other.Bold;
public override int GetHashCode() => HashCode.Combine(FontFamily, Size, Color, Bold);
}
// 享元工厂
public class TextStyleFactory
{
private readonly ConcurrentDictionary<int, TextStyle> _cache = new();
public TextStyle GetStyle(string fontFamily, int size, string color, bool bold = false)
{
var key = HashCode.Combine(fontFamily, size, color, bold);
return _cache.GetOrAdd(key, _ => new TextStyle(fontFamily, size, color, bold));
}
public int CacheSize => _cache.Count;
}
// 外在状态 — 文本字符位置
public class Character
{
public char Value { get; }
public int X { get; set; }
public int Y { get; set; }
public TextStyle Style { get; set; } // 引用共享的享元
public Character(char value, int x, int y, TextStyle style)
{
Value = value; X = x; Y = y; Style = style;
}
public void Render() => Console.WriteLine($"'{Value}' at ({X},{Y}) font={Style.FontFamily} {Style.Size}px {Style.Color}");
}
// 使用 — 100 万个字符可能只有几十种样式
var factory = new TextStyleFactory();
var normalStyle = factory.GetStyle("Microsoft YaHei", 14, "#333", false);
var boldStyle = factory.GetStyle("Microsoft YaHei", 14, "#333", true);
var headingStyle = factory.GetStyle("Microsoft YaHei", 24, "#111", true);
var chars = new List<Character>
{
new('H', 0, 0, headingStyle),
new('e', 10, 0, headingStyle),
new('l', 20, 0, headingStyle),
new('l', 30, 0, headingStyle), // 共享同一个 headingStyle
new('o', 40, 0, headingStyle),
};
Console.WriteLine($"字符数: {chars.Count}, 样式对象数: {factory.CacheSize}"); // 5 chars, 3 styles游戏粒子享元
// 享元 — 粒子类型(纹理、动画等昂贵资源)
public record ParticleType(string Texture, string Color, string Shape, double Size);
// 享元工厂
public class ParticleTypeFactory
{
private readonly Dictionary<string, ParticleType> _types = new();
public ParticleType Get(string name, string texture, string color, string shape, double size)
{
return _types.GetValueOrDefault(name) ?? (_types[name] = new ParticleType(texture, color, shape, size));
}
}
// 外在状态 — 粒子实例
public class Particle
{
public double X { get; set; }
public double Y { get; set; }
public double VelocityX { get; set; }
public double VelocityY { get; set; }
public ParticleType Type { get; } // 共享享元
public Particle(double x, double y, ParticleType type)
{
X = x; Y = y; Type = type;
}
public void Update(double dt)
{
X += VelocityX * dt;
Y += VelocityY * dt;
}
}
// 粒子系统
public class ParticleSystem
{
private readonly List<Particle> _particles = new();
private readonly ParticleTypeFactory _factory = new();
public void Emit(string typeName, int count, double originX, double originY)
{
var type = _factory.Get(typeName, $"{typeName}.png", "#FF0000", "circle", 5.0);
var random = new Random();
for (int i = 0; i < count; i++)
{
_particles.Add(new Particle(originX, originY, type)
{
VelocityX = random.NextDouble() * 100 - 50,
VelocityY = random.NextDouble() * 100 - 50
});
}
}
public void Update(double dt) => _particles.ForEach(p => p.Update(dt));
public int Count => _particles.Count;
}
// 10000 个粒子可能只有 5 种类型 — 节省大量内存
var system = new ParticleSystem();
system.Emit("fire", 5000, 100, 100);
system.Emit("smoke", 3000, 100, 100);
system.Emit("spark", 2000, 100, 100);
Console.WriteLine($"粒子总数: {system.Count}"); // 10000,但 ParticleType 对象只有 3 个线程连接池享元
// 享元 — 数据库连接配置
public record ConnectionProfile(string Host, int Port, string Database, string Username);
public class ConnectionProfileFactory
{
private readonly ConcurrentDictionary<string, ConnectionProfile> _profiles = new();
public ConnectionProfile Get(string host, int port, string db, string user)
{
var key = $"{host}:{port}/{db}";
return _profiles.GetOrAdd(key, _ => new ConnectionProfile(host, port, db, user));
}
}实战:象棋棋子享元
象棋只有 32 个棋子,但每种棋子的外观和规则是共享的。在实际应用中,如果要在界面上显示大量棋局,享元模式可以节省内存。
// 棋子类型(内在状态 — 共享)
public class ChessPieceType
{
public string Name { get; }
public string Color { get; }
public char Symbol { get; }
public ChessPieceType(string name, string color, char symbol)
{
Name = name; Color = color; Symbol = symbol;
}
public void Display(int row, int col)
{
Console.WriteLine($"[{Color} {Name} {Symbol}] 位置: ({row},{col})");
}
}
// 享元工厂
public class ChessPieceFactory
{
private static readonly Dictionary<string, ChessPieceType> _pieces = new();
static ChessPieceFactory()
{
// 初始化所有棋子类型(红方)
_pieces["red_king"] = new ChessPieceType("帅", "红", 'K');
_pieces["red_advisor"] = new ChessPieceType("仕", "红", 'A');
_pieces["red_elephant"] = new ChessPieceType("相", "红", 'E');
_pieces["red_horse"] = new ChessPieceType("马", "红", 'H');
_pieces["red_chariot"] = new ChessPieceType("车", "红", 'R');
_pieces["red_cannon"] = new ChessPieceType("炮", "红", 'C');
_pieces["red_soldier"] = new ChessPieceType("兵", "红", 'S');
// 黑方
_pieces["black_king"] = new ChessPieceType("将", "黑", 'k');
_pieces["black_advisor"] = new ChessPieceType("士", "黑", 'a');
_pieces["black_elephant"] = new ChessPieceType("象", "黑", 'e');
_pieces["black_horse"] = new ChessPieceType("马", "黑", 'h');
_pieces["black_chariot"] = new ChessPieceType("车", "黑", 'r');
_pieces["black_cannon"] = new ChessPieceType("炮", "黑", 'c');
_pieces["black_soldier"] = new ChessPieceType("卒", "黑", 's');
}
public ChessPieceType GetPiece(string key) => _pieces[key];
}
// 棋子实例(外在状态 — 独有)
public class ChessPiece
{
public int Row { get; set; }
public int Col { get; set; }
public ChessPieceType Type { get; }
public ChessPiece(int row, int col, ChessPieceType type)
{
Row = row; Col = col; Type = type;
}
public void Move(int newRow, int newCol)
{
Row = newRow;
Col = newCol;
Type.Display(Row, Col);
}
}
// 使用
var factory = new ChessPieceFactory();
var pieces = new List<ChessPiece>
{
new(0, 0, factory.GetPiece("red_chariot")), // 红车
new(0, 1, factory.GetPiece("red_horse")), // 红马
new(9, 0, factory.GetPiece("black_chariot")), // 黑车
new(9, 4, factory.GetPiece("black_king")), // 黑将
};
// 移动棋子 — 棋子类型对象始终只有 14 个
pieces[0].Move(1, 0);享元模式进阶应用
带过期策略的享元工厂
// 带自动过期和清理的享元工厂
public class ExpirableFlyweightFactory<TKey, TFlyweight>
where TFlyweight : class
{
private readonly ConcurrentDictionary<TKey, (TFlyweight Instance, DateTime LastAccess)> _cache = new();
private readonly Func<TKey, TFlyweight> _factory;
private readonly TimeSpan _expiration;
private readonly Timer _cleanupTimer;
public ExpirableFlyweightFactory(Func<TKey, TFlyweight> factory,
TimeSpan? expiration = null)
{
_factory = factory;
_expiration = expiration ?? TimeSpan.FromMinutes(30);
// 每 10 分钟清理过期享元
_cleanupTimer = new Timer(_ => Cleanup(), null,
TimeSpan.FromMinutes(10), TimeSpan.FromMinutes(10));
}
public TFlyweight Get(TKey key)
{
var now = DateTime.UtcNow;
return _cache.AddOrUpdate(key,
_ => (_factory(key), now),
(_, existing) =>
{
// 如果过期则重新创建
if (now - existing.LastAccess > _expiration)
return (_factory(key), now);
return (existing.Instance, now);
}).Instance;
}
private void Cleanup()
{
var now = DateTime.UtcNow;
var expired = _cache
.Where(kv => now - kv.Value.LastAccess > _expiration)
.Select(kv => kv.Key)
.ToList();
foreach (var key in expired)
{
_cache.TryRemove(key, out _);
}
if (expired.Count > 0)
Console.WriteLine($"[享元清理] 移除 {expired.Count} 个过期享元");
}
public int CacheSize => _cache.Count;
}享元 + 对象池组合
// 享元管理共享状态,对象池管理实例复用
public class PooledFlyweight<T> where T : class, new()
{
private readonly ConcurrentBag<T> _pool = new();
private readonly T _sharedState; // 享元 — 共享状态
private int _createdCount;
private int _activeCount;
public PooledFlyweight(T sharedState)
{
_sharedState = sharedState;
}
public T Acquire()
{
Interlocked.Increment(ref _activeCount);
if (_pool.TryTake(out var instance))
return instance;
Interlocked.Increment(ref _createdCount);
return new T();
}
public void Release(T instance)
{
Interlocked.Decrement(ref _activeCount);
_pool.Add(instance);
}
public T SharedState => _sharedState;
public int PoolSize => _pool.Count;
public int CreatedCount => _createdCount;
public int ActiveCount => _activeCount;
}
// 使用 — 数据库连接池的享元化
public class ConnectionConfig // 享元 — 共享配置
{
public string Host { get; }
public int Port { get; }
public string Database { get; }
public ConnectionConfig(string host, int port, string db)
{
Host = host; Port = port; Database = db;
}
}
public class Connection // 可复用的连接实例
{
public Guid Id { get; } = Guid.NewGuid();
public bool IsOpen { get; private set; }
public void Open() => IsOpen = true;
public void Close() => IsOpen = false;
}
// 享元工厂 + 对象池
public class ConnectionPoolManager
{
private readonly Dictionary<string, PooledFlyweight<Connection>> _pools = new();
private readonly FlyweightFactory<string, ConnectionConfig> _configFactory = new();
public Connection Acquire(string host, int port, string database)
{
var key = $"{host}:{port}/{database}";
var config = _configFactory.Get(key, () => new ConnectionConfig(host, port, database));
if (!_pools.ContainsKey(key))
{
lock (_pools)
{
if (!_pools.ContainsKey(key))
_pools[key] = new PooledFlyweight<Connection>(config);
}
}
return _pools[key].Acquire();
}
public void Release(string host, int port, string database, Connection conn)
{
var key = $"{host}:{port}/{database}";
if (_pools.TryGetValue(key, out var pool))
pool.Release(conn);
}
}内存监控与享元效果度量
// 享元效果度量工具
public class FlyweightMetrics
{
private long _totalRequests;
private long _cacheHits;
private long _cacheMisses;
private long _bytesSaved;
public void RecordRequest(bool cacheHit, int estimatedBytes)
{
Interlocked.Increment(ref _totalRequests);
if (cacheHit)
{
Interlocked.Increment(ref _cacheHits);
Interlocked.Add(ref _bytesSaved, estimatedBytes);
}
else
{
Interlocked.Increment(ref _cacheMisses);
}
}
public void PrintReport()
{
var hitRate = _totalRequests > 0
? (double)_cacheHits / _totalRequests * 100 : 0;
Console.WriteLine("=== 享元效果报告 ===");
Console.WriteLine($"总请求数: {_totalRequests}");
Console.WriteLine($"缓存命中: {_cacheHits}");
Console.WriteLine($"缓存未命中: {_cacheMisses}");
Console.WriteLine($"命中率: {hitRate:F1}%");
Console.WriteLine($"节省内存: {FormatBytes(_bytesSaved)}");
}
private static string FormatBytes(long bytes)
{
string[] sizes = { "B", "KB", "MB", "GB" };
int order = 0;
double size = bytes;
while (size >= 1024 && order < sizes.Length - 1)
{
order++;
size /= 1024;
}
return $"{size:F2} {sizes[order]}";
}
}
// 带度量的享元工厂
public class MonitoredFlyweightFactory
{
private readonly ConcurrentDictionary<string, TextStyle> _cache = new();
private readonly FlyweightMetrics _metrics = new();
public TextStyle GetStyle(string fontFamily, int size, string color, bool bold)
{
var key = $"{fontFamily}|{size}|{color}|{bold}";
var cached = _cache.TryGetValue(key, out var style);
// 估算一个 TextStyle 对象的内存占用(粗略估计)
var estimatedSize = fontFamily.Length * 2 + color.Length * 2 + 20;
_metrics.RecordRequest(cached, estimatedSize);
if (cached) return style;
return _cache.GetOrAdd(key, _ => new TextStyle(fontFamily, size, color, bold));
}
public void PrintMetrics() => _metrics.PrintReport();
public int CacheSize => _cache.Count;
}泛型享元工厂
// 通用的泛型享元工厂
public static class FlyweightFactory
{
private static readonly ConcurrentDictionary<Type, object> _factories = new();
public static FlyweightPool<T> GetPool<T>(Func<T, T> copyFn = null)
where T : class
{
var type = typeof(T);
if (!_factories.TryGetValue(type, out var factory))
{
factory = new FlyweightPool<T>(copyFn);
_factories[type] = factory;
}
return (FlyweightPool<T>)factory;
}
}
public class FlyweightPool<T> where T : class
{
private readonly ConcurrentDictionary<string, T> _pool = new();
private readonly Func<T, T> _copyFn;
private readonly Func<T, string> _defaultKeyFn;
public FlyweightPool(Func<T, T> copyFn = null)
{
_copyFn = copyFn;
// 默认使用 GetHashCode 作为 key
_defaultKeyFn = item => item.GetHashCode().ToString();
}
public T GetShared(T prototype, Func<T, string> keyFn = null)
{
var key = (keyFn ?? _defaultKeyFn)(prototype);
return _pool.GetOrAdd(key, _ => _copyFn != null ? _copyFn(prototype) : prototype);
}
public int Count => _pool.Count;
}
// 使用
var imagePool = FlyweightFactory.GetPool<Image>(img =>
(Image)img.Clone()); // 深拷贝
var sharedImg1 = imagePool.GetShared(loadImage("logo.png"));
var sharedImg2 = imagePool.GetShared(loadImage("logo.png"));
// sharedImg1 == sharedImg2 — 同一个对象享元 vs 单例 vs 对象池
享元模式 单例模式 对象池模式
+--------+ +--------+ +--------+
| 多种 | | 一种 | | 同类 |
| 享元 | | 实例 | | 对象 |
| 对象 | | 全局 | | 复用 |
+--------+ +--------+ +--------+
| 按键 | | 全局 | | 借出 |
| 缓存 | | 访问 | | 归还 |
+--------+ +--------+ +--------+
| 目的: | | 目的: | | 目的: |
| 节省 | | 全局 | | 减少 |
| 内存 | | 唯一 | | 创建 |
+--------+ +--------+ +--------+最佳实践
- 确保内在状态不可变:享元对象创建后其内在状态不应被修改,否则会影响所有引用该享元的对象。
- 合理划分内外状态:将不随环境变化的属性作为内在状态,随环境变化的属性作为外在状态。
- 使用并发集合:在多线程环境中使用
ConcurrentDictionary作为享元池。 - 监控享元池大小:设置合理的容量上限,避免内存泄漏。
- 配合对象池使用:享元关注共享,对象池关注复用,两者可以结合使用。
优点
缺点
总结
享元模式通过共享不可变的内在状态来减少内存消耗。核心是区分内在状态(可跨对象共享,如字体样式、纹理)和外在状态(每个实例独有,如坐标、速度)。享元工厂使用字典缓存已创建对象,相同参数直接返回。建议在需要创建大量相似对象(文本编辑器字符、游戏粒子、树/草实例)的场景使用享元模式。
享元模式的本质价值在于:当你需要创建大量对象,且这些对象的大部分状态可以共享时,通过提取共享状态到独立的享元对象,让多个外部对象引用同一个享元,从而大幅减少内存使用。这是一种经典的"以共享换空间"策略。
关键知识点
- 模式不是目标,降低耦合和控制变化才是目标。
- 先找变化点、稳定点和协作边界,再决定是否引入模式。
- 同一个模式在不同规模下的收益和代价差异很大。
项目落地视角
- 优先画出参与对象、依赖方向和调用链,再落到代码。
- 把模式放到一个真实场景里,比如支付、规则引擎、工作流或插件扩展。
- 配合单元测试或契约测试,保证重构后的行为没有漂移。
常见误区
- 为了看起来"高级"而套模式。
- 把简单问题拆成过多抽象层,导致阅读和排障都变难。
- 只会背 UML,不会解释为什么这里需要这个模式。
进阶路线
- 继续关注模式之间的组合用法,而不是孤立记忆。
- 从业务建模、演进策略和团队协作角度看模式的适用性。
- 把模式结论沉淀为项目模板、基类或约束文档。
适用场景
- 当你准备把《享元模式》真正落到项目里时,最适合先在一个独立模块或最小样例里验证关键路径。
- 适合在业务规则频繁变化、分支增多或对象协作复杂时引入。
- 当你希望提高扩展性,但又不想把系统拆得过度抽象时,这类主题很有参考价值。
落地建议
- 先识别变化点,再决定是否引入模式,而不是反过来套模板。
- 优先为模式的边界、依赖和调用路径画出简单结构图。
- 把模式落到一个明确场景,例如支付、规则计算、插件扩展或工作流。
排错清单
- 检查抽象层是否过多,导致调用路径和责任不清晰。
- 确认引入模式后是否真的减少了条件分支和重复代码。
- 警惕"为了模式而模式",尤其是在简单业务里。
复盘问题
- 如果把《享元模式》放进你的当前项目,最先要验证的输入、输出和失败路径分别是什么?
- 《享元模式》最容易在什么规模、什么边界条件下暴露问题?你会用什么指标或日志去确认?
- 相比默认实现或替代方案,采用《享元模式》最大的收益和代价分别是什么?
