享元模式——结构型模式(6)

前言

上一篇,我们详细讲解了外观模式,回顾一下其主要目的是”封装交互,简化调用“,通过提供一个高层接口,隔离了外部系统与子系统间复杂的交互过程,使得复杂系统的子系统更易使用,从软件学的角度上来说就是将两者进行解耦,松散耦合。此外,我们也可以通过实现不同的外观对象,来完成不同高层接口的实现过程,供外部系统选择调用。对外观模式的回顾我们就是说这么多,详细的介绍大家可以参考上一篇博文,那里有更为全面深刻的叙述。今天,我们将重点讲述另一种结构型模式——享元模式。

动机

在实际的软件系统开发中,运用面向对象技术可以较好解决抽象性问题,也能获得面向对象给我们带来的种种好处,总结起来就是可维护性可复用性可扩展性灵活性好。通常,运用面向对象技术并不会造成程序性能的影响,但是在某些情况里,生成的对象实例数量过多同时对象粒度相对又小,造成较大的内存压力,继而也就不可避免地影响系统运行时效率呢。面对这样一种场景,如何提出一种怎样的解决之道来应对这种大量细粒度的对象的生成工作呢?说是对象的生成工作,但是从我们当前所谈的模式种类来说,显然不是通过创建型模式来解决这种应用场景,而是从结构上来寻求问题的解决之道。目的很简单,就是需要尽可能地减少细粒度对象实例的数量,换句话来说就是能否通过对象复用的方式来避免对细粒度对象多次创建问题,同时还得保证不影响客户程序使用面向对象技术进行操作的便利性?这就是享元模式带给我们的解决思路,接下来,让我们一起揭开其“神秘面纱”吧!

意图

运用共享技术有效地支持大量细粒度的对象。

结构图

image

  1. 抽象享元(Flyweight)角色:享元接口,通过这个接口可以接收并作用于外部状态。通过这个接口传入外部的状态,在享元对象的方法处理中可能会使用这些外部状态数据。
  2. 具体享元(ConcreteFlyweight)角色:具体的享元对象,必须是共享的,需要封装Flyweight的内部状态。
  3. 非共享的具体享元(UnsharedConcreteFlyweight)角色:非共享的具体享元对象,并不是所有的Flyweight对象都需要共享,非共享的享元对象通常是对共享享元对象的组合对象。
  4. 享元工厂(FlyweightFactory)角色:享元工厂,主要用来创建并管理共享的享元对象,并对外提供访问共享享元对象的接口。
  5. 客户端(Client)角色:享元客户端,主要的工作是维持一个对享元对象的引用,计算或存储享元对象的外部状态,也可以访问共享和非共享的享元对象。

代码示例

 1:  public abstract class Flyweight{
 2:      public abstract void Operation(String extrinsicState);
 3:  }
 4:   
 5:  public class ConcreteFlyweight extends Flyweight{
 6:      private String intrinsicState;
 7:   
 8:      public ConcreteFlyweight(String intrinsicState){
 9:          this.intrinsicState=intrinsicState;
10:      }
11:   
12:      public void Operation(String extrinsicState){
13:          //具体操作需要使用到外部状态相关数据信息
14:          //...
15:      }
16:  }
17:   
18:  public class UnsharedConcreteFlyweight extends Flyweight{
19:      private String allState;
20:   
21:      public UnsharedConcreteFlyweight(String allState){
22:          this.allState=allState;
23:      }
24:   
25:      public void Operation(String extrinsicState){
26:          //具体操作需要使用到外部状态相关数据信息
27:          //...
28:      }
29:  }
30:   
31:  public class FlyweightFactory{
32:      private HashMap<String, Flyweight> flyweights=new HashMap<String, Flyweight>();
33:   
34:      public static Flyweight GetFlyweight(String key){
35:          Flyweight flyweight=flyweights.get(key);
36:          if(flyweight!=null){
37:              return flyweight;
38:          }else{
39:              flyweight=new ConcreteFlyweight(key);
40:              flyweights.put(key, flyweight);
41:          }
42:      }
43:  }
44:   
45:  public class Client{
46:      public static void main(String[] args){
47:          Flyweight flyweight=FlyweightFactory.GetFlyweight("concreteFlyweight1");
48:          flyweight.Operation("test");
49:   
50:          //...
51:          Flyweight flyweight2=FlyweightFactory.GetFlyweight("concreteFlyweight1");
52:          System.out.println(flyweight.equals(flyweight2));
53:          //结果为true,也就证明当需要相同类型对象时,系统返还给我们的是同一对象实例,
54:          //这也达到了避免多次创建相同类型对象的目的呢。        
55:      }
56:  }    

