C# 中的**享元工厂**模式

今天我们来详细解释一下 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. 总结

记住享元模式的三个关键点:

  1. 共享不变的部分(内在状态)- 就像角色模板
  2. 个性化变化的部分(外在状态)- 就像玩家名和位置
  3. 使用工厂来管理共享 - 避免重复创建

简单比喻:

就像用橡皮图章 - 你只需要一个章(共享模板),但可以在不同位置盖出很多图案(个性化)。

这样理解享元模式是不是很简单?这就是一个"共享经济"的设计模式!

5. 性能优势分析

假设我们有一个包含 1000 个字符的文档,但只使用 26 个字母:

  • 不使用享元模式:创建 1000 个字符对象
  • 使用享元模式:只创建 26 个字符对象 + 1000 个位置/颜色信息

内存节省

复制代码
节省比例 = (1 - 26/1000) × 100% = 97.4%

6. 总结

享元工厂的核心要点:

  1. 分离状态:明确区分内在状态(可共享)和外在状态(不可共享)
  2. 对象池管理:使用字典或哈希表来管理和重用对象
  3. 线程安全:在多线程环境下需要适当的同步机制
  4. 性能权衡:适用于对象创建成本高、内存有限的场景
  5. 适用场景:文本编辑器、游戏开发、图形系统等需要大量细粒度对象的场景

这种模式通过共享相同内在状态的对象,显著减少了内存占用和对象创建开销,是优化性能的重要工具。

相关推荐
Jayden_Ruan10 小时前
C++蛇形方阵
开发语言·c++·算法
洛阳纸贵10 小时前
Redis
数据库·redis·缓存
心.c11 小时前
如何基于 RAG 技术,搭建一个专属的智能 Agent 平台
开发语言·前端·vue.js
智航GIS11 小时前
10.7 pyspider 库入门
开发语言·前端·python
跟着珅聪学java11 小时前
JavaScript 底层原理
java·开发语言
l1t11 小时前
DeepSeek辅助编写的利用位掩码填充唯一候选数方法求解数独SQL
数据库·sql·算法·postgresql
项目題供诗11 小时前
C语言基础(二)
c语言·开发语言
J_liaty11 小时前
RabbitMQ面试题终极指南
开发语言·后端·面试·rabbitmq
墨月白11 小时前
[QT] QT中的折线图和散点图
数据库·qt