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. 适用场景:文本编辑器、游戏开发、图形系统等需要大量细粒度对象的场景

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

相关推荐
u***u6851 小时前
C++在系统中的异常处理
java·开发语言·c++
空空kkk1 小时前
SpringMVC——拦截器
java·数据库·spring·拦截器
爱学测试的雨果1 小时前
收藏!软件测试面试题
开发语言·面试·职场和发展
鹿衔`2 小时前
通过Flink 1.19 客户端实现Flink集群连接 Kafka 基础测试报告
c#·linq
J***51682 小时前
MySql中的事务、MySql事务详解、MySql隔离级别
数据库·mysql·adb
安然无虞2 小时前
JMeter性能测试工具·下
开发语言·测试工具·jmeter
4***R2402 小时前
C++在音视频处理中的库
开发语言·c++·音视频
SelectDB2 小时前
Apache Doris 中的 Data Trait:性能提速 2 倍的秘密武器
数据库·后端·apache
i***27952 小时前
Spring boot 3.3.1 官方文档 中文
java·数据库·spring boot