从示例代码上,我们可以基本了解享元模式的实现方式。抽象享元接口Flyweight定义了具体享元对象的共有操作,而享元实现对象(ConcreteFlyweight和UnsharedConcreteFlyweight)则实现了该接口。ConcreteFlyweight对象是可共享的,同时也具有自己的内部信息,而UnsharedConcreteFlyweight是非共享的享元对象,大家需要记住的是,并非所有的享元对象都需要共享,这些非共享的享元对象通过是对共享享元对象的组合对象。通常,可共享的享元对象是叶子对象(还记得组合模式中的叶子对象概念吗?),而非共享的享元对象是由共享对象组合而成的,但由于细粒度的叶子对象都已经被缓存起来(即可以多次重复使用),那么缓存组合对象也就没有什么意义呢。这里需要指出的是,享元对象的状态部分有内部状态和外部状态,其中内部状态指的是包含在享元对象内部的、对象本身的状态信息,是独立于使用享元对象场景的信息,一般创建之后就不会再发生变化,故可以共享;而外部状态是指享元对象之外的状态,取决于使用享元的场景,随使用场景的不同而变化,因此其不可共享,而外部状态通常是通过方法的参数来传递到享元对象中,供其使用。总结一句就是享元对象的内部状态是可缓存和共享的,而外部状态则不能,同时两者应该是独立的,外部状态的改变不应该影响到内部状态,否则内部状态缓存共享也就失去了根本意义呢。而在享元模式中用于完成对享元对象缓存和共享操作的是享元工厂,它只会在第一次接收到获取新享元对象时才会进行真正的享元对象创建操作,之后对相同类型的享元对象索取都只是直接将原有的享元对象传递给客户端而已。当然,在这里,我们完全可以通过创建型模式来完成对享元对象的创建工作,同时,我们也完全可以将享元工厂设计成单例,因为在全局范围内只需要一个享元工厂来应对客户端对不同享元对象的创建要求,如果是在多线程环境下,这种要求会成为一种必须,否则就不能实现完全意义上共享操作呢,具体原因大家可以参考单例模式的对于线程安全一节的介绍。

现实场景

在现实的生活场景中,我们不会遇到很容易抽象成享元模式的例子,但是在我们日常生活中所使用的工具里,在设计实现的过程却有很多运用到享元模式来完成,比如大多数的文档编辑器,都提供文本格式化和编辑功能,这些功能在一定程度上是模块化的。尤其是面向对象的文档编辑器通常使用对象来表示嵌入的成分,例如表格和图形。尽管通过对象来表示文档中的每个字符会极大地提高应用程序的灵活性,但是这些编辑器通常并不会利用这种”铺张“的实现方式。如果文档中的一个字符都对应一个抽象的对象实例的话,那即使一个中等大小的文档也可能要求成百上千的字符对象,极大的耗费内存,产生难以接收的运行开销,当然从实用的角度来说也是完全没有必要的。所以,大多数通过面向对象技术实现的文档编辑器都会运用享元模式来设计实现,即只为字母表中的每个字母创建一个享元对象,每个享元对象存储一个字符代码,而它在文档中的位置和排版几枚在字符出现时由正文排版算法和使用的格式化命令决定。显然,字符代码是享元对象的内部状态,而其它的信息都是其外部状态。这样实现的好处是,逻辑上,文档中的给定字符每次出现都有一个对象实例与之对应,但是在物理上,每一个相同的的字符都共享着同一个享元对象,只是这个对象可以出现在文档中的不同地方。最重要的是,由于不同的字符对象远小于文档中的字符数,因为,对象的总数远小于一个初次执行的程序所使用的对象数目。对于一个所有字符都使用同样的字体和颜色的文档而言,不管这个文档有多长,需要分配100个左右的字符对象(ASCII字符集的数目),同时大多数文档使用的字体颜色组合不超过十种,实际运用中这一数目也不会明显增加。因此,这种实现方案是最节约内存,保证运行时效率的有效途径。【上述描述来设计模式一书总结

