享元模式浅析
面向对象的思想确实很好地解决了抽象性的问题,以至于在面向对象的眼中,万事万物一切皆对象。不可避免的是,采用面向对象的编程方式,可能会增加一些资源和性能上的开销。不过,在大多数情况下,这种影响还不是太大,所以,它带来的空间和性能上的损耗相对于它的优点而言,基本上不用考虑。但是,在某些特殊情况下,大量细粒度对象的创建、销毁以及存储所造成的资源和性能上的损耗,可能会在系统运行时形成瓶颈。那么我们该如何去避免产生大量的细粒度对象,同时又不影响系统使用面向对象的方式进行操作呢?享元设计模式提供了一个比较好的解决方案。
享元模式(英语:Flyweight Pattern)是一种软件设计模式。它使用共享物件,用来尽可能减少内存使用量以及分享资讯给尽可能多的相似物件;它适合用于当大量物件只是重复因而导致无法令人接受的使用大量内存。通常物件中的部分状态是可以分享。常见做法是把它们放在外部数据结构,当需要使用时再将它们传递给享元。图1.1为享元模式的结构图。
图1.1享元模式结构图
享元模式(Flyweight):运用共享的技术有效地支持大量细粒度的对象。
抽象享元角色(Flyweight):此角色是所有的具体享元类的超类,为这些类规定出需要实现的公共接口或抽象类。那些需要外部状态(External State)的操作可以通过方法的参数传入。抽象享元的接口使得享元变得可能,但是并不强制子类实行共享,因此并非所有的享元对象都是可以共享的。
具体享元(ConcreteFlyweight)角色:实现抽象享元角色所规定的接口。如果有内部状态的话,必须负责为内部状态提供存储空间。享元对象的内部状态必须与对象所处的周围环境无关,从而使得享元对象可以在系统内共享。有时候具体享元角色又叫做单纯具体享元角色,因为复合享元角色是由单纯具体享元角色通过复合而成的。
复合享元(UnsharableFlyweight)角色:复合享元角色所代表的对象是不可以共享的,但是一个复合享元对象可以分解成为多个本身是单纯享元对象的组合。复合享元角色又称作不可共享的享元对象。这个角色一般很少使用。
享元工厂(FlyweightFactoiy)角色:本角色负责创建和管理享元角色。本角色必须保证享元对象可以被系统适当地共享。当一个客户端对象请求一个享元对象的时候,享元工厂角色需要检查系统中是否已经有一个符合要求的享元对象,如果已经有了,享元工厂角色就应当提供这个已有的享元对象;如果系统中没有一个适当的享元对象的话,享元工厂角色就应当创建一个新的合适的享元对象。
客户端(Client)角色:本角色还需要自行存储所有享元对象的外部状态。
内部状态与外部状态:在享元对象内部并且不会随着环境改变而改变的共享部分,可以称之为享元对象的内部状态,反之随着环境改变而改变的,不可共享的状态称之为外部状态。
公共交换电话网的使用方式就是生活中常见的享元模式的例子。公共交换电话网中的一些资源,例如拨号音发生器、振铃发生器和拨号接收器,都是必须由所有用户共享的,不可能为每一个人都配备一套这样的资源,否则公共交换电话网的资源开销也太大了。当一个用户拿起听筒打电话时,他根本不需要知道到底使用了多少资源,对用户而言所有的事情就是有拨号音,拨打号码,拨通电话就行了。所以,就有很有人会共用一套资源,非常节省,这就是享元模式的基本思想。
假如我们要开发一个类似MS Word的字处理软件,下面分析一下将如何来实现。对于这样一个字处理软件,它需要处理的对象既有单个字符,又有由字符组成的段落以及整篇文档,根据面向对象的设计思想,不管是字符、段落还是文档都应该作为单个的对象去看待。我们暂不考虑段落和文档对象,只考虑单个的字符,于是可以很容易的得到下面的结构图:
Java代码:
//抽象的字符类
public abstract class Charactor{
//属性
protected char letter;
protected int fontsize;
//显示方法
public abstract void display();
}
//具体的字符类A
public class CharactorA extends Charactor{
//构造函数
public CharactorA(){
this.letter = 'A';
this.fontsize = 12;
}
//显示方法
public void display(){
try{
System.out.println(this.letter);
}catch(Exception err){
}
}
}
//具体的字符类B
public class CharactorB extends Charactor{
//构造函数
public CharactorB(){
this.letter = 'B';
this.fontsize = 12;
}
//显示方法
public void display(){
try{
System.out.println(this.letter);
}catch(Exception err){
}
}
}
我们的这段代码完全符合面向对象的思想,但是却为此搭上了太多的性能损耗,代价很昂贵。
一篇文档的字符数量很可能达到成千上万,甚至更多,那么在内存中就会同时存在大量的Charactor对象,这时候的内存开销可想而知。
我们对内存中的对象稍加分析就能发现,虽然内存中Character实例很多,但是里面有很多实例差不多是相同的,比如CharactorA类的实例就有可能出现过很多次,这些不同的CharactorA的实例之间只有部分状态不同而已。那么,我们是不是可以只创建一份CharactorA的实例,然后让整个系统共享这个实例呢?直接使用显然是行不通的。比如一份文档中使用了很多的字符A,虽然它们的属性letter相同,都是'A',但是它们的fontsize却不相同的,即字符大小并不相同。显然,对于实例中的相同状态是可以共享的,不同的状态就不能共享了。
为了解决这个问题,我们可以变换一下思路:首先将不可共享的状态从类里面剔除出去,即去掉fontsize这个属性,这时候我们再写一下代码:
Java代码:
//抽象的字符类
public abstract class Charactor{
//属性
protected char letter;
//显示方法
public abstract void display();
}
//具体的字符类A
public class CharactorA extends Charactor{
//构造函数
public CharactorA(){
this.letter = 'A';
}
//显示方法
public void display(){
try{
System.out.println(this.letter);
}catch(Exception err){
}
}
}
//具体的字符类B
public class CharactorB extends Charactor{
//构造函数
public CharactorB(){
this.letter = 'B';
}
//显示方法
public void display(){
try{
System.out.println(this.letter);
}catch(Exception err){
}
}
}
经过这次重构,类里面剩余的状态就可以共享了,下面我们要做的工作就是要控制Charactor类的创建过程。如果已经存在了“A”字符这样的实例,就不需要再创建,直接返回实例;如果没有,则创建一个新的实例,这跟单例模式的做法有点类似了。在单例模式中是由类自身维护一个唯一的实例,享元模式则引入一个单独的工厂类CharactorFactory来完成这项工作:
Java代码:
public class CharactorFactory{
private Hashtable<String,Charactor> charactors = new Hashtable<String,Charactor>();
//构造函数
public CharactorFactory(){
charactors.put("A", new CharactorA());
charactors.put("B", new CharactorB());
}
//获得指定字符实例
public Charactor getCharactor(String key){
Charactor charactor = (Charactor)charactors.get(key);
if (charactor == null){
if(key.equals("A")){
charactor = new CharactorA();
}else if(key.equals("B")){
charactor = new CharactorB();
}
charactors.put(key, charactor);
}
return charactor;
}
}
经过本次重构,已经可以使用同一个实例来存储可共享的状态,下面还需要做的工作就是要处理被剔除出去的那些不可共享的状态。缺少了这些不可共享的状态,Charactor对象就无法正常工作。
那么如何解决对象中不可共享状态的问题呢?
我们把这些不可共享的状态仍然保留在Charactor对象中,把不同的状态通过参数化的方式,由客户程序注入。以下代码是我们最终实现的一个版本:
Java代码:
//抽象的字符类
public abstract class Charactor{
//属性
protected char letter;
protected int fontsize;
//显示方法
public abstract void display();
//设置字体大小
public abstract void setFontSize(int fontsize);
}
//具体的字符类A
public class CharactorA extends Charactor{
//构造函数
public CharactorA(){
this.letter = 'A';
this.fontsize = 12;
}
//显示方法
public void display(){
try{
System.out.println(this.letter);
}catch(Exception err){
}
}
//设置字体大小
public void setFontSize(int fontsize){
this.fontsize = fontsize;
}
}
//具体的字符类B
public class CharactorB extends Charactor{
//构造函数
public CharactorB(){
this.letter = 'B';
this.fontsize = 12;
}
//显示方法
public void display(){
try{
System.out.println(this.letter);
}catch(Exception err){
}
}
//设置字体大小
public void setFontSize(int fontsize){
this.fontsize = fontsize;
}
}
//客户程序
public class ClinetTest{
public static void main(String[] args){
Charactor a = new CharactorA();
Charactor b = new CharactorB();
//设置字符A的大小
a.setFontSize(12);
//显示字符B
a.display();
//设置字符B的大小
b.setFontSize(14);
//显示字符B
b.display();
}
}
本文通过给出享元模式的典型应用例子,来介绍了享元模式的具体应用,但享元模式在一般的开发中并不常用,而是常常应用于系统底层的开发,以便解决系统的性能问题。
适用性
Flyweight模式的有效性很大程度上取决于如何使用它以及在何处使用它。当以下情况都成立时使用Flyweight模式。
1)一个应用程序使用了大量的对象。
2)完全由于使用大量的对象,造成很大的存储开销。
3)对象的大多数状态都可变为外部状态。
4)如果删除对象的外部状态,那么可以用相对较少的共享对象取代很多组对象。
5)应用程序不依赖对象标识。
优缺点
1)享元模式使得系统更加复杂。为了使对象可以共享,需要将一些状态外部化,这使得程序的逻辑复杂化。
2)享元模式将享元对象的状态外部化,而读取外部状态使得运行时间稍微变长。