65.9K
CodeProject 正在变化。 阅读更多。
Home

C# 中的享元设计模式

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.79/5 (8投票s)

2014年7月6日

CPOL

6分钟阅读

viewsIcon

36715

使用 C# 实现享元设计模式

引言

本文将介绍享元设计模式的概念以及如何在 C# 中实现它。

背景

享元设计模式虽然不常用,但在内存受限的场景下非常有帮助。简单来说,享元模式适用于我们希望限制对象创建数量的情况。我说“限制对象创建数量”了吗?我们生活在一个面向对象的、鼓励创建对象的时代,如何能减少对象创建呢?所以我要重新审视享元模式的定义。根据 GOF 的定义——“享元是可以在多个上下文同时使用的共享对象”。那么,为什么这个模式叫做享元呢?“软件中的享元”这个词与“拳击中的羽量级”类似,羽量级指的是一个轻量级的拳击级别。同样,我们也有一个轻量级对象(不是拳击手),它可以在系统中同时用于不同的地方。

享元对象可以由 2 种状态来描述:

  1. 内在状态 - 正如其名,内在状态是享元对象固有的、可以共享的。内在状态独立于享元的上下文。
  2. 外在状态 - 外在状态是随享元上下文而变化的,因此不能共享。

为了区分内在状态和外在状态,让我们看一个非常著名的例子(GOF 书中也有提及)。文字处理器需要处理字符对象。如果我们看文字处理器中的一个字符对象,它的状态包括字体、样式、位置、实际内容。仅从这个例子来看,我们可以发现字符的字体、样式、位置都可以变化,但字符的内容可以是相同的(至少在 26 个字母 [a-z] 中)。所以,字符的字体、样式、位置都是外在状态,因为它们会变化且不能共享。字符的内容(如“a”、“b”……)可以共享,构成了内在状态。因此,我们可以得出结论,如果我们在一个系统中需要创建大量对象,并且每个对象都可以通过共同的内在状态和外在状态来描述,那么我们就应该考虑应用享元模式。

让我们来看一下享元模式的 UML 图和各个参与者。

 

 

FlyweightFactory(享元工厂):顾名思义,它是一个工厂,负责管理享元。当请求一个享元对象时,工厂会返回已有的实例,否则会创建一个新的享元,将其添加到工厂缓存中并返回。

IFlyweight(享元接口):这是享元实现的接口。GetExtrinsicState 方法获取外在状态,IntrinsicState 存储享元的内在状态。

ConcreteFlyweight(具体享元):这是实际的享元对象,它是可共享的。它存储内在状态并派生外在状态。

使用代码

现在考虑一个实现享元模式的例子。假设我们要创建一个动画游戏,我最喜欢的卡通人物必须收集从天而降的金钱。

你在这里闻到享元模式的味道了吗?现在,由于我们需要跟踪“卡通人物”收集了多少金额,我们将创建这些独立货币面额的“对象”,以便计算总额。由于在这个动画游戏中会有大量的金钱从天而降,想象一下“纸币”对象会占用多少内存?通常,货币对象会包含国父的肖像、艺术字体等许多其他内容,而这些对于这些对象来说总是相同的。同样,每种金属货币的面额也会是相似的对象。因此,我们不必为这些掉落的金钱创建数千个对象,而是只创建 2 个对象:一个用于金属货币,一个用于纸币。此外,由于这些对象的各种属性很容易分为内在状态和外在状态,因此这是一个应用享元模式的理想场景。下表显示了纸币的内在状态和外在状态。

纸币

基于上述场景/问题,让我们看看代码,了解享元模式的使用。(注意:在上面我们可以创建单个面额货币的享元,但为了代码的简洁性,我避免了这样做,只创建了金属货币和纸币的享元)。

public enum EnMoneyType
{
  Metallic,
  Paper
}

public interface IMoney
{
  EnMoneyType MoneyType { get; } //IntrinsicState()
  void GetDisplayOfMoneyFalling(int moneyValue); //GetExtrinsicSate()
}

上述接口“IMoney”对应于 IFlyweight 接口。“MoneyType”将保存内在状态(金属货币、纸币将是享元类的内在状态),而 GetDisplayOfMoneyFalling 操作将作用于享元的外在状态,即货币面额。