接下来,我们再通过一个更为详细具体的例子来说明享元模式的运用。想像一下,如下的场景:在淘宝商城里,我们普通用户只要通过申请注册交保证金就可以成功地经营一网店呢,大家可以想像一下,由淘宝商城提供的网店模板个数无论如何都是有限的,但是现在淘宝商城里所经营的网店个数却是已有网店模板的N多倍呢。现在让我们根据这样一个场景抽象一下,如果将每一个经营的网店都需要通过一个网店对象来表示的话,由于每一种网店模板都会被N多个商家一起采用,那么,很显然我们将需要数量巨大的网店对象实例。但是,由于采用相同一套网店模板的网店除了里面所展示的内容不一样或者使用权限不同一样以外,网店风格样式应该说是几乎完全一致的,除了一些可定制化的功能,比如logo等。所以,如果一个网店对应一个网店对象来表示的话,那么众多的网店对象中重复的内容将是非常可观的,也是完全没有必要的,因为有很大一部分是完全可以在使用同一套模板的网店对象里共享使用的,比如网店的风格、样式等,这些可以共享的部分其实就是享元模式中享元对象的内部状态;而网店所展示的内容由于与网店经营的产品有关,所以这部分基本上不会有相同的情况,也就是根据场景的不同,其内容也会随之变化,自然这些部分就是享元模式中享元对象的外部状态。不知道这样抽象过程大家是否可以接受?但是相信,上述例子基本上已经符合享元模式的运用场景呢。解决上述场景的网店对象数量巨大问题的较好途径便是将采用了同一网店模板的所有网店都用这个网店模板对象来表示,这样,需要的网店对象实例数量就与商城中所提供的网店模板数量保持一致呢,而由于网店模板数量有限,故产生的网店对象实例个数也不会太多,不足以影响系统的运行时效率。接下来,让我们再简化一下上述场景,方便我们通过代码的方式来演绎场景过程吧。假设我们每个网店模板的内部状态只是一个简单的网店模板类型,比如有服装类网店模式和电器类网店模式等,而外部状态只有一个用户凭证,也就是网店经营者信息(现实情况肯定远比这样的假设要来得复杂,这里重点为了讲述享元模式本质,所以请不要太当真哈:))

 1:  public class User{
 2:      private String username;
 3:   
 4:      public User(String usernameString){
 5:          this.username=username;
 6:      }
 7:   
 8:      public String getUsername() {
 9:          return username;
10:      }
11:   
12:  }
13:   
14:  public abstract class WebSite{
15:      public abstract void Use(User user);
16:  }
17:   
18:  public class ConcreteWebSite extends WebSite{
19:      private String webType;
20:   
21:      public ConcreteWebSite(String webType){
22:          this.webType=webType;
23:      }
24:   
25:      public void Use(User user){
26:          System.out.println("网站类型:"+webType+",经营者:"+user.getUsername());
27:      }
28:  }
29:   
30:   
31:   
32:  public  class WebSiteFactory{
33:      private  HashMap<String, WebSite> websites=new HashMap<String, WebSite>();
34:   
35:      public WebSite GetWebSite(String webType){
36:          WebSite webSite=websites.get(webType);
37:          if(webSite!=null){
38:              return webSite;
39:          }else{
40:              webSite=new ConcreteWebSite(webType);
41:              websites.put(webType, webSite);
42:              return webSite;
43:          }
44:      }
45:   
46:      public int getWebSiteNum(){
47:          return websites.size();
48:      }
49:  }
50:   
51:  public class Client{
52:      public static void main(String[] args){
53:          WebSiteFactory webSiteFactory=new WebSiteFactory();
54:   
55:          WebSite webSite1=webSiteFactory.GetWebSite("服装类网店");
56:          webSite1.Use(new User("zjb1"));
57:          WebSite webSite2=webSiteFactory.GetWebSite("服装类网店");
58:          webSite2.Use(new User("zjb2"));
59:          WebSite webSite3=webSiteFactory.GetWebSite("服装类网店");
60:          webSite3.Use(new User("zjb3"));
61:   
62:          WebSite webSite4=webSiteFactory.GetWebSite("电器类网店");
63:          webSite4.Use(new User("zjb4"));
64:          WebSite webSite5=webSiteFactory.GetWebSite("电器类网店");
65:          webSite5.Use(new User("zjb5"));
66:          WebSite webSite6=webSiteFactory.GetWebSite("电器类网店");
67:          webSite6.Use(new User("zjb6"));
68:   
69:          System.out.println("得到所有网店总数为:"+webSiteFactory.getWebSiteNum());
70:          //输出结果为2,说明使用同一网店模板的网店对象个数始终只有一个。这也达到了共享的目的
71:      }
72:  }    

