您的位置:首页 > 其它

设计模式:享元模式Flyweight(转)

2009-07-30 15:20 92 查看
面向对象的思想确实很好地解决了抽象性的问题,以至于在面向对象的眼中,万事万物一切皆对象。不可避免的是,采用面向对象的编程方式,可能会增加一些资源和性能上的开销。不过,在大多数情况下,这种影响还不是太大,所以,它带来的空间和性能上的损耗相对于它的优点而言,基本上不用考虑。但是,在某些特殊情况下,大量细粒度对象的创建、销毁以及存储所造成的资源和性能上的损耗,可能会在系统运行时形成瓶颈。那么我们该如何去避免产生大量的细粒度对象,同时又不影响系统使用面向对象的方式进行操作呢?享元设计模式提供了一个比较好的解决方案。

公共交换电话网的使用方式就是生活中常见的享元模式的例子。公共交换电话网中的一些资源,例如拨号音发生器、振铃发生器和拨号接收器,都是必须由所有用户共享的,不可能为每一个人都配备一套这样的资源,否则公共交换电话网的资源开销也太大了。当一个用户拿起听筒打电话时,他根本不需要知道到底使用了多少资源,对用户而言所有的事情就是有拨号音,拨打号码,拨通电话就行了。所以,就有很有人会共用一套资源,非常节省,这就是享元模式的基本思想。

假如我们要开发一个类似MS Word的字处理软件,下面分析一下将如何来实现。对于这样一个字处理软件,它需要处理的对象既有单个字符,又有由字符组成的段落以及整篇文档,根据面向对象的设计思想,不管是字符、段落还是文档都应该作为单个的对象去看待。我们暂不考虑段落和文档对象,只考虑单个的字符,于是可以很容易的得到下面的结构图:

//抽象的字符类
2
3
17//具体的字符类A
18
19
41//具体的字符类B
42
43
65

我们的这段代码完全符合面向对象的思想,但是却为此搭上了太多的性能损耗,代价很昂贵。

一篇文档的字符数量很可能达到成千上万,甚至更多,那么在内存中就会同时存在大量的Charactor对象,这时候的内存开销可想而知。

我们对内存中的对象稍加分析就能发现,虽然内存中Character实例很多,但是里面有很多实例差不多是相同的,比如CharactorA类的实例就有可能出现过很多次,这些不同的CharactorA的实例之间只有部分状态不同而已。那么,我们是不是可以只创建一份CharactorA的实例,然后让整个系统共享这个实例呢?直接使用显然是行不通的。比如一份文档中使用了很多的字符A,虽然它们的属性letter相同,都是'A',但是它们的fontsize却不相同的,即字符大小并不相同。显然,对于实例中的相同状态是可以共享的,不同的状态就不能共享了。

为了解决这个问题,我们可以变换一下思路:首先将不可共享的状态从类里面剔除出去,即去掉fontsize这个属性,这时候我们再写一下代码:

//抽象的字符类
2
3
15//具体的字符类A
16
17
37//具体的字符类B
38
39
59

经过这次重构,类里面剩余的状态就可以共享了,下面我们要做的工作就是要控制Charactor类的创建过程。如果已经存在了“A”字符这样的实例,就不需要再创建,直接返回实例;如果没有,则创建一个新的实例,这跟单例模式的做法有点类似了。在单例模式中是由类自身维护一个唯一的实例,享元模式则引入一个单独的工厂类CharactorFactory来完成这项工作:

Code
1 //抽象的字符类
2
3 public abstract class Charactor{
4
5 //属性
6
7 protected char letter;
8
9 protected int fontsize;
10
11 //显示方法
12
13 public abstract void display();
14
15 //设置字体大小
16
17 public abstract void setFontSize(int fontsize);
18
19 }
20
21 //具体的字符类A
22
23 public class CharactorA : Charactor{
24
25 //构造函数
26
27 public CharactorA(){
28
29 this.letter = 'A';
30
31 this.fontsize = 12;
32
33 }
34
35 //显示方法
36
37 public override void display(){
38
39 Console.WriteLine(this.letter);
40
41 }
42
43 //设置字体大小
44
45 public override void setFontSize(int fontsize){
46
47 this.fontsize = fontsize;
48
49 }
50
51 }
52
53 //具体的字符类B
54
55 public class CharactorB: Charactor{
56
57 //构造函数
58
59 public CharactorB(){
60
61 this.letter = 'B';
62
63 this.fontsize= 14;
64
65 }
66
67 //显示方法
68
69 public override void display(){
70
71 Console.WriteLine(this.letter);
72
73 }
74
75 //设置字体大小
76
77 public override void setFontSize(int fontsize){
78
79 this.fontsize = fontsize;
80
81 }
82
83 }
84
85 //客户程序
86
87 public class ClinetTest{
88
89 public static void Main(String[] args){
90
91 Charactor a = new CharactorA();
92
93 Charactor b = new CharactorB();
94
95 //设置字符A的大小
96
97 a.setFontSize(12);
98
99 //显示字符B
100
101 a.display();
102
103 //设置字符B的大小
104
105 b.setFontSize(14);
106
107 //显示字符B
108
109 b.display();
110
111 }
112
113 }
114
115

可以看到这样的实现明显优于第一种实现思路,这就是享元模式的基本思想。我们通过享元模式实现了节省存储资源的目的。

什么是享元模式
享元的英文是Flyweight,它是一个来自于体育方面的专业用语,在拳击、摔跤和举重比赛中特指最轻量的级别。把这个单词移植到软件工程里面,也是用来表示特别小的对象,即细粒度对象。至于为什么我们把Flyweight翻译为“享元”,可以理解为共享元对象,也就是共享细粒度对象。享元模式就是通过使用共享的方式,达到高效地支持大量的细粒度对象。它的目的就是节省占用的空间资源,从而实现系统性能的改善。
我们把享元对象的所有状态分成两类,其实前面的例子中letter和fontsize属性在运行时,就形成了两类不同的状态。
享元对象的第一类状态称为内蕴状态(Internal State)。它不会随环境改变而改变,存储在享元对象内部,因此内蕴状态是可以共享的,对于任何一个享元对象来讲,它的值是完全相同的。我们例子中Character类的letter属性,它代表的状态就是内蕴状态。

享元对象的第二类状态称为外蕴状态(External State)。它会随环境的改变而改变,因此是不可以共享的状态,对于不同的享元对象来讲,它的值可能是不同的。享元对象的外蕴状态必须由客户端保存,在享元对象被创建之后,需要使用的时候再传入到享元对象内部。我们例子中Character类的fontsize属性,它代表的状态就是外蕴状态。

所以享元的外蕴状态与内蕴状态是两类相互独立的状态,彼此没有关联。
类图:



抽象享元类(Flyweight)

