`
yangyou230
  • 浏览: 1648022 次
文章分类
社区版块
存档分类

第12章 享元模式(Flyweight Pattern)

 
阅读更多

享元模式(Flyweight Pattern)

——.NET设计模式系列之十三

Terrylee,2006年3月

摘要:面向对象的思想很好地解决了抽象性的问题,一般也不会出现性能上的问题。但是在某些情况下,对象的数量可能会太多,从而导致了运行时的代价。那么我们如何去避免大量细粒度的对象,同时又不影响客户程序使用面向对象的方式进行操作?

本文试图通过一个简单的字符处理的例子,运用重构的手段,一步步带你走进Flyweight模式,在这个过程中我们一同思考、探索、权衡,通过比较而得出好的实现方式,而不是给你最终的一个完美解决方案。

主要内容:

1. Flyweight模式解说

2..NET中的Flyweight模式

3.Flyweight模式的实现要点

……

概述

面向对象的思想很好地解决了抽象性的问题,一般也不会出现性能上的问题。但是在某些情况下,对象的数量可能会太多,从而导致了运行时的代价。那么我们如何去避免大量细粒度的对象,同时又不影响客户程序使用面向对象的方式进行操作?

意图

运用共享技术有效地支持大量细粒度的对象。[GOF 《设计模式》]

结构图

图1 Flyweight模式结构图

生活中的例子

享元模式使用共享技术有效地支持大量细粒度的对象。公共交换电话网(PSTN)是享元的一个例子。有一些资源例如拨号音发生器、振铃发生器和拨号接收器是必须由所有用户共享的。当一个用户拿起听筒打电话时,他不需要知道使用了多少资源。对于用户而言所有的事情就是有拨号音,拨打号码,拨通电话。

图2 使用拨号音发生器例子的享元模式对象图

Flyweight模式解说

Flyweight在拳击比赛中指最轻量级,即“蝇量级”,这里翻译为“享元”,可以理解为共享元对象(细粒度对象)的意思。提到Flyweight模式都会一般都会用编辑器例子来说明,这里也不例外,但我会尝试着通过重构来看待Flyweight模式。考虑这样一个字处理软件,它需要处理的对象可能有单个的字符,由字符组成的段落以及整篇文档,根据面向对象的设计思想和Composite模式,不管是字符还是段落,文档都应该作为单个的对象去看待,这里只考虑单个的字符,不考虑段落及文档等对象,于是可以很容易的得到下面的结构图:

图3

示意性实现代码:

//"Charactor"
publicabstractclassCharactor
{
//Fields
protectedchar_symbol;

protectedint_width;

protectedint_height;

protectedint_ascent;

protectedint_descent;

protectedint_pointSize;

//Method
publicabstractvoidDisplay();
}

//"CharactorA"
publicclassCharactorA:Charactor
{
//Constructor
publicCharactorA()
{
this._symbol='A';
this._height=100;
this._width=120;
this._ascent=70;
this._descent=0;
this._pointSize=12;
}

//Method
publicoverridevoidDisplay()
{
Console.WriteLine(this._symbol);
}
}

//"CharactorB"
publicclassCharactorB:Charactor
{
//Constructor
publicCharactorB()
{
this._symbol='B';
this._height=100;
this._width=140;
this._ascent=72;
this._descent=0;
this._pointSize=10;
}

//Method
publicoverridevoidDisplay()
{
Console.WriteLine(this._symbol);
}
}

//"CharactorC"
publicclassCharactorC:Charactor
{
//Constructor
publicCharactorC()
{
this._symbol='C';
this._height=100;
this._width=160;
this._ascent=74;
this._descent=0;
this._pointSize=14;
}

//Method
publicoverridevoidDisplay()
{
Console.WriteLine(this._symbol);
}
}


好了,现在看到的这段代码可以说是很好地符合了面向对象的思想,但是同时我们也为此付出了沉重的代价,那就是性能上的开销,可以想象,在一篇文档中,字符的数量远不止几百个这么简单,可能上千上万,内存中就同时存在了上千上万个Charactor对象,这样的内存开销是可想而知的。进一步分析可以发现,虽然我们需要的Charactor实例非常多,这些实例之间只不过是状态不同而已,也就是说这些实例的状态数量是很少的。所以我们并不需要这么多的独立的Charactor实例,而只需要为每一种Charactor状态创建一个实例,让整个字符处理软件共享这些实例就可以了。看这样一幅示意图:

图4

现在我们看到的A,B,C三个字符是共享的,也就是说如果文档中任何地方需要这三个字符,只需要使用共享的这三个实例就可以了。然而我们发现单纯的这样共享也是有问题的。虽然文档中的用到了很多的A字符,虽然字符的symbol等是相同的,它可以共享;但是它们的pointSize却是不相同的,即字符在文档中中的大小是不相同的,这个状态不可以共享。为解决这个问题,首先我们将不可共享的状态从类里面剔除出去,即去掉pointSize这个状态(只是暂时的J),类结构图如下所示:

图5

示意性实现代码:

//"Charactor"
publicabstractclassCharactor
{
//Fields
protectedchar_symbol;

protectedint_width;

protectedint_height;

protectedint_ascent;

protectedint_descent;

//Method
publicabstractvoidDisplay();
}

//"CharactorA"
publicclassCharactorA:Charactor
{
//Constructor
publicCharactorA()
{
this._symbol='A';
this._height=100;
this._width=120;
this._ascent=70;
this._descent=0;
}

//Method
publicoverridevoidDisplay()
{
Console.WriteLine(this._symbol);
}
}

//"CharactorB"
publicclassCharactorB:Charactor
{
//Constructor
publicCharactorB()
{
this._symbol='B';
this._height=100;
this._width=140;
this._ascent=72;
this._descent=0;
}

//Method
publicoverridevoidDisplay()
{
Console.WriteLine(this._symbol);
}
}

//"CharactorC"
publicclassCharactorC:Charactor
{
//Constructor
publicCharactorC()
{
this._symbol='C';
this._height=100;
this._width=160;
this._ascent=74;
this._descent=0;
}

//Method
publicoverridevoidDisplay()
{
Console.WriteLine(this._symbol);
}
}


好,现在类里面剩下的状态都可以共享了,下面我们要做的工作就是控制Charactor类的创建过程,即如果已经存在了“A”字符这样的实例,就不需要再创建,直接返回实例;如果没有,则创建一个新的实例。如果把这项工作交给Charactor类,即Charactor类在负责它自身职责的同时也要负责管理Charactor实例的管理工作,这在一定程度上有可能违背类的单一职责原则,因此,需要一个单独的类来做这项工作,引入CharactorFactory类,结构图如下:

图6

示意性实现代码:

//"CharactorFactory"
publicclassCharactorFactory
{
//Fields
privateHashtablecharactors=newHashtable();

//Constructor
publicCharactorFactory()
{
charactors.Add("A",newCharactorA());
charactors.Add("B",newCharactorB());
charactors.Add("C",newCharactorC());
}

//Method
publicCharactorGetCharactor(stringkey)
{
Charactorcharactor=charactors[key]asCharactor;

if(charactor==null)
{
switch(key)
{
case"A":charactor=newCharactorA();break;
case"B":charactor=newCharactorB();break;
case"C":charactor=newCharactorC();break;
//
}
charactors.Add(key,charactor);
}
returncharactor;
}
}


到这里已经完全解决了可以共享的状态(这里很丑陋的一个地方是出现了switch语句,但这可以通过别的办法消除,为了简单期间我们先保持这种写法)。下面的工作就是处理刚才被我们剔除出去的那些不可共享的状态,因为虽然将那些状态移除了,但是Charactor对象仍然需要这些状态,被我们剥离后这些对象根本就无法工作,所以需要将这些状态外部化。首先会想到一种比较简单的解决方案就是对于不能共享的那些状态,不需要去在Charactor类中设置,而直接在客户程序代码中进行设置,类结构图如下:

图7

示意性实现代码:

publicclassProgram
{
publicstaticvoidMain()
{
Charactorca=newCharactorA();
Charactorcb=newCharactorB();
Charactorcc=newCharactorC();

//显示字符

//设置字符的大小ChangeSize();
}

publicvoidChangeSize()
{
//在这里设置字符的大小
}
}


按照这样的实现思路,可以发现如果有多个客户端程序使用的话,会出现大量的重复性的逻辑,用重构的术语来说是出现了代码的坏味道,不利于代码的复用和维护;另外把这些状态和行为移到客户程序里面破坏了封装性的原则。再次转变我们的实现思路,可以确定的是这些状态仍然属于Charactor对象,所以它还是应该出现在Charactor类中,对于不同的状态可以采取在客户程序中通过参数化的方式传入。类结构图如下:

图8

示意性实现代码:

//"Charactor"
publicabstractclassCharactor
{
//Fields
protectedchar_symbol;

protectedint_width;

protectedint_height;

protectedint_ascent;

protectedint_descent;

protectedint_pointSize;

//Method
publicabstractvoidSetPointSize(intsize);
publicabstractvoidDisplay();
}

//"CharactorA"
publicclassCharactorA:Charactor
{
//Constructor
publicCharactorA()
{
this._symbol='A';
this._height=100;
this._width=120;
this._ascent=70;
this._descent=0;
}

//Method
publicoverridevoidSetPointSize(intsize)
{
this._pointSize=size;
}

publicoverridevoidDisplay()
{
Console.WriteLine(this._symbol+
"pointsize:"+this._pointSize);
}
}

//"CharactorB"
publicclassCharactorB:Charactor
{
//Constructor
publicCharactorB()
{
this._symbol='B';
this._height=100;
this._width=140;
this._ascent=72;
this._descent=0;
}

//Method
publicoverridevoidSetPointSize(intsize)
{
this._pointSize=size;
}

publicoverridevoidDisplay()
{
Console.WriteLine(this._symbol+
"pointsize:"+this._pointSize);
}
}

//"CharactorC"
publicclassCharactorC:Charactor
{
//Constructor
publicCharactorC()
{
this._symbol='C';
this._height=100;
this._width=160;
this._ascent=74;
this._descent=0;
}

//Method
publicoverridevoidSetPointSize(intsize)
{
this._pointSize=size;
}

publicoverridevoidDisplay()
{
Console.WriteLine(this._symbol+
"pointsize:"+this._pointSize);
}
}

//"CharactorFactory"
publicclassCharactorFactory
{
//Fields
privateHashtablecharactors=newHashtable();

//Constructor
publicCharactorFactory()
{
charactors.Add("A",newCharactorA());
charactors.Add("B",newCharactorB());
charactors.Add("C",newCharactorC());
}

//Method
publicCharactorGetCharactor(stringkey)
{
Charactorcharactor=charactors[key]asCharactor;

if(charactor==null)
{
switch(key)
{
case"A":charactor=newCharactorA();break;
case"B":charactor=newCharactorB();break;
case"C":charactor=newCharactorC();break;
//
}
charactors.Add(key,charactor);
}
returncharactor;
}
}

publicclassProgram
{
publicstaticvoidMain()
{
CharactorFactoryfactory=newCharactorFactory();

//Charactor"A"
CharactorAca=(CharactorA)factory.GetCharactor("A");
ca.SetPointSize(12);
ca.Display();

//Charactor"B"
CharactorBcb=(CharactorB)factory.GetCharactor("B");
ca.SetPointSize(10);
ca.Display();

//Charactor"C"
CharactorCcc=(CharactorC)factory.GetCharactor("C");
ca.SetPointSize(14);
ca.Display();
}
}


可以看到这样的实现明显优于第一种实现思路。好了,到这里我们就到到了通过Flyweight模式实现了优化资源的这样一个目的。在这个过程中,还有如下几点需要说明:

1.引入CharactorFactory是个关键,在这里创建对象已经不是new一个Charactor对象那么简单,而必须用工厂方法封装起来。

2.在这个例子中把Charactor对象作为Flyweight对象是否准确值的考虑,这里只是为了说明Flyweight模式,至于在实际应用中,哪些对象需要作为Flyweight对象是要经过很好的计算得知,而绝不是凭空臆想。

3.区分内外部状态很重要,这是享元对象能做到享元的关键所在。

到这里,其实我们的讨论还没有结束。有人可能会提出如下问题,享元对象(Charactor)在这个系统中相对于每一个内部状态而言它是唯一的,这跟单件模式有什么区别呢?这个问题已经很好回答了,那就是单件类是不能直接被实例化的,而享元类是可以被实例化的。事实上在这里面真正被设计为单件的应该是享元工厂(不是享元)类,因为如果创建很多个享元工厂的实例,那我们所做的一切努力都是白费的,并没有减少对象的个数。修改后的类结构图如下:

图9

示意性实现代码:

//"CharactorFactory"
publicclassCharactorFactory
{
//Fields
privateHashtablecharactors=newHashtable();

privateCharactorFactoryinstance;
//Constructor
privateCharactorFactory()
{
charactors.Add("A",newCharactorA());
charactors.Add("B",newCharactorB());
charactors.Add("C",newCharactorC());
}

//Property
publicCharactorFactoryInstance
{
get
{
if(instance!=null)
{
instance=newCharactorFactory();
}
returninstance;
}
}

//Method
publicCharactorGetCharactor(stringkey)
{
Charactorcharactor=charactors[key]asCharactor;

if(charactor==null)
{
switch(key)
{
case"A":charactor=newCharactorA();break;
case"B":charactor=newCharactorB();break;
case"C":charactor=newCharactorC();break;
//
}
charactors.Add(key,charactor);
}
returncharactor;
}
}


.NET框架中的Flyweight

Flyweight更多时候的时候一种底层的设计模式,在我们的实际应用程序中使用的并不是很多。在.NET中的String类型其实就是运用了Flyweight模式。可以想象,如果每次执行string s1 = “abcd”操作,都创建一个新的字符串对象的话,内存的开销会很大。所以.NET中如果第一次创建了这样的一个字符串对象s1,下次再创建相同的字符串s2时只是把它的引用指向“abcd”,这样就实现了“abcd”在内存中的共享。可以通过下面一个简单的程序来演示s1和s2的引用是否一致:

publicclassProgram
{
publicstaticvoidMain(string[]args)
{
strings1="abcd";
strings2="abcd";

Console.WriteLine(Object.ReferenceEquals(s1,s2));

Console.ReadLine();
}
}


可以看到,输出的结果为True。但是大家要注意的是如果再有一个字符串s3,它的初始值为“ab”,再对它进行操作s3 = s3 + “cd”,这时虽然s1和s3的值相同,但是它们的引用是不同的。关于String的详细情况大家可以参考SDK,这里不再讨论了。

效果及实现要点

1.面向对象很好的解决了抽象性的问题,但是作为一个运行在机器中的程序实体,我们需要考虑对象的代价问题。Flyweight设计模式主要解决面向对象的代价问题,一般不触及面向对象的抽象性问题。

2.Flyweight采用对象共享的做法来降低系统中对象的个数,从而降低细粒度对象给系统带来的内存压力。在具体实现方面,要注意对象状态的处理。

3.享元模式的优点在于它大幅度地降低内存中对象的数量。但是,它做到这一点所付出的代价也是很高的:享元模式使得系统更加复杂。为了使对象可以共享,需要将一些状态外部化,这使得程序的逻辑复杂化。另外它将享元对象的状态外部化,而读取外部状态使得运行时间稍微变长。

适用性

当以下所有的条件都满足时,可以考虑使用享元模式:

1、 一个系统有大量的对象。

2、 这些对象耗费大量的内存。

3、 这些对象的状态中的大部分都可以外部化。

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

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

满足以上的这些条件的系统可以使用享元对象。最后,使用享元模式需要维护一个记录了系统已有的所有享元的表,而这需要耗费资源。因此,应当在有足够多的享元实例可供共享时才值得使用享元模式。

总结

Flyweight模式解决的是由于大量的细粒度对象所造成的内存开销的问题,它在实际的开发中并不常用,但是作为底层的提升性能的一种手段却很有效。

参考资料

Erich Gamma等,《设计模式:可复用面向对象软件的基础》,机械工业出版社

Robert C.Martin,《敏捷软件开发:原则、模式与实践》,清华大学出版社

阎宏,《Java与模式》,电子工业出版社

Alan Shalloway James R. Trott,《Design Patterns Explained》,中国电力出版社

MSDN WebCast 《C#面向对象设计模式纵横谈(12):Flyweight享元模式(结构型模式)》

http://www.dofactory.com/

作者:TerryLee
出处:http://terrylee.cnblogs.com
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。

分享到:
评论

相关推荐

    24个设计模式与6大设计原则

    第 23 章 亨元模式【FLYWEIGHT PATTERN】 287 第 24 章 备忘录模式【MEMENTO PATTERN】 288 第 25 章 模式大PK 289 第 26 章 六大设计原则 290 26.1 单一职责原则【SINGLE RESPONSIBILITY ...

    [源代码] 《易学 设计模式》 随书源代码

    第15章 源源不断:享元模式 (Flyweight) 第16章 按部就班:模板方法模式 (TemplateMethod) 第17章 风吹草动:观察者模式 (Observer) 第18章 变化多端:状态模式 (State) 第19章 明修栈道,暗度陈仓:策略模式 ...

    编程机制探析

    第十二章 Iterator Pattern 第十三章 动态类型 第十四章 关于方法表的那些事 第十五章 递归 第十六章 树形递归 第十七章 函数式编程 第十八章 函数式语法 第十九章 函数 = 数据 = 类型 ? 第二十章 流程控制 第二十...

    Java设计模式

    Java设计模式 ...第 23 章 亨元模式【FLYWEIGHT PATTERN】 ............................................................................................. 258 第 24 章 备忘录模式【MEMENTO PATTERN

    design patterns elements of reusable object-oriented software

    ★第12章介绍了如何将两个以上的设计模式结合起来成为新的设计模式(例如,著名的MVC模式),作者称其为复合设计模式。 ★第13章介绍了如何进一步学习设计模式,如何发掘新的设计模式等主题。 ★附录A介绍了剩下的...

    asp.net知识库

    深入剖析ASP.NET组件设计]一书第三章关于ASP.NET运行原理讲述的补白 asp.net 运行机制初探(httpModule加载) 利用反射来查看对象中的私有变量 关于反射中创建类型实例的两种方法 ASP.Net应用程序的多进程模型 NET委托...

    二十三种设计模式【PDF版】

    设计模式之 Flyweight(共享元) 提供 Java运行性能,降低小而大量重复的类的开销. C. 行为模式 设计模式之 Command(命令) 什么是将行为封装,Command 是最好的说明. 设计模式之 Observer(观察者) 介绍如何使用 ...

    java 面试题 总结

    抽象包括两个方面,一是过程抽象,二是数据抽象。 2.继承: 继承是一种联结类的层次模型,并且允许和鼓励类的重用,它提供了一种明确表述共性的方法。对象的一个新类可以从现有的类中派生,这个过程称为类继承。新类...

    超级有影响力霸气的Java面试题大全文档

    抽象包括两个方面,一是过程抽象,二是数据抽象。 2.继承:  继承是一种联结类的层次模型,并且允许和鼓励类的重用,它提供了一种明确表述共性的方法。对象的一个新类可以从现有的类中派生,这个过程称为类继承...

    EXTJS总结.txt

    注:享元模式(Flyweight Design Pattern)是一种节省内存的模式,该模式的大概原理是建立单个全体对象然后不断反复使用它。 3.Ext.getDom var elDom = Ext.getDom('elId'); // 依据id来查dom节点 var elDom1 = ...

Global site tag (gtag.js) - Google Analytics