public class MetallicMoney:IMoney
{
   public EnMoneyType MoneyType
   {
     get { return EnMoneyType.Metallic; }
   }

   public void GetDisplayOfMoneyFalling(int moneyValue)
   {
     //This method would display graphical representation of a metallic currency like a    
       gold coin.
       Console.WriteLine(string.Format("Displaying a graphical object of {0} currency of 
                                        value ${1} falling from sky."
                                        , MoneyType.ToString(), moneyValue)
                                      );
   }
}

这是实现 IMoney 接口的具体享元“金属货币”类。这个享元将用于创建不同的对象(每个对象都有不同的外在状态)。这里的外在状态将是 moneyValue,内在状态将是金属货币。

class PaperMoney:IMoney
{
  public EnMoneyType MoneyType
  {
    get { return EnMoneyType.Paper; }
  }

  public void GetDisplayOfMoneyFalling(int moneyValue) //GetExtrinsicState()
  {
    // This method would display a graphical representation of a paper currency.
       Console.WriteLine(string.Format("Displaying a graphical object of {0} currency                                        of value ${1} falling from sky."
                                       ,MoneyType.ToString(), moneyValue));
  }
}

这是另一个将被用于创建多个对象的具体享元。它的内在状态是“纸币”,外在状态将是 moneyValue(货币面额)。

public class MoneyFactory
{
   public static int ObjectsCount=0;
   private Dictionary<enmoneytype,imoney> _moneyObjects;
   public IMoney GetMoneyToDisplay(EnMoneyType form) // Same as GetFlyWeight()
   {
      if (_moneyObjects == null)
           _moneyObjects = new Dictionary<enmoneytype,>();
      if (_moneyObjects.ContainsKey(form))
            return _moneyObjects[form];
      switch (form)
      {
          case EnMoneyType.Metallic:
              _moneyObjects.Add(form, new MetallicMoney());
               ObjectsCount++;
               break;
          case EnMoneyType.Paper:
              _moneyObjects.Add(form, new PaperMoney());
               ObjectsCount++;
               break;
          default:
               break;
      }
      return _moneyObjects[form];
   }
}

所以这个“MoneyFactory”类就是我们的享元工厂,它负责管理享元的创建,并确保如果对象已存在于字典中,则返回该对象的实例,否则则创建新对象。上面的类还通过 ObjectsCount 变量跟踪创建的对象数量。

现在我们来看看将要使用享元模式的客户端。

class Program
{
   static void Main(string[] args)
   {
     const int ONE_MILLION=10000; // <--- Suppose this is one million :)
     int[] currencyDenominations = new[] { 1, 5, 10, 20, 50, 100 };
     MoneyFactory moneyFactory = new MoneyFactory();
     int sum = 0;
     while (sum <= ONE_MILLION)
     {
       IMoney graphicalMoneyObj=null;
       Random rand = new Random();
       int currencyDisplayValue=currencyDenominations[rand.Next(0,currencyDenominations
                                                      .Length)];
       if (currencyDisplayValue == 1 || currencyDisplayValue==5)
          graphicalMoneyObj=moneyFactory.GetMoneyToDisplay(EnMoneyType.Metallic);
       else
          graphicalMoneyObj=moneyFactory.GetMoneyToDisplay(EnMoneyType.Paper);
                
       graphicalMoneyObj.GetDisplayOfMoneyFalling(currencyDisplayValue);
       sum = sum + currencyDisplayValue;
     }
     Console.WriteLine("Total Objects created="+ MoneyFactory.ObjectsCount.ToString());
            Console.ReadLine();
        }
}

客户端获取享元实例并显示结果(金钱从天而降的图形化结果——暂时忽略实际实现)。在这里,我们循环到从天而降的金钱数量达到一百万。我们生成一些随机的货币面额,以掉落随机面额的金钱。如果货币面额是 1 或 5,我们就掉落金属货币,否则我们就掉落纸币,这样我们的游戏就完成了……。

程序的输出将是这样的。

创建的总对象数量将是

关注点

因此,从上面的代码我们可以看出,享元模式在需要创建许多相似类型的对象,且这些对象只有细微差别的情况下非常有用。享元模式将为我们节省内存空间,并让我们编写处理较少对象的代码。

历史

版本 1.0 (2014/07/06)

© . All rights reserved.