1.1.9 Composite复合模式
Composite(复合、合成、组合)模式是一种结构型模式,定义:将对象组合成树形结构以表示“部分-整体”的层次结构,它使得客户对单个对象和复合对象的使用具有一致性。
这里的复合对象是很多单个对象的“组合”,而复合对象与单个对象又有共同的特征和操作。我们往往总是希望用一致的方式访问不同类型的对象,不论这个对象是同一类系中类型A的对象,还是类型B的对象,OO的多态性为我们提供了这种支持。Composite模式将这种观点更进一步,当一个复杂对象由多个同一类系中的对象组成的时候,我们仍然希望用与访问单个对象一致的方式来访问该复杂对象(这其实仍是多态性在发挥作用,但在这个多态方法的内部处理使得我们可以做到“用一致的方法访问”这一点)。
下面给出这个模式的结构图:
图9-1 复合模式
如果把Composite模式看成是树形结构的话,那么它主要角色有:
(1) 树角色(Component):代表复合对象和单个对象共有特征的抽象,该角色是一个抽象类,它有一个对象列表,定义了一些增删对象的操作。
(2) 子树角色(Composite):代表复合对象。树上有很多子树,子树也是树的一种。
(3) 树叶角色(Leaf):单个对象,也就是Component中的操作的单个对象。树叶是只一个结点的树,也是特殊的一种树。
上述类图中的Leaf相当于数据结构Tree的叶子节点,而Composite相当于Tree的子节点。实际应用中,是否与上述类图一样,在基类Component中提供add/remove/getChild等方法应视需求而定,因为有些情况下这些方法对于Leaf而言是没有意义的。
例如做一个影视节目列表的树形结构,要求支持二级分类,不用设计模式很难做好。现在使用Composite模式来解决这个问题,非常简单,别说是二级了,N级都没问题。代码如下:
从这里可以看出组合对象是整体,由很多原子对象组成;原子对象是部分。组合对象和原子对象有共同的特征和操作,把共同部分抽象成component角色。这个例子只是一个简单的模拟,并不通用。在我们的实际应用中,节目的来源(也就是Leaf)基本上都是从数据中读出来放到一个javabean中,我们不可能让这个bean来再来继承我们的(Component),至少绝大部分情况是这样,而且还要有很多操作要实现,如判断一个component是否是单个对象还是一个对象的组合,这个对象是否有子节点(Component),父节点(Component)以及异常处理等等。实现一个树形菜单的通用程序并不是那么容易的事。
Java的AWT中的Component-Container体系是一个很好的Composite模式的例子。Container从Component派生,而Container中又可以包含有多个Component(甚至是Container,因为Container也是Component)。这样通过Component-Container结构的对象组合,形成一个树状的层次结构。
但是,需要注意的是,是否能通过类似add的操作来添加被包容的对象,形成树状结构不是Composite模式的重点。 Composite模式的重点在于,形成特定结构后,是否可以保证用统一的方法,在无需关心各被包容对象的前提下访问该对象,执行某个操作。
因此,虽然可以用Java的Collection构建多种容器类型的树状结构,这是一种Composite,但不是这里所讨论的Composite模式。虽然大家有相同的上层接口Collection,但是,各容器类缺少共同的“某个操作”。对于上面讲的AWT中的Component类系而言,“某个操作”可能是invalidate操作,或者是repaint操作。
在现代OS的文件系统实现中,往往不区分文件/目录,甚至设备,因为对于系统而言,他们没有太多的不同,在这里,目录就相当于类图中的Composite。
Composite模式也有一定的局限性,比如,在处理树的时候,我们往往需要处理三类对象:子树,叶节点和非叶节点。而在Composite模式中对于子树和非叶节点的区分并不明显,而是把他们合成为一个Composite对象了。而且在GOF给出的Composite的模式中,对于添加,删除子节点等属于Composite对象的方法,是放在了Component对象中的,这虽然在实现的时候可以区分开来,但容易造成一些概念上的误解。我们可以提出一个改进了的Composite模式,引入子树对象,从而将子树和非叶节点分开,如下图所示:
图9-2 Composite模式的一种变体
这里将Composite从Component类层次中分离出来,但并没有损害Composite模式的内涵。这样做不一定就会比上面的那个要好,各有不同的应用,不过有时候用这样的方法来处理子树要容易些,概念上也更为清晰。
以下情况使用Composite模式:
1、你想表示对象的部分-整体层次结构(这是基本的Composite的应用)。
2、你希望用户忽略组合对象与单个对象的不同,用户将统一地使用组合结构中的所有对象,在这些对象上执行某个操作(这才是Composite模式带给我们的好处)。
Composite模式具有以下优缺点:
1、定义了包含基本对象和组合对象的类层次结构。基本对象可以被组合成更复杂的组合对象,而这个组合对象又可以被组合,这样不断的递归下去。客户代码中,任何用到基本对象的地方都可以使用组合对象。
2、简化客户代码。客户可以一致地使用组合结构和单个对象。通常用户不知道(也不关心)处理的是一个叶节点还是一个组合组件。这就简化了客户代码,因为在定义组合的那些类中不需要写一些充斥着选择语句的函数。
3、使得更容易增加新类型的组件。新定义的Composite或Leaf子类自动地与已有的结构和客户代码一起工作,客户程序不需因新的Component类而改变。
4、使你的设计变得更加一般化。容易增加新组件也会产生一些问题,那就是很难限制组合中的组件。有时你希望一个组合只能有某些特定的组件。使用Composite时,你不能依赖类型系统施加这些约束,而必须在运行时刻进行检查。
组合模式可以清楚地定义分层次的复杂对象,表示对象的全部或部分层次,使得增加新部件也更容易,因为它让客户忽略了层次的不同性,而它的结构又是动态的,提供了对象管理的灵活接口。组合模式对于树结构的控制有着神奇的功效,例如在人力资源系统的组织架构及ERP系统的BOM设计中,组合模式得到重点应用。
1.1.10 Decorator装饰模式
《设计模式》一书对Decorator是这样描述的:动态地给一个对象添加一些额外的职责。就增加功能来说,Decorator模式比生成子类更为灵活。也就是说,动态地给对象添加一些额外的功能。因此Decorator模式又名包装器(Wrapper)。它的工作原理是:创建一个始于Decorator对象(负责新功能的对象)终止于原对象的一个对象“链”。
继承是对类进行扩展,以提供更多特性的一种基本方法,但是有时候,简单的继承可能不能满足我们的需求。如我们的系统需要提供多种类型的产品:类型A、类型B、...,同时,这些产品需要支持多种特性:特性a、特性b、...。以下是两种可能的实现:
1、继承,分别实现类型Aa、类型Ab、类型Ba、类型Bb、...。这种实现方式在类型的数目和所支持特性的数目众多时会造成“类爆炸”,即会引入太多的类型,并且,这种实现的封装性也很差,造成客户代码编写十分困难,十分不可取。
2、修改各类型实现,在其中包含是否支持特性a、特性b、...的选项,根据客户选择启用各特性。这种实现是典型的MFC实现方法,但是这种实现只适合特性非常稳定的情况,否则,当特性发生增减时,各类型实现都可能需要修改。
因此,虽然类似的实现屡见不鲜,但以上两种实现方式由于对特性的变化或者类型的变化过于敏感,无法满足类型或特性动态变化的设计需求。如果我们可以将类型和特性分别定义,并且根据客户代码的需要动态对类型和特性进行组合,则可以克服上述问题。
正如Decorator(装饰)模式的名字暗示的那样,Decorator模式可以在我们需要为对象添加一些附加的功能/特性时发挥作用,除此之外,更为关键的是Decorator模式研究的是如何以对客户透明的方式动态地给一个对象附加上更多的特性,换言之,客户端并不会觉得对象在装饰前和装饰后有什么不同。Decorator模式可以在不创造更多子类的情况下,将对象的功能加以扩展。Decorator模式使用原来被装饰的类的一个子类的实例,把客户端的调用委派到被装饰类,Decorator模式的关键在于这种扩展是完全透明的。 正因为Decorator模式可以动态扩展decoratee所具有的特性,有人将其称为“动态继承模式”,该模式基于继承,但与静态继承下进行功能扩展不同,这种扩展可以被动态赋予decoratee。
Decorator模式的结构如下图所示:
图10-1 Decorator模式类图
在上面的类图中包括以下组成部分:
1、Component(抽象构件)角色:给出一个抽象接口,以规范要接收附加责任的对象。
2、Concrete Component(具体构件)角色:定义一个将要接收附加责任的类。
3、Decorator(装饰)角色:持有一个Component对象的实例,并定义一个与抽象构件接口一致的接口。
4、Concrete Decorator(具体装饰)角色:负责给构件对象“贴上”附加的责任。
GOF的书中有一个例子。有时候,我们需要为一个对象而不是整个类添加一些新的功能,比如,给一个文本区添加一个滚动条的功能。我们可以使用继承机制来实现这一功能,但是这种方法不够灵活,我们无法控制文本区加滚动条的方式和时机。而且当文本区需要添加更多的功能时,比如边框等,需要创建新的类,而当需要组合使用这些功能时无疑将会引起类的爆炸。
我们可以使用一种更为灵活的方法,就是把文本区嵌入到滚动条中。而这个滚动条的类就相当于对文本区的一个装饰。这个装饰(滚动条)必须与被装饰的组件(文本区)继承自同一个接口,这样,用户就不必关心装饰的实现,因为这对他们来说是透明的。装饰会将用户的请求转发给相应的组件(即调用相关的方法),并可能在转发的前后做一些额外的动作(如添加滚动条)。通过这种方法,我们可以根据组合对文本区嵌套不同的装饰,从而添加任意多的功能。这种动态的对对象添加功能的方法不会引起类的爆炸,也具有了更多的灵活性。
下面是一个具体的例子,我们要为超市的收银台设计一个打印票据的程序,有的需要打印票据的头信息,有的需要打印票据的页脚信息,有的只需要打印票据的内容。如果针对每一种情况都修改一次程序,势必会很麻烦。这时我们可以考虑使用Decorator模式。其结构类图如下:
图10-2 收银台打印票据程序
代码如下:
输出结果:
这里的关键点是抽象的装饰器装饰抽象的构件类,具体的装饰器才真正地给具体的构件对象添加额外的功能。为给票据SalesTicket类添加打印页头和页脚的功能,并没有直接继承,而是使用一个TicketDecorator类来“装饰”票据接口Component,它组合了一个票据对象,并定义一个与原来一致的打印内容的接口(是原来的功能,并不是附加的功能),然后在具体的装饰类Head, Footer中添加额外的功能。 可见装饰器实质是组合原来的对象,再添加这个对象没有的功能,相当于对原来的对象进行了“装饰”。客户端并不会觉得对象在装饰前和装饰后有什么不同,这种扩展是透明的。
实现方式:Decorator类从待修饰类ConcreteComponent的基类Component派生,以便与ConcreteComponent保持相同的接口,同时,在内部将所有函数调用转发给内部包容的ConcreteComponent对象来执行(被包容的ConcreteComponent对象通过Decorator的构造函数传入,而后再附加一些“修饰”,否则,Decorator就徒有虚名了)。
上面例子实现的Decorator模式实质上是对包容的扩展(由于只有一个ConcreteComponent,它看起来很像Proxy模式,但意图不同),以上示例基本阐明了Decorator实现的基本方法:重新定义Decoratee中的接口方法(由于Decorator与Decoratee从相同基类派生,所以这是可能的),在其中添加必要的修饰Decoration,并调用Decoratee的相应方法完成Decoration以外的工作,对于无需修饰的辅助方法,可以直接将方法调用转发给Decoratee。
但是,上述实现同时也告诉了我们Decorator模式存在的一个非常重要的限制。由于Decorator从抽象基类派生,我们必须实现抽象基类的每一个虚方法(即抽象方法),当然也可以直接使用基类的实现(在Java抽象类中,方法可以有具体实现),但这就丢失了ConcreteComponent子类中重载的实现,即失去了特性,这与Decorator模式增加特性的实质相悖。另一方面,当虚方法的数目众多时,全部实现它们将成为一种负担。
要解决这一问题,Decorator可以从ConcreteComponent而不是Component派生,当我们只需要装饰已经实现的一部分方法时,这一招似乎“很管用”,但这有悖于Decorator模式的初衷,因为Decorator模式提出的目的在于修饰一个类系,而不是单个的类。如果单纯的为了修饰单个的类,简单的继承扩展即可解决问题,根本就无需从Decorator的角度来考虑这个问题,而如果面对的是多个类,这种方式使我们必须再次面对“类组合爆炸”的窘境。
幸好以上这个问题对于基于消息/事件的应用中不存在,如在MFC中,进行界面修饰时,可以只需要对特别修饰的消息进行处理,而将所有其它消息转发给待修饰的控件。
这么看来Decorator模式似乎在MFC应用中大有用武之地,但事实并非如此,之所以出现这种局面,大概是因为其一 Decorator模式会使得客户代码变得复杂,我们必须自己创建特性,而不是在Create时直接指定 ,MFC的实现者希望MFC封装类保持与API一样的接口,在“将特性在创建控件时静态指定”与“由用户动态创建并指定”之间,MFC的实现者选择了前者;其二,从上面可以看出, Decorator模式对于小特性的定义比较合适,当特性或类系十分复杂时,Decorator模式很难做到面面俱到。
Java IO库:
Java IO库的设计就是Decorator模式的典范。在IO处理中,Java将数据抽象为流(Stream)。在IO库中,最基本的是InputStream和OutputStream两个分别处理输出和输入的对象(为了叙述简便起见,这儿只涉及字节流,字符流和其完全相似),但是在InputStream和OutputStream中只提供了最简单的流处理方法,只能读入/写出字符,没有缓冲处理,无法处理文件,等等。它们只是提供了最纯粹的抽象,最简单的功能。
如何来添加功能,以处理更为复杂的事情呢?你可能会想到用继承。不错,继承确实可以解决问题,但是继承也带来更大的问题,它对每一个功能,都需要一个子类来实现。比如,我先实现了三个子类,分别用来处理文件,缓冲,和读入/写出数据,但是,如果我需要一个既能处理文件,又具有缓冲功能的类呢?这时候又必须在进行一次继承,重写代码。实际上,仅仅这三种功能的组合,就已经是一个很大的数字,如果再加上其它的功能,组合起来的IO类库,如果只用继承来实现的话,恐怕你真的是要被它折磨疯了。如果用Decorator模式,只需要三个Decorator类,分别代表文件处理,缓冲和数据读写三个功能,在此基础上所衍生的功能,都可以通过添加装饰来完成,而不必需要繁杂的子类继承了。 更为重要的是,比较继机制承而言,Decorator是动态的,可以在运行时添加或者去除附加的功能,因而也就具有比继承机制更大的灵活性。
图10-3 Java IO流的层次
首先来看一段用来创建IO流的代码,以下是代码片段:
这段代码对于使用过JAVA输入输出流的人来说再熟悉不过了,我们使用DataOutputStream封装了一个FileOutputStream。这是一个典型的Decorator模式的使用,FileOutputStream相当于Component,DataOutputStream就是一个Decorator。由于FileOutputStream和DataOutputStream有公共的父类OutputStream,因此对对象的装饰对于用户来说几乎是透明的。
下面就来看看OutputStream及其子类是如何构成Decorator模式的。
Component角色:OutputStream
ConcreteComponent角色:ByteArrayOutputStream, FileOutputStream, PipedOutputStream, ObjectOutputStream
Decorator角色:FilterOutputStream
ConcreteDecorator角色:BufferedOutputStream, DataOutputStream, PrintStream
顶层的OutputStream是一个抽象类,它是所有输出流的公共父类,其源代码片段如下:
它定义了write(int b)的抽象方法。这相当于Decorator模式中的Component类。ByteArrayOutputStream,FileOutputStream ,PipedOutputStream 和ObjectOutputStream类都直接从OutputStream继承,以ByteArrayOutputStream为例,以下是代码片段:
它实现了OutputStream中的write(int b)方法,因此我们可以用来创建输出流的对象,并完成特定格式的输出。它相当于Decorator模式中的ConcreteComponent类。
接着来看一下FilterOutputStream,代码片段如下:
同样,它也是从OutputStream继承。但是,它的构造函数很特别,需要传递一个OutputStream的引用给它,并且它将保存对此对象的引用。而如果没有具体的OutputStream对象存在,我们将无法创建FilterOutputStream。由于out既可以是指向FilterOutputStream类型的引用,也可以是指向ByteArrayOutputStream等具体输出流类的引用,因此使用多层嵌套的方式,我们可以为ByteArrayOutputStream添加多种装饰。这个FilterOutputStream类相当于Decorator模式中的Decorator类,它的write(int b)方法只是将调用转发给了传入的流的write(int b)方法,而没有做更多的处理,因此它本质上没有对流进行装饰,所以继承它的子类必须覆盖此方法,以达到装饰的目的。
BufferedOutputStream 和 DataOutputStream是FilterOutputStream的两个子类,它们相当于Decorator模式中的ConcreteDecorator,并对传入的输出流做了不同的装饰。以BufferedOutputStream类为例,以下是代码片段:
这个类提供了一个缓存机制,等到缓存的容量达到一定的字节数时才写入输出流。首先它继承了FilterOutputStream,并且覆盖了父类的write(int b)方法,在调用输出流写出数据前都会检查缓存是否已满,如果未满,则不写。这样就实现了对输出流对象动态的添加新功能的目的。
了解了OutputStream及其子类的结构原理后,我们可以利用Decorator模式为IO写一个新的输出流,来添加新的功能。这里给出一个新的输出流的例子,它将过滤待输出语句中的空格符号。比如需要输出"java io OutputStream",则过滤后的输出为"javaioOutputStream"。以下为SkipSpaceOutputStream类的代码:
它从FilterOutputStream继承,并且重写了它的write(int b)方法。在write(int b)方法中首先对输入字符进行了检查,如果不是空格,则输出。输出结果: Java中自定义的流的步骤有4步:
1、创建两个分别继承了FilterInputStream和 FilterOutputStream的子类
2、重载read()和write()方法来实现自己想要的功能。
3、可以定义或者重载其它方法来提供附加功能。
4、确定这两个类会被一起使用,因为它们在功能上是对称的。
就这样,你就可以无限的扩展IO的功能了。在java.io包中,不仅OutputStream用到了Decorator设计模式,InputStream,Reader,Writer等都用到了此模式。而作为一个灵活的,可扩展的类库,JDK中使用了大量的设计模式,比如在Swing包中的MVC模式,RMI中的Proxy模式等等。对于JDK中模式的研究不仅能加深对于模式的理解,而且还有利于更透彻的了解类库的结构和组成。
在以下情况下可以考虑使用Decorator模式:
1、在不影响其他对象的情况下,以动态、透明的方式给单个对象添加职责。
2、处理那些可以撤消的职责。(与上面类似,当有这种动态添加撤销的需求时,可以为类添加相应的装饰类成员,但想要撤销装饰时,将该成员设置为NULL即可,同样要支持动态切换也很容易)
3、当不能采用生成子类的方法进行扩充时。一种情况是,可能有大量独立的扩展,为支持每一种组合将产生大量的子类,使得子类数目呈指数增长。另一种情况可能是因为类定义被隐藏,或类定义不能用于生成子类。
Decorator和Adapter的不同在于前者不改变接口而后者则提供新的接口。可以将Decorator视为一个退化的、仅有一个组件的Composite,然而,Decorator的目的在于给对象添加一些额外的职责,而不是对象聚集。
既然Decorator模式如此强大,是不是可以大加推广,大量运用Decorator来替代简单的继承呢?这样,直接通过继承来扩展类的功能就可以退出历史舞台了!实际上这是不可能的,主要原因如下:
1、虽然“继承破坏了封装性”(父类向子类开放了过多的权限),但是,继承关系是客观世界及OOP中最基本的关系,而且,继承是深化接口规范的基础,没有继承就没有多态等诸多OO特性,因此,继承比Decorator更常见,也更容易定义和实现;
2、由于Decorator动态叠加及不影响decoratee等特性的要求,Decorator很难用于复杂特性的定义;
3、Decorator是一种聚合与继承的结合,应用Decorate模式还存在着其它一些限制,具体将在实现举例部分讨论。
使用装饰模式主要有以下的优点:
1、装饰模式与继承关系的目的都是要扩展对象的功能,但是装饰模式可以提供比继承更多的灵活性。
2、通过使用不同的具体装饰类以及这些装饰类的排列组合,设计师可以创造出很多不同行为的组合。
使用装饰模式主要有以下的缺点:
由于使用装饰模式,可以比使用继承关系需要较少数目的类,使用较少的类,固然使设计比较易于进行,但是,在另一方面,Decorator的缺点是会产生一些极为类似的小型对象,这些小型对象是为了提供极少量的特殊功能而定制的。
1.1.11 Flyweight享元模式
FlyWeight模式定义:运用共享技术有效地支持大量细粒度对象。
当类的部分属性在整个系统中的多个对象间重复出现时,一个通常的作法是将重复出现的属性从类定义中分离出来,并在多个对象间通过共享来节约系统开销,这种情况在界面相关的应用中尤其常见。如用于浏览目录内容的树,每个节点前面有一个Icon用于表示该节点的类型,如果将该Icon保存在每个节点的数据结构中,无疑是一种巨大的浪费,这时候通过共享(每个节点只需要保存一个所使用Icon的标识即可,在C++中,可以通过引用、指针或ID标识等来实现)可以提高性能,并且当被共享的次数越多时,这种提高就越明显。
Flyweight(享元)模式采用共享来避免大量拥有相同内容对象的开销,这种开销中最常见、最直观的就是内存的损耗,也就是说在一个系统中如果有多个相同的对象,那么只共享一份就可以了,不必每个都去实例化一个对象。Flyweight模式以共享的方式高效地支持大量的细粒度对象。在Flyweight模式中,由于要产生各种各样的对象,所以在Flyweight(享元)模式中常出现Factory模式。Flyweight的内部状态是用来共享的,Flyweight factory负责维护一个对象存储池(Flyweight Pool)来存放内部状态的对象。Flyweight模式是一个提高程序效率和性能的模式,会大大加快程序的运行速度。
存在两种典型的运用Flyweight模式的情形:单纯Flyweight模式和复合Flyweight模式。
单纯Flyweight模式的类图结构如下:
图11-1 单纯Flyweight模式类图
在上面的类图中包括以下组成部分:
1、抽象享元(Flyweight)角色:此角色是所有的具体享元类的超类,为这些类规定出需要实现的公共接口,通过这个接口Flyweight可以接受并作用于外部状态(Extrinsic State)。
2、具体享元(ConcreteFlyweight)角色:实现抽象享元角色所规定的接口。如果有内蕴状态(Intrinsic State)的话,必须负责为内蕴状态提供存储空间。享元对象的内蕴状态必须与对象所处的周围环境无关,从而使得享元对象可以在系统内共享的。
3、享元工厂(FlyweightFactory)角色:本角色负责创建和管理享元角色。本角色必须保证享元对象可以被系统适当地共享。当一个客户端对象调用一个享元对象的时候,享元工厂角色会检查系统中是否已经有一个符合要求的享元对象。如果已经有了,享元工厂角色就应当提供这个已有的享元对象;如果系统中没有一个适当的享元对象的话,享元工厂角色就应当创建一个合适的享元对象。
4、客户端(Client)角色:本角色需要维护一个对所有享元对象的引用。本角色需要自行存储所有享元对象的外蕴状态。
复合Flyweight模式的类图结构如下:
图11-2 复合Flyweight模式类图
在上面的类图中包括以下组成部分:
1、抽象享元角色:为具体享元角色规定了必须实现的方法,那些需要外蕴状态(External State)的操作可以通过方法的参数传入。抽象享元的接口使得享元变得可能,但是并不强制子类实行共享,因此并非所有的享元对象都是可以共享的。有时也可不用抽象享元,而是通过组合方式来复用享元。
2、具体享元角色:实现抽象角色规定的方法。如果存在内蕴状态,就负责为内蕴状态提供存储空间。有时候具体享元角色又叫做单纯具体享元角色,因为复合享元角色是由单纯具体享元角色通过复合而成的。
3、复合享元角色:它所代表的对象是不可以共享的,并且可以分解成为多个单纯享元对象的组合。复合享元角色又称做不可共享的享元对象。这个角色一般很少使用。
4、享元工厂角色:负责创建和管理享元角色。要想达到共享的目的,这个角色的实现是关键!
5、客户端角色:维护对所有享元对象的引用,而且还需要存储对应的外蕴状态。
下面是一个单纯Flyweight模式的例子:
从这个例子中可以看出,当有很多对象有相同的Intrinsic属性,不同的Extrinsc属性时,可用单纯Flyweight模式。 准确划分Intrinsic State和Extrinsic State是应用Flyweight模式的关键,划分时应保证内蕴状态尽可能多,而外蕴状态尽可能少,以充分利用共享减小重复消耗。
单纯Flyweight模式在收到对象创建请求时检查是否该类型对象已存在,若存在,则直接返回该对象,否则,创建新的对象。单纯Flyweight模式的的结构十分简单,其思想与Singleton模式及Simple Factory Pattern也有几分相似之处,但单纯Flyweight模式注重对多个对象(数量不确定)的共享,希望通过这种共享来达到效率或者空间上的节省,而Singleton模式注重对对象创建数目的控制,Simple Factory Pattern则注重对对象创建细节的屏蔽和分离。
复合Flyweight模式是Flyweight模式的一种延伸。当多个单纯Flyweight对象具有相同的Extrinsic属性,而具有不同的Intrinsic属性时,可用复合Flyweight模式。通常外蕴属性写成一个对象,内蕴属性写一个对象,然后通过组合的方式把对象聚合起来。
下面举一个绘图的复合Flyweight例子,通常来讲,图形会包含线型、线宽、颜色等信息,在一个包含大量线条(直线或曲线)的绘图系统中,如果在每一个线条对象中均保存这些信息,无疑是一种巨大的浪费。为此,我们将线条对象的属性进行如下划分:
Intrinsic State:Color, LineWidth, ...
Extrinsic State:Start Point, End Point
这里线条具有相同的外蕴属性:起点和终点,不同的内蕴属性:颜色和线粗。根据以上划分,相应示例的实现如下(Java Code):
作为一个设计良好的程序库,在JDK中存在着一些运用Flyweight模式的例子,如BorderFactory就是一个享元工厂类,下面的例子输出为Yes:
此外,Java中的String和JTree、JTable等也通过共享使用部分公共元素,使得性能得以提升。
在以下情况下可以考虑使用Flyweight模式:
(1) 系统中有大量的对象,他们使系统的效率降低。
(2) 这些对象的状态可以分离出所需要的内外两部分。
GoF的DP一书举出了一个字处理的例子,由于在通常情况下,一篇文档中字符及其字体颜色属性的组合并不会太多(如果是纯文本文件,我认为没有使用Flyweight模式的必要,因为存一个字符与存一个字符的索引的消耗是相当的),根据GoF的统计,一篇包含180000个字符(英文)的文档需要分配的Flyweight的数目大约只有480个。因此,通过保存各字符的索引(通过字符的颜色、大小等信息进行分类,可以对保存的策略进行进一步优化,如DP一书提到采用B-Tree进行存储),而不是实际保存每一个字符以及其大小、颜色信息,可以大大节约实际使用的内存大小。不过话说回来,虽然我没有实际测试过,但是个人认为这种存储策略可能在很多情况下并非最优,对于类似的情况,其它一些处理策略,如采用类似位图行程压缩的方式存放属性变化信息,而将文档内容以纯文本形式存放,在很多情况下可能空间使用效率也非常高,只是可能需要涉及比较复杂的逻辑处理。
我不知道是由于Flyweight模式的名字的原因或者其它什么原因,在通常所能看到的关于Flyweight模式的材料中总是假设被共享的对象很小,我并不同意这种观点。实际上,个人认为, Flyweight模式对于大的对象(可能内存消耗大,也可能创建成本高)更有价值,如连接池/线程池就是共享大的对象的最好的例证,只是由于大的对象往往具有更多的属性,这在一定程度上阻碍了共享的发生。
优缺点:
享元模式优点就在于它能够大幅度的降低内存中对象的数量;而为了做到这一步也带来了它的缺点:使得系统逻辑变得更加复杂,把外蕴状态外部化,通过读取操作来获得,这在一定程度上影响了系统的速度。
同时,外蕴状态和内蕴状态的划分,以及两者关系的对应关系也是必须考虑的因素。只有将内外划分妥当才能使内蕴状态发挥它应有的作用;如果划分失误,可能在空间和时间两个方面都得不偿失。
1.1.12 Proxy代理模式
Proxy模式定义:为其他对象提供一个代理以控制这个对象的访问。
大家都用过代理服务器,代理服务器是从出发点到目的地之间的中间层。而Proxy模式中的Proxy功能上与此类似,是对象的访问者与对象之间的中间层。
Proxy(代理)模式可用于解决在直接访问对象不方便或不符合要求时,为这个对象提供一种代理,以控制对该对象的访问。比如说:要访问的对象在远程的机器上。在面向对象系统中,有些对象由于某些原因(比如对象创建开销很大,或者某些操作需要安全控制,或者需要进程外的访问),直接访问会给使用者或者系统结构带来很多麻烦,我们可以在访问此对象时加上一个对此对象的访问层,这个访问层也叫代理。Proxy模式是最常见的模式,在我们生活中处处可见,例如我们买火车票不一定非要到火车站去买,可以到一些火车票的代售点去买。寄信不一定是自己去寄,可以把信委托给邮局,由邮局把信送到目的地,现实生活中还有很多这样的例子。
Proxy模式结构图如下:
图12-1 代理模式
在上面的类图中,Proxy类是Subject类的子类,但单纯从Proxy的意图上讲,这一约束不是必须的。
举一个比较通俗的例子,一个男孩boy喜欢上了一个女孩girl,男孩一直想认识女孩,直接去和女孩打招呼吧,又觉得不好意思(这个男孩比较害羞)。于是男孩想出了一个办法,委托女孩的室友Proxy去帮他搞定这件事(获得一些关于女孩的信息,如有没有BF等)。下面给出这个例子的程序实现:
乍看,这好像与Decorator模式类似,如果在Proxy中添加了一些对对象的访问进行安全控制之类的功能,就相当于给被代理的对象了添加了装饰。第一,在应用Proxy模式时,我们可能知道目标,这就是上面例子的形式,把目标对象引入到Proxy中,这是静态代理;我们也可能不知道目标,此时被代理的对象由Proxy类创建,这是动态代理。而Decorator模式下我们往往按照访问目标的方式去访问Decorator,即我们总是知道目标,或者说我们更注意的是目标,而不是Decorator。第二, 动态Proxy类可能提供与被代理对象不同的接口,即动态Proxy类不一定要实现Subject接口,而Decorator模式下应保证接口的一致性,以便用户可以用与访问Decoratee一样的方式来访问Decorator。
单纯从结构上讲,Adapter模式与Proxy模式也比较相似,但二者的区别在于意图的不同:Adapter的意图在于接口的转换,而Proxy的意图在于代理(或控制),因此,有人形象地将Proxy模式称为“票贩子模式”,而将Adapter模式称为“外汇买卖模式”。
Proxy这个模式的目的比较笼统,引入Proxy的目的是在Subject和Client之间构建一个中间层,它适用的情况很多。Decorator,Adapter等也是引入中间层,但目的更明确,前者是装饰,后者是适配,因此它们可以看作是Proxy的具体化。
从JDK1.3开始,Java类库特别添加了对Proxy的支持,详见java.lang.reflect.Proxy说明文档。我们可以利用JDK中的Proxy实现来修改上面的例子,以实现动态代理:
Proxy模式中要创建“stub”或“surrogate”对象,它们的目的是接受请求并把请求转发到实际执行工作的其他对象。远程方法调用(RMI)利用Proxy模式,使得在其他JVM中执行的对象就像本地对象一样;企业JavaBeans(EJB)利用Proxy模式添加远程调用、安全性和事务分界;而JAX-RPC Web服务则用Proxy 模式让远程服务表现得像本地对象一样。在每一种情况中,潜在的远程对象的行为是由接口定义的,而接口本质上接受多种实现。调用者(在大多数情况下)不能区分出它们只是持有一个对stub而不是实际对象的引用,因为二者实现了相同的接口;stub的工作是查找实际的对象、封送参数、把参数发送给实际对象、解除封送返回值、把返回值返回给调用者。代理可以用来提供远程控制(就像在RMI、EJB和JAX-RPC中那样),用安全性策略包装对象(EJB)、为昂贵的对象(EJB 实体Bean)提供惰性装入,或者添加检测工具(例如日志记录)。
在5.0以前的JDK中,RMI stub(以及它对等的skeleton)是在编译时由RMI编译器(rmic)生成的类,RMI编译器是JDK工具集的一部分。对于每个远程接口,都会生成一个stub(代理)类,它代表远程对象,还生成一个skeleton对象,它在远程JVM中做与stub相反的工作 —— 解除封送参数并调用实际的对象。类似地,用于Web服务的JAX-RPC工具也为远程Web服务生成代理类,从而使远程Web服务看起来就像本地对象一样。
不管stub类是以源代码还是以字节码生成的,代码生成仍然会向编译过程添加一些额外步骤,而且因为命名相似的类的泛滥,会带来意义模糊的可能性。另一方面,动态代理机制支持在编译时没有生成stub类的情况下,在运行时创建代理对象。 在JDK 5.0及以后版本中,RMI工具使用动态代理代替了生成的 stub,结果RMI变得更容易使用。 许多J2EE容器也使用动态代理来实现EJB。EJB 技术严重地依靠使用拦截(interception)来实现安全性和事务分界;动态代理为接口上调用的所有方法提供了集中的控制流程路径。
Proxy的应用可分成以下几类:
(1) 远程访问代理(可能为了简化客户代码,也可能为了集中管理等);
(2) 重要对象(可能是共享对象或大的,耗资源的对象)访问代理;
(3) 访问控制代理。
在实际应用中,可能出现同时属于以上多种类别的情况。下面是一些可以应用Proxy模式的常见情况:
1、远程(Remote)代理: 为一个位于不同的地址空间的对象提供一个局域代表对象。这个不同的地址空间可以是在本机器中,也可是在另一台机器中,远程代理又叫做大使(Ambassador)。常见的应用如CORBA、DCOM等。
2、虚拟(Virtual)代理: 根据需要创建一个资源消耗较大的对象,使得此对象只在需要时才会被真正创建。如某个Word文档中包含很多较大的图片,需要花费很长时间才能显示出来,那么使用编辑器或浏览器打开这个文档,不能等待大图片处理完成,这时需要做个图片Proxy来代替真正的图片,先在图片的位置显示一个框,然后随着图片逐步被打开,再对显示区域以及内容进行调整,直至显示所需内容。再比如说数据库的显示,客户现在只需要显示1-10条,你放在内存中10000条对客户没有任何意义,这时可以用一个代理,只是取出前10条就可以了,当客户选择显示下10条时再取出接下来的10条数据显示给客户。
3、Copy-on-Write代理: 虚拟代理的一种。把复制(克隆)拖延到只有在客户端需要时,才真正采取行动。这在访问大的对象和防止频繁拷贝造成过多消耗时经常被用到,现代操作系统往往使用这种技术来防止频繁写磁盘,对于我们的普通应用而言,这种技术也被经常使用,当对象未发生修改时,先使用已有的对象,任何一方发生修改时才执行拷贝动作,创建新的对象,以避免在创建对象上过多的消耗(因为我们有可能在整个对象存在期间不会被修改),从而提高处理效率。
4、保护(Protect or Access)代理: 控制对一个对象的访问,如果需要,可以给不同的用户提供不同级别的使用权限。对于硬件等系统资源而言,OS即是一层保护代理。
5、Cache代理: 为某一个目标操作的结果提供临时的存储空间,以便多个客户端可以共享这些结果。
6、防火墙(Firewall)代理: 保护目标,不让恶意用户接近。
7、同步化(Synchronization)代理: 使几个用户能够同时使用一个对象而没有冲突。
8、智能引用(Smart Reference)代理: 当一个对象被引用时,提供一些额外的操作,比如将对此对象调用的次数记录下来等(这类似于Decorator)。
总之,当需要对Client访问目标对象的行为进行控制时,请尽可能不要通过继承来对目标进行深入规范,而是采用Proxy模式在Client和Subject之间建立一个中间层,这将为我们的系统提供更大的可扩展性。