11.享元模式(Flyweight Pattern)
面向对象的代价
面向对象很好地解决了系统抽象性的问题,同时在大多数情况下,也不会损及系统的性能。但是,在
某些特殊的应用中下,由于对象的数量太大,采用面向对象会给系统带来难以承受的内存开销。比如:
图形应用中的图元等对象、字处理应用中的字符对象等。
动机(Motivate):
采用纯粹对象方案的问题在于大量细粒度的对象会很快充斥在系统中,从而带来很高的运行时代价--------主要指内存需求方面的代价。
如何在避免大量细粒度对象问题的同时,让外部客户程序仍然能够透明地使用面向对象的方式来进行操作?
意图(Intent):
运用共享技术有效地支持大量细粒度的对象。 -------《设计模式》GOF
结构(Struct):
适用性:
当以下所有的条件都满足时,可以考虑使用享元模式:
1、 一个系统有大量的对象。
2、 这些对象耗费大量的内存。
3、 这些对象的状态中的大部分都可以外部化。
4、 这些对象可以按照内蕴状态分成很多的组,当把外蕴对象从对象中剔除时,每一个组都可以仅用一个对象代替。
5、 软件系统不依赖于这些对象的身份,换言之,这些对象可以是不可分辨的。
满足以上的这些条件的系统可以使用享元对象。最后,使用享元模式需要维护一个记录了系统已有的所有享元的表,而这需要耗费资源。因此,应当在有足够多的享元实例可供共享时才值得使用享元模式。
生活中的例子:
享元模式使用共享技术有效地支持大量细粒度的对象。公共交换电话网(PSTN)是享元的一个例子。有一些资源例如拨号音发生器、振铃发生器和拨号接收器是必须由所有用户共享的。当一个用户拿起听筒打电话时,他不需要知道使用了多少资源。对于用户而言所有的事情就是有拨号音,拨打号码,拨通电话。
代码实现:
Flyweight在拳击比赛中指最轻量级,即“蝇量级”,这里翻译为“享元”,可以理解为共享元对象(细粒度对象)的意思。提到Flyweight模式都会一般都会用编辑器例子来说明,这里也不例外,但我会尝试着通过重构来看待Flyweight模式。考虑这样一个字处理软件,它需要处理的对象可能有单个的字符,由字符组成的段落以及整篇文档,根据面向对象的设计思想和Composite模式,不管是字符还是段落,文档都应该作为单个的对象去看待,这里只考虑单个的字符,不考虑段落及文档等对象,于是可以很容易的得到下面的结构图:
/// <summary>
/// "Charactor"
/// </summary>
public abstract class Charactor
{
//Fields
protected char _symbol; protected int _width; protected int _height; protected int _ascent; protected int _descent; protected int _pointSize; //Method public abstract void Display();
}
// "CharactorA"
public class CharactorA : Charactor
{
public CharactorA()
{
this._symbol = 'A'; this._width = ; this._height = ; this._ascent = ; this._descent = ; this._pointSize = ;
}
public override void Display()
{
Console.WriteLine(this._symbol);
}
}
// "CharactorB"
public class CharactorB : Charactor
{
public CharactorB()
{
this._symbol = 'B'; this._width = ; this._height = ; this._ascent = ; this._descent = ; this._pointSize = ;
}
public override void Display()
{
Console.WriteLine(this._symbol);
}
}
// "CharactorC"
public class CharactorC : Charactor
{
public CharactorC()
{
this._symbol = 'C'; this._width = ; this._height = ; this._ascent = ; this._descent = ; this._pointSize = ;
}
public override void Display()
{
Console.WriteLine(this._symbol);
}
}
好了,现在看到的这段代码可以说是很好地符合了面向对象的思想,但是同时我们也为此付出了沉重的代价,那就是性能上的开销,可以想象,在一篇文档中,字符的数量远不止几百个这么简单,可能上千上万,内存中就同时存在了上千上万个Charactor对象,这样的内存开销是可想而知的。进一步分析可以发现,虽然我们需要的Charactor实例非常多,这些实例之间只不过是状态不同而已,也就是说这些实例的状态数量是很少的。所以我们并不需要这么多的独立的Charactor实例,而只需要为每一种Charactor状态创建一个实例,让整个字符处理软件共享这些实例就可以了。看这样一幅示意图:
现在我们看到的A,B,C三个字符是共享的,也就是说如果文档中任何地方需要这三个字符,只需要使用共享的这三个实例就可以了。然而我们发现单纯的这样共享也是有问题的。虽然文档中的用到了很多的A字符,虽然字符的symbol等是相同的,它可以共享;但是它们的pointSize却是不相同的,即字符在文档中中的大小是不相同的,这个状态不可以共享。为解决这个问题,首先我们将不可共享的状态从类里面剔除出去,即去掉pointSize这个状态(只是暂时的J),类结构图如下所示:
/// <summary>
/// "Charactor"
/// </summary>
public abstract class Charactor
{
//Fields
protected char _symbol; protected int _width; protected int _height; protected int _ascent; protected int _descent; //Method public abstract void Display();
}
// "CharactorA"
public class CharactorA : Charactor
{
public CharactorA()
{
this._symbol = 'A'; this._width = ; this._height = ; this._ascent = ; this._descent = ;
}
public override void Display()
{
Console.WriteLine(this._symbol);
}
}
// "CharactorB"
public class CharactorB : Charactor
{
public CharactorB()
{
this._symbol = 'B'; this._width = ; this._height = ; this._ascent = ; this._descent = ;
}
public override void Display()
{
Console.WriteLine(this._symbol);
}
}
// "CharactorC"
public class CharactorC : Charactor
{
public CharactorC()
{
this._symbol = 'C'; this._width = ; this._height = ; this._ascent = ; this._descent = ;
}
public override void Display()
{
Console.WriteLine(this._symbol);
}
}
好,现在类里面剩下的状态都可以共享了,下面我们要做的工作就是控制Charactor类的创建过程,即如果已经存在了“A”字符这样的实例,就不需要再创建,直接返回实例;如果没有,则创建一个新的实例。如果把这项工作交给Charactor类,即Charactor类在负责它自身职责的同时也要负责管理Charactor实例的管理工作,这在一定程度上有可能违背类的单一职责原则,因此,需要一个单独的类来做这项工作,引入CharactorFactory类,结构图如下:
/// <summary>
/// "CharactorFactory"
/// </summary>
public class CharactorFactory
{
// Fields
public Hashtable charactors = new Hashtable(); public CharactorFactory()
{
charactors.Add("A", new CharactorA());
charactors.Add("B", new CharactorB());
charactors.Add("C", new CharactorC());
} //Method
public Charactor GetCharactor(string key)
{
Charactor charactor = charactors[key] as Charactor; if (charactor == null)
{
switch (key)
{
case "A": charactor = new CharactorA(); break;
case "B": charactor = new CharactorB(); break;
case "C": charactor = new CharactorC(); break;
}
charactors.Add(key, charactor);
}
return charactor;
}
}
到这里已经完全解决了可以共享的状态(这里很丑陋的一个地方是出现了switch语句,但这可以通过别的办法消除,为了简单期间我们先保持这种写法)。下面的工作就是处理刚才被我们剔除出去的那些不可共享的状态,因为虽然将那些状态移除了,但是Charactor对象仍然需要这些状态,被我们剥离后这些对象根本就无法工作,所以需要将这些状态外部化。首先会想到一种比较简单的解决方案就是对于不能共享的那些状态,不需要去在Charactor类中设置,而直接在客户程序代码中进行设置,类结构图如下:
class Program
{
static void Main(string[] args)
{
Charactor ca = new CharactorA();
Charactor cb = new CharactorB();
Charactor cc = new CharactorC(); //显示字符
ca.Display(); //设置字符的大小ChangeSize(); Console.ReadLine();
} public void ChangeSize()
{
//在这里设置字符的大小
}
}
按照这样的实现思路,可以发现如果有多个客户端程序使用的话,会出现大量的重复性的逻辑,用重构的术语来说是出现了代码的坏味道,不利于代码的复用和维护;另外把这些状态和行为移到客户程序里面破坏了封装性的原则。再次转变我们的实现思路,可以确定的是这些状态仍然属于Charactor对象,所以它还是应该出现在Charactor类中,对于不同的状态可以采取在客户程序中通过参数化的方式传入。类结构图如下:
/// <summary>
/// "Charactor"
/// </summary>
public abstract class Charactor
{
//Fields
protected char _symbol; protected int _width; protected int _height; protected int _ascent; protected int _descent; protected int _pointSize; //Method
public abstract void SetPointSize(int size); public abstract void Display(); }
// "CharactorA"
public class CharactorA : Charactor
{
public CharactorA()
{
this._symbol = 'A'; this._width = ; this._height = ; this._ascent = ; this._descent = ; }
public override void Display()
{
Console.WriteLine(this._symbol + " pointsize:" + this._pointSize);
} public override void SetPointSize(int size)
{
this._pointSize = size;
}
}
// "CharactorB"
public class CharactorB : Charactor
{
public CharactorB()
{
this._symbol = 'B'; this._width = ; this._height = ; this._ascent = ; this._descent = ; }
public override void Display()
{
Console.WriteLine(this._symbol + "pointsize:" + this._pointSize);
} public override void SetPointSize(int size)
{
this._pointSize = size;
}
}
// "CharactorC"
public class CharactorC : Charactor
{
public CharactorC()
{
this._symbol = 'C'; this._width = ; this._height = ; this._ascent = ; this._descent = ;
}
public override void Display()
{
Console.WriteLine(this._symbol + "pointsize:" + this._pointSize);
} public override void SetPointSize(int size)
{
this._pointSize = size;
}
}
/// <summary>
/// "CharactorFactory"
/// </summary>
public class CharactorFactory
{
// Fields
public Hashtable charactors = new Hashtable(); public CharactorFactory()
{
charactors.Add("A", new CharactorA());
charactors.Add("B", new CharactorB());
charactors.Add("C", new CharactorC());
} //Method
public Charactor GetCharactor(string key)
{
Charactor charactor = charactors[key] as Charactor; if (charactor == null)
{
switch (key)
{
case "A": charactor = new CharactorA(); break;
case "B": charactor = new CharactorB(); break;
case "C": charactor = new CharactorC(); break;
}
charactors.Add(key, charactor);
}
return charactor;
}
}
class Program
{
static void Main(string[] args)
{
CharactorFactory factory = new CharactorFactory(); // Charactor "A"
CharactorA ca = (CharactorA)factory.GetCharactor("A");
ca.SetPointSize();
ca.Display(); // Charactor "B"
CharactorB cb = (CharactorB)factory.GetCharactor("B");
cb.SetPointSize();
cb.Display(); // Charactor "c"
CharactorC cc= (CharactorC)factory.GetCharactor("C");
cc.SetPointSize();
cc.Display(); Console.ReadLine();
}
}
可以看到这样的实现明显优于第一种实现思路。好了,到这里我们就到到了通过Flyweight模式实现了优化资源的这样一个目的。在这个过程中,还有如下几点需要说明:
1.引入CharactorFactory是个关键,在这里创建对象已经不是new一个Charactor对象那么简单,而必须用工厂方法封装起来。
2.在这个例子中把Charactor对象作为Flyweight对象是否准确值的考虑,这里只是为了说明Flyweight模式,至于在实际应用中,哪些对象需要作为Flyweight对象是要经过很好的计算得知,而绝不是凭空臆想。
3.区分内外部状态很重要,这是享元对象能做到享元的关键所在。
到这里,其实我们的讨论还没有结束。有人可能会提出如下问题,享元对象(Charactor) 在这个系统中相对于每一个内部状态而言它是唯一的,这跟单件模式有什么区别呢?这个问题已经很好回答了,那就是单件类是不能直接被实例化的,而享元类是可 以被实例化的。事实上在这里面真正被设计为单件的应该是享元工厂(不是享元)类,因为如果创建很多个享元工厂的实例,那我们所做的一切努力都是白费的,并 没有减少对象的个数。修改后的类结构图如下:
/// <summary>
/// "CharactorFactory"
/// </summary>
public class CharactorFactory
{
//Fields
private Hashtable charactors = new Hashtable(); private static CharactorFactory instance;
public CharactorFactory()
{
charactors.Add("A", new CharactorA());
charactors.Add("B", new CharactorB());
charactors.Add("C", new CharactorC());
} // Property
public static CharactorFactory Instance
{
get
{
if (instance == null)
{
instance = new CharactorFactory();
}
return instance;
}
} // Method
public Charactor GetCharactor(string key)
{
Charactor charactor = charactors[key] as Charactor; if (charactor == null)
{
switch (key)
{
case "A": charactor = new CharactorA(); break;
case "B": charactor = new CharactorB(); break;
case "C": charactor = new CharactorC(); break;
//
}
charactors.Add(key, charactor);
}
return charactor;
}
}
class Program
{
static void Main(string[] args)
{
// Charactor "A"
CharactorA ca = (CharactorA)CharactorFactory.Instance.GetCharactor("A");
ca.SetPointSize();
ca.Display(); // Charactor "B"
CharactorB cb = (CharactorB)CharactorFactory.Instance.GetCharactor("B");
cb.SetPointSize();
cb.Display(); // Charactor "c"
CharactorC cc = (CharactorC)CharactorFactory.Instance.GetCharactor("C");
cc.SetPointSize();
cc.Display(); Console.ReadLine();
}
}
.NET框架中的应用:
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"; Console.WriteLine(Object.ReferenceEquals(s1,s2)); Console.ReadLine();
}
}
Flyweight实现要点:
1.面向对象很好的解决了抽象性的问题,但是作为一个运行在机器中的程序实体,我们需要考虑对象的代价问题。Flyweight设计模式主要解决面向对象的代价问题,一般不触及面向对象的抽象性问题。
2.Flyweight采用对象共享的做法来降低系统中对象的个数,从而降低细粒度对象给系统带来的内存压力。在具体实现方面,要注意对象状态的处理。
3.享元模式的优点在于它大幅度地降低内存中对象的数量。但是,它做到这一点所付出的代价也是很高的:享元模式使得系统更加复杂。为了使对象可以共享,需要将一些状态外部化,这使得程序的逻辑复杂化。另外它将享元对象的状态外部化,而读取外部状态使得运行时间稍微变长。
11.享元模式(Flyweight Pattern)的更多相关文章
- 设计模式-11享元模式(Flyweight Pattern)
1.模式动机 在面向对象程序设计过程中,有时会面临要创建大量相同或相似对象实例的问题.创建那么多的对象将会耗费很多的系统资源,它是系统性能提高的一个瓶颈. 享元模式就是把相同或相似对象的公共部分提取出 ...
- 二十四种设计模式:享元模式(Flyweight Pattern)
享元模式(Flyweight Pattern) 介绍运用共享技术有效地支持大量细粒度的对象. 示例有一个Message实体类,某些对象对它的操作有Insert()和Get()方法,现在要运用共享技术支 ...
- 乐在其中设计模式(C#) - 享元模式(Flyweight Pattern)
原文:乐在其中设计模式(C#) - 享元模式(Flyweight Pattern) [索引页][源码下载] 乐在其中设计模式(C#) - 享元模式(Flyweight Pattern) 作者:weba ...
- Java享元模式(Flyweight Pattern)
享元模式(Flyweight Pattern)主要用于减少创建的对象数量,并减少内存占用并提高性能. 这种类型的设计模式属于结构模式,因为该模式提供了减少对象计数的方法,从而改善应用的对象结构. 享元 ...
- 设计模式系列之享元模式(Flyweight Pattern)——实现对象的复用
说明:设计模式系列文章是读刘伟所著<设计模式的艺术之道(软件开发人员内功修炼之道)>一书的阅读笔记.个人感觉这本书讲的不错,有兴趣推荐读一读.详细内容也可以看看此书作者的博客https:/ ...
- 七个结构模式之享元模式(Flyweight Pattern)
定义: 运用共享技术对大量细粒度对象的复用,这要求这些对象都很相似,状态变化很小.将这些对象的内部状态和外部状态进行区分,对于内部状态相同的只存储一个对象,而对不同的外部状态则采用不同的操作. 结构图 ...
- [设计模式] 11 享元模式 Flyweight
转 http://blog.csdn.net/wuzhekai1985/article/details/6670298 问题 在面向对象系统的设计何实现中,创建对象是最为常见的操作.这里面就有一个问题 ...
- 享元模式(Flyweight Pattern)
定义: 采用一个共享来避免大量拥有相同内容对象的开销.这种开销中最常见.直观的就是内存的损耗.享元模式以共享的方式高效的支持大量的细粒度对象. 享元的英文是flyweight,是一个来自体育方面的专业 ...
- 【UE4 设计模式】享元模式 Flyweight Pattern
概述 描述 运用共享技术有效地支持大量细粒度对象的复用.系统只使用少量的对象,而这些对象都很相似,状态变化很小,可以实现对象的多次复用. 由于享元模式要求能够共享的对象必须是细粒度对象,因此它又称为轻 ...
- 12.享元模式(Flyweight Pattern)
using System; using System.Collections; namespace ConsoleApplication5 { class Program { /// <summ ...
随机推荐
- BZOJ2561最小生成树——最小割
题目描述 给定一个边带正权的连通无向图G=(V,E),其中N=|V|,M=|E|,N个点从1到N依次编号,给定三个正整数u,v,和L (u≠v),假设现在加入一条边权为L的边(u,v),那么需要删掉最 ...
- SpringMVC 拦截器使用说明
spring-content.xml <!-- 配置用于session验证的拦截器 --> <!-- 如果有多个拦截器满足拦截处理的要求,则依据配置的先后顺序来执行 --> & ...
- Markdown文本的学习
大标题 小标题 小小标题 小小小标题 萌啊萌啊萌 斜体 萌啊萌啊萌 粗体 萌啊萌啊萌 又粗又斜 萌啊萌啊萌 我是分割线 p.s. 标准Markdown换行要打两个空格 短代码 #include < ...
- 理解Spark的核心RDD
http://www.infoq.com/cn/articles/spark-core-rdd/
- FZU 2150 Fire Game (bfs+dfs)
Problem Description Fat brother and Maze are playing a kind of special (hentai) game on an N*M board ...
- Servlet -- 中文乱码解决
请求:对于get和post都有效果 request.setCharacterEncoding("UTF-8"); 相应: 设置服务器输出的编码为UTF-8 response.set ...
- socket,tcp,http三者之间的区别和原理
http.TCP/IP协议与socket之间的区别下面的图表试图显示不同的TCP/IP和其他的协议在最初OSI模型中的位置: 7 应用层 例如HTTP.SMTP.SNMP.FTP.Telnet.SIP ...
- Go实战--golang中使用JWT(JSON Web Token)
http://blog.csdn.net/wangshubo1989/article/details/74529333 之前写过关于golang中如何使用cookie的博客: 实战–go中使用cook ...
- 时间复杂度和大O表示法
大O表示法:称一个函数g(n)是O(f(n)),当且仅当存在常数c>0和n0>=1,对一切n>n0均有|g(n)|<=c|f(n)|成立,也称函数g(n)以f(n)为界或者称g ...
- JavaScript深入之词法作用域和动态作用域
作用域 作用域是指程序源代码中定义变量的区域. 作用域规定了如何查找变量,也就是确定当前执行代码对变量的访问权限. JavaScript 采用词法作用域,也就是静态作用域. 静态作用域与动态作用域 因 ...