它是所有具体享元类的超类。为这些类规定出需要实现的公共接口,那些需要外蕴状态(Exte的操作可以通过方法的参数传入。抽象享元的接口使得享元变得可能,但是并不强制子类实行共享,因此并非所有的享元对象都是可以共享的。

具体享元类(ConcreteFlyweight)

具体享元类实现了抽象享元类所规定的接口。如果有内蕴状态的话,必须负责为内蕴状态提供存储空间。享元对象的内蕴状态必须与对象所处的周围环境无关,从而使得享元对象可以在系统内共享。有时候具体享元类又称为单纯具体享元类,因为复合享元类是由单纯具体享元角色通过复合而成的。

不能共享的具体享元类(UnsharableFlyweight)

不能共享的享元类,又叫做复合享元类。一个复合享元对象是由多个单享元对象组成,这些组成的对象是可以共享的,但是复合享元类本身并不能共享。

享元工厂类(FlyweightFactoiy)

享元工厂类负责创建和管理享元对象。当一个客户端对象请求一个享元对象的时候,享元工厂需要检查系统中是否已经有一个符合要求的享元对象,如果已经有了,享元工厂角色就应当提供这个已有的享元对象;如果系统中没有适当的享元对象的话,享元工厂角色就应当创建一个新的合适的享元对象。

客户类(Client)

客户类需要自行存储所有享元对象的外蕴状态。

实现和使用享元模式需要注意的问题

面向对象虽然很好地解决了抽象性的问题,但是对于一个实际运行的软件系统,我们还需要考虑面向对象的代价问题,享元模式解决的就是面向对象的代价问题。享元模式采用对象共享的做法来降低系统中对象的个数,从而降低细粒度对象给系统带来的内存压力。

在具体实现方面,我们要注意对象状态的处理,一定要正确地区分对象的内蕴状态和外蕴状态,这是实现享元模式的关键所在。

享元模式的优点在于它大幅度地降低内存中对象的数量。为了做到这一点,享元模式也付出了一定的代价:

1、享元模式为了使对象可以共享,它需要将部分状态外部化,这使得系统的逻辑变得复杂。

2、享元模式将享元对象的部分状态外部化,而读取外部状态使得运行时间会有所加长。

另外,我们还有一个比较关心的问题:到底系统需要满足什么样的条件才能使用享元模式。对于这个问题,我们总结了以下几条:

1、一个系统中存在着大量的细粒度对象;

2、这些细粒度对象耗费了大量的内存。

3、这些细粒度对象的状态中的大部分都可以外部化;

4、这些细粒度对象可以按照内蕴状态分成很多的组,当把外蕴对象从对象中剔除时,每一个组都可以仅用一个对象代替。

5、软件系统不依赖于这些对象的身份,换言之,这些对象可以是不可分辨的。

满足以上的这些条件的系统可以使用享元对象。最后,使用享元模式需要维护一个记录了系统已有的所有享元的哈希表,也称之为对象池,而这也需要耗费一定的资源。因此,应当在有足够多的享元实例可供共享时才值得使用享元模式。如果只能够节省百八十个对象的话,还是没有必要引入享元模式的,毕竟性价比不高。

什么情况下使用享元模式
享元模式在一般的项目开发中并不常用,而是常常应用于系统底层的开发,以便解决系统的性能问题。

Java和.Net中的String类型就是使用了享元模式。如果在Java或者.NET中已经创建了一个字符串对象s1,那么下次再创建相同的字符串s2的时候,系统只是把s2的引用指向s1所引用的具体对象,这就实现了相同字符串在内存中的共享。如果每次执行s1=“abc”操作的时候,都创建一个新的字符串对象的话,那么内存的开销会很大。

如果大家有兴趣的话,可以用下面的程序进行测试,就会知道s1和s2的引用是否一致:

.Net代码:

1 String s1 = "测试字符串1";
2
3 String s2 = "测试字符串1";
4
5 if( Object.ReferenceEquals(s1, s2) ){
6
7 Console.WriteLine("两者一致");
8
9 }
10 else{
11
12 Console.WriteLine("两者不一致");
13
14 }
15

程序运行后,输出的结果为“两者一致”,这说明String类的设计采用了享元模式。如果s1的内容发生了变化,比如执行了s1 += "变化"的语句,那么s1与s2的引用将不再一致。

至于Php作为一种弱类型语言,它的字符串类型是一种基本类型,不是对象。另外,它的执行方式与Java和.Net也有明显区别,每一个脚本文件执行开始,将会装入所有需要的资源;执行结束后,又将占用的资源就立即全部释放,所以它基本上不会产生类似的性能问题,它的字符串处理的设计,自然也使用不到享元模式。

本文来自CSDN博客,转载请标明出处:http://blog.csdn.net/wanghao72214/archive/2009/04/03/4046182.aspx
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: