步步为营 .NET 设计模式学习笔记 十七、Flyweight(享元模式)
概述
面向对象的思想很好地解决了抽象性的问题,一般也不会出现性能上的问题。但是在某些情况下,对象的数量可能会太多,从而导致了运行时的代价。那么我们如何去避免大量细粒度的对象,同时又不影响客户程序使用面向对象的方式进行操作?
意图
运用共享技术有效地支持大量细粒度的对象。[GOF 《设计模式》]
结构图
1.单纯享元模式的结构
在单纯享元模式中,所有的享元对象都是可以共享的。单纯享元模式所涉及的角色如下:
抽象享元(Flyweight)角色:此角色是所有的具体享元类的超类,为这些类规定出需要实现的公共接口。那些需要外蕴状态(External State)的操作可以通过调用商业方法以参数形式传入。
具体享元(ConcreteFlyweight)角色:实现抽象享元角色所规定的接口。如果有内蕴状态的话,必须负责为内蕴状态提供存储空间。享元对象的内蕴状态必须与对象所处的周围环境无关,从而使得享元对象可以在系统内共享的。
享元工厂(FlyweightFactory)角色:本角色负责创建和管理享元角色。本角色必须保证享元对象可以被系统适当地共享。当一个客户端对象调用一个享元对象的时候,享元工厂角色会检查系统中是否已经有一个复合要求的享元对象。如果已经有了,享元工厂角色就应当提供这个已有的享元对象;如果系统中没有一个适当的享元对象的话,享元工厂角色就应当创建一个合适的享元对象。
客户端(Client)角色:本角色需要维护一个对所有享元对象的引用。本角色需要自行存储所有享元对象的外蕴状态。
2.复合享元模式的结构
单纯享元模式中,所有的享元对象都可以直接共享。下面考虑一个较为复杂的情况,即将一些单纯享元使用合成模式加以复合,形成复合享元对象。这样的复合享元对象本身不能共享,但是它们可以分解成单纯享元对象,而后者则可以共享。
复合享元模式的类图如下图所示:
享元模式所涉及的角色有抽象享元角色、具体享元角色、复合享元角色、享员工厂角色,以及客户端角色等。
抽象享元角色:此角色是所有的具体享元类的超类,为这些类规定出需要实现的公共接口。那些需要外蕴状态(External State)的操作可以通过方法的参数传入。抽象享元的接口使得享元变得可能,但是并不强制子类实行共享,因此并非所有的享元对象都是可以共享的。
具体享元(ConcreteFlyweight)角色:实现抽象享元角色所规定的接口。如果有内蕴状态的话,必须负责为内蕴状态提供存储空间。享元对象的内蕴状态必须与对象所处的周围环境无关,从而使得享元对象可以在系统内共享。有时候具体享元角色又叫做单纯具体享元角色,因为复合享元角色是由单纯具体享元角色通过复合而成的。
复合享元(UnsharableFlyweight)角色:复合享元角色所代表的对象是不可以共享的,但是一个复合享元对象可以分解成为多个本身是单纯享元对象的组合。复合享元角色又称做不可共享的享元对象。
享元工厂(FlyweightFactoiy)角色:本角色负责创建和管理享元角色。本角色必须保证享元对象可以被系统适当地共享。当一个客户端对象请求一个享元对象的时候,享元工厂角色需要检查系统中是否已经有一个符合要求的享元对象,如果已经有了,享元工厂角色就应当提供这个已有的享元对象;如果系统中没有一个适当的享元对象的话,享元工厂角色就应当创建一个新的合适的享元对象。
客户端(Client)角色:本角色还需要自行存储所有享元对象的外蕴状态。
生活中的例子
享元模式使用共享技术有效地支持大量细粒度的对象。公共交换电话网(PSTN)是享元的一个例子。有一些资源例如拨号音发生器、振铃发生器和拨号接收器是必须由所有用户共享的。当一个用户拿起听筒打电话时,他不需要知道使用了多少资源。对于用户而言所有的事情就是有拨号音,拨打号码,拨通电话。
图2 使用拨号音发生器例子的享元模式对象图
.NET框架中的Flyweight
Flyweight更多时候的时候一种底层的设计模式,在我们的实际应用程序中使用的并不是很多。在.NET中的String类型其实就是运用了Flyweight模式。可以想象,如果每次执行string s1 = “abcd”操作,都创建一个新的字符串对象的话,内存的开销会很大。所以.NET中如果第一次创建了这样的一个字符串对象s1,下次再创建相同的字符串s2时只是把它的引用指向“abcd”,这样就实现了“abcd”在内存中的共享。可以通过下面一个简单的程序来演示s1和s2的引用是否一致:
public class Program { public static void Main(string[] args) { string s1 = "abcd"; string s2 = "abcd"; string s3 = "ab"; string s4 = s3 + "cd"; Console.WriteLine(Object.ReferenceEquals(s1, s2)+"\n"); Console.WriteLine(Object.ReferenceEquals(s1, s4) + "\n"); Console.WriteLine(s1.Equals(s4)); Console.ReadLine(); } }
可以看到,输出的结果为:
True
False
True
示例用例图
一个User用户实例和UserFactory.
代码设计
先创建User.cs:
public class User { private string _UserName; private string _Age; public string UserName { get { return _UserName; } set { _UserName = value; } } public string Age { get { return _Age; } set { _Age = value; } } public User(string userName, string age) { this.UserName = userName; this.Age = age; } }
再创建UserFactory.cs:
public class UserFactory { private Hashtable modelList = new Hashtable(); private static UserFactory _UserInstance; public static UserFactory GetUserInstance() { if (_UserInstance == null) { _UserInstance = new UserFactory(); } return _UserInstance; } public User GetUser(string userName, string Age) { User user = modelList[userName] as User; if (user == null) { modelList.Add(userName, new User(userName, Age)); user = modelList[userName] as User; } return user; } public int GetUserCount() { return modelList.Count; } }
最后调用:
public partial class Run : Form { public Run() { InitializeComponent(); } private void btnRun_Click(object sender, EventArgs e) { //------------------------------------- rtbResult.AppendText(string.Format("现在的内存是{0}.\n", GC.GetTotalMemory(false))); Random random = new Random(); UserFactory userFactory = new UserFactory(); for (int i = 0; i < 100000; i++) { userFactory.GetUser(random.Next(3).ToString(), (i % 20).ToString()); } rtbResult.AppendText(string.Format("创建两个实例的个数是{0},消耗的内存是{1}.", userFactory.GetUserCount(), GC.GetTotalMemory(false))); } }
运行结果如下图:
再看如下调用:
public partial class Run : Form { public Run() { InitializeComponent(); } private void btnRun_Click(object sender, EventArgs e) { //------------------------------------- rtbResult.AppendText(string.Format("现在的内存是{0}.\n", GC.GetTotalMemory(false))); Random random = new Random(); List<Flyweight.User> userlist = new List<Flyweight.User>(); for (int i = 0; i < 100000; i++) { Flyweight.User user = new Flyweight.User(random.Next(3).ToString(), (i % 20).ToString()); userlist.Add(user); } rtbResult.AppendText(string.Format("创建两个实例的个数是{0},消耗的内存是{1}.", userlist.Count, GC.GetTotalMemory(false))); } }
运行结果如下图:
效果及实现要点
1.面向对象很好的解决了抽象性的问题,但是作为一个运行在机器中的程序实体,我们需要考虑对象的代价问题。Flyweight设计模式主要解决面向对象的代价问题,一般不触及面向对象的抽象性问题。
2.Flyweight采用对象共享的做法来降低系统中对象的个数,从而降低细粒度对象给系统带来的内存压力。在具体实现方面,要注意对象状态的处理。
3.享元模式的优点在于它大幅度地降低内存中对象的数量。但是,它做到这一点所付出的代价也是很高的:享元模式使得系统更加复杂。为了使对象可以共享,需要将一些状态外部化,这使得程序的逻辑复杂化。另外它将享元对象的状态外部化,而读取外部状态使得运行时间稍微变长。
4.享元工厂维护一张享元实例表。
5.享元不可共享的状态需要在外部维护。
6.按照需求可以对享元角色进行抽象。
注意事项
1.享元模式通常针对细粒度的对象,如果这些对象比较拥有非常多的独立状态(不可共享的状态),或者对象并不是细粒度的,那么就不适合运用享元模式。维持大量的外蕴状态不但会使逻辑复杂而且并不能节约资源。
2.享元工厂中维护了享元实例的列表,同样也需要占用资源,如果享元占用的资源比较小或者享元的实例不是非常多的话(和列表元素数量差不多),那么就不适合使用享元,关键还是在于权衡得失。
适用性
当以下所有的条件都满足时,可以考虑使用享元模式:
1.一个系统有大量的对象。
2.这些对象耗费大量的内存。
3.这些对象的状态中的大部分都可以外部化。
4.这些对象可以按照内蕴状态分成很多的组,当把外蕴对象从对象中剔除时,每一个组都可以仅用一个对象代替。
5.软件系统不依赖于这些对象的身份,换言之,这些对象可以是不可分辨的。
满足以上的这些条件的系统可以使用享元对象。最后,使用享元模式需要维护一个记录了系统已有的所有享元的表,而这需要耗费资源。因此,应当在有足够多的享元实例可供共享时才值得使用享元模式。
优点
1.享元模式的优点在于它大幅度地降低内存中对象的数量。
缺点
1.享元模式使得系统更加复杂。为了使对象可以共享,需要将一些状态外部化,这使得程序的逻辑复杂化。
2.享元模式将享元对象的状态外部化,而读取外部状态使得运行时间稍微变长。
总结
Flyweight模式解决的是由于大量的细粒度对象所造成的内存开销的问题,它在实际的开发中并不常用,但是作为底层的提升性能的一种手段却很有效。