从上述示例代码中,我们可以看到:网店类型作为享元对象的内部状态,而网店经营者用户凭证User作为享元对象的外部状态,前者不会随着外部状态的改变而改变,而后者却能够根据运用场景的不同而发生改变,比如User对象。应该说当前代码片段与上文的代码示例结构上没有区别,想提醒大家是,对于享元工厂,我们完全可以将其单例化,全局只保留一个工厂实例,同时,享元工厂中生成新的具体享元对象操作也完全可以通过创建型模式来完成,封装对享元对象的创建过程,简化享元对象的生成工作。最后,从代码69行的输出来看,尽管同一网店模板有多个使用者,但是每一个网店模板对象实例数只会与使用的网店模板数一致,这样也就较好地达到了共享的目的。对享元模式的举例就说这么多呢,大家可以充分发挥想像力,提出更加合理的运用场景,也欢迎留言交流:)

实现要点

  1. 删除外部状态。享元模式的可用性在很大程序上取决于是否容易识别外部状态并将它从共享对象中删除。同时,如果不同种类的外部状态与共享前对象的数目相同的话,删除外部对象不会降低存储消耗。理想的状况是,外部状态也可以由一个单独的对象结构计算得到,且该结构的存储要求很小,这样才能达到真正节约内存开销,提高运行时效率。
  2. 管理共享对象。由于享元对象是共享的,客户端不能直接对享元对象进行实例化,应该统一通过享元工厂来获得需要的享元对象。而享元工厂主要用于控制对相同类型的享元对象生成工作,工作原理很简单,工厂根据传来的键值返回相应的享元对象,如果不存在,则创建一个新享元对象再返回之,实现的方式可以通过key_value结构来完成。当然,共享也就意味着某种形式的引用计数和垃圾回收,当一个享元对象不再使用时,可以回收它的存储空间,如果享元对象的数目固定或者很小的时候也完全没有进行回收的必要,可以对其永久保存。

运用效果

使用享元模式时,传输、查找和计算外部状态都会引入运行时开销,尤其是当享元对象原先被存储为内部状态时。但是,空间上的节省抵消了这部分开销,共享的享元对象越多,空间节省也就相对越大。存储节约是由几下三个部分因素决定的:

  1. 因为共享,对象实例总数减少的数目。很显然的结果,不解释。
  2. 对象内部状态的平均数目。数目越多,说明可以共享的信息越多,自然越节约空间。
  3. 外部状态是计算的还是存储的。前者比后者相较而言,更节约空间。

总结起来,就是共享的享元对象越多,存储节约也就越多。同时节约量随着共享状态的增多而增大。当对象使用大量的内部状态和外部状态,且外部状态是计算得出而非存储的时候,节约量将达到最大。故我们可以通过两种方法来节约空间:用共享减少内部状态的消耗,用计算时间换取对外部状态的存储。

适用性

  1. 如果一个应用程序使用了大量的细粒度对象,可以使用享元模式来减少对象数量。
  2. 如果由于使用大量的对象,造成很大的存储开销,可以使用享元模式来减少对象数量,并节约内存。
  3. 如果对象的大多数状态都可以转变为外部状态,比如通过计算得到,或是从外部传入等,可以使用享元模式来实现内部状态和外部状态的分离。
  4. 如果不考虑对象的外部状态,可以用相对较少的共享对象取代很多组合对象,可以使用享元模式来共享对象,然后组合对象来使用这些共享对象。

相关模式

  1. 享元模式与单例模式:两者可以组合使用。享元模式中的享元工厂完全可以实现为单例,另外,享元工厂中缓存的享元对象,都是单例实例,可以看成是单例模式的一种变形控制,在享元工厂中来单例享元对象。
  2. 享元模式与组合模式:两者可以组合使用。在享元模式中,存在不需要共享软件的享元实现,这些不需要共享的享元通常是对共享的享元对象的组合对象。换句话来说,就是通过将将两种模式组合使用,可以实现更复杂的对象层次结构。
  3. 享元模式与状态模式:两者可以组合使用。可以使用享元模式来共享状态模式中的状态对象。通常在状态模式中,会存在数量很大的,细粒度的状态对象,而且它们基本上可以重复使用的,都是用来处理某一个固定的状态的,它们需要的数据通常都是由上下文传入,也就是变化部分都被分离出去呢,所以可以用享元模式来实现这些状态对象呢。
  4. 享元模式与策略模式:两者可以组合使用。也可以使用享元来实现策略模式中的策略对象。和状态模式一样,策略模式中也存在大量细粒度的策略对象,它们需要的数据同样也是从上下文传入的,因而可以通过享元模式来实现这些策略对象。

总结

享元模式的本质是:分离和共享。分离的是对象状态中变与不变的部分,共享的是对象中不变的部分。享元模式的关键之处就是在于分离变与不变,把不变的部分作为享元对象的内部状态,把变化的部分作为外部状态,这样享元对象就可以达到共享的目的,从而减少对象数量,节约内存空间。所以,在运用享元模式之时,需要考虑的方面无非就是以下方面:哪些状态需要分离,如何分离,分离后又如何处理,哪些可以共享,哪些不能共享,非共享的信息如何被享元对象使用,如何管理共享的享元对象,外部又如何使用享元对象,是否需要非共享的享元对象等等这些问题。考虑清楚了上述问题,相信运用享元模式也就是水到渠成呢。当然实现的方式可能千变万化,但是万变不离其宗。好呢,对享元模式的讲解就到此为止吧,下一篇我们将介绍最后一种结构型模式——代理模式,敬请期待!

参考资料

  1. 程杰著《大话设计模式》一书
  2. 陈臣等著《研磨设计模式》一书
  3. GOF著《设计模式》一书
  4. Terrylee .Net设计模式系列文章
  5. 吕震宇老师 设计模式系列文章
posted @ 2012-10-13 09:57  JackyBing  阅读(1755)  评论(4编辑  收藏  举报