C# 中的享元设计模式






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