Design Pattern----Structural Patterns

Structural Patterns(结构模式)主要关注于如何将一些小的类或对象组织成一个大的结构。Structural Patterns分为Structural Class Patterns(结构类模式)和Structural Object Patterns(结构对象模式),Structural Class Patterns用一些面向对象的继承和封装性质将一些接口和对接口的实现组织起来,使得接口的外部调用和接口的内部实现独立;而Structural Object Patterns描述的却是将一些小的对象组合起来实现一些新的功能结构的方法,在这种模式下,使得这些对象的组合变得灵活,甚至可以在运行时决定或者修改组合方式。

Structural Patterns主要分为以下7种模式:Adapter Pattern(适配器模式),Bridge Pattern(桥接模式),Composite Pattern(组合模式),Decorator Pattern(装饰器模式),Facade Pattern(外观模式),Flyweight Pattern(享元模式),Proxy Pattern(代理模式)。


Adapter Pattern:

Adapter Pattern(适配器模式)会将一些类接口转化成使用者所期望的接口,使得一些在接口上不兼容的类相互协调起来。

在开发的过程中可能会遇到一种情况,某个功能如果自己去实现的话可能需要花费大量的时间和精力(甚至自己根本就不会实现),而网上又有现成可用的工具包或者库,这个时候如果用工具包来实现这一功能当然能够事半功倍。工具包开发出来就是方便将来的开发者代码重用,但是有的时候并不能直接重用,因为工具包只提供了一种(或者少量的)形式的接口,而不同的开发所需要用到的接口形式几乎是各不相同,工具包所提供的接口不可能全部对各种开发所需要的接口形式兼容。有两种方法可以解决这个问题:1.修改工具包的接口。2.用Adapter Pattern将工具包的接口转化(不是修改)成所需要用到的形式。使用第一种方法有几点需要注意:1.工具包的接口和所需要用到的接口形式不兼容是正常的,工具包本来就是通用的,而不是适用于某一个特定的开发,所以要改动也是特定开发中使用工具包方式的改动。2.如果要改动工具包的接口形式的话,还需要知道工具包的源代码。3.对工具包的改动可能需要花费大量的工作,还可能出错。因此,通常不会选择直接修改工具包的接口形式,而是用Adapter将这些接口形式间的不兼容转化成兼容。

举个例子,图形编辑器可以让用户绘制和调整图形元素的位置,对于所有图形元素有一个抽象的类Shape,在这个里面有一些基本的抽象接口,例如编辑图形,绘制该图形等。而不同的图形元素可以从抽象类Shape继承,而该图形元素特有的子类,比如直线类LineShape,多边形类PolygonShape等。这些基本的图形元素类还算容易实现,但是如果把文本看作图形元素呢?并且要实现想图形编辑和绘制等功能,相对来说就不是很容易实现了。而这个时候又有一个现成的TextView类库实现了对文本的图形化功能(包括文本图形编辑和绘制等功能),一般来说就会选择使用TextView库去实现所需功能了。而一般来说,TextView又会和Shape所提供的抽象接口不兼容,前面已经讲过,一般不会去改动类库的接口,这时Adapter就派上 用场了。

解决上述接口不兼容问题需要新建一个TextShape将TextView的接口适配到Shape中去,可以用两种方法来实现:1.让TextShape分别继承Shape的接口和TextView的实现。这样及可以在Shape的接口内,通过一定的方式调用TextView中的实现。2.在TextShape中保存一个TextView的实例。同样地,可以在Shape接口内通过这个实例调用TextView的实现。这两种方法分别对应了Adapter Pattern的类版本和对象版本。

Adapter Pattern的类版本具体结构如下:


Adapter Pattern的对象版本具体结构如下:


两者的区别就在于是将被适配接口(Adaptee,类库接口)直接继承到类本身,还是通过保存一个被适配类的实例,通过实例来访问被适配的接口。

在使用Adapter Pattern的时候需要注意两点:

1.在使用Adapter Pattern的类版本时,Adapter应该从Target那公有继承,而应该从Adaptee那私有继承,这样,Adapter是Target的子类而不是Adaptee的子类。

2.Pluggable adapters,大概是说使得适配器更加灵活吧,具体也没有怎么弄懂。以后补上。


Bridge Pattern:

Bridge Pattern(桥接模式)将接口的抽象和接口内部的具体实现分隔开来,使得接口的实现不受接口定义的影响,可以独立地修改。

当一个抽象的接口希望通过多种方式来实现时,一个比较常见的方法是通过继承的方式来实现。抽象接口在基类里面定义,而子类里面可以根据需要给接口内部按照不同的方式实现。但是这种方式有个比较显著的缺陷:接口的扩展、修改不灵活,代码一旦写好,就将接口的定义和实现永久地捆绑起来了。在任何一个子类里面,一旦对接口进行了实现,就永久地把这个接口的定义和实现捆绑了,不可以变动了,除非修改实现代码,但是修改代码以后又重新永久地捆绑起来了。而使用Bridge Pattern就可以非常灵活地解决这个问题,并且使得接口的定义和实现可以在程序运行时进行组合。

举个例子,实现一个可移植的图形化的窗口界面,可以分别在X Windows系统下和IBM的Presentation Manager(PM)系统下显示。如果用继承的方式来实现这一功能,首先需要定义一个抽象的窗口类Window,然后从Window类分别继承得到XWindow和PMWindow子类,最后在各个子类里面实现各自的接口。另外,如果还要添加一些特殊风格的窗口,比如IconWindow,那么这个IconWindow同样需要从Window类继承,而且IconWindow这个类还需要派生两个子类分别对应于两个操作系统的具体子类。之前已经讲过,这种方式不利于扩展和修改,而且每当添加一个新式风格的窗口,就需要写三个类,这也需要较多的工作。而利用Bridge Pattern,同样地需要一个Window基类,这个基类下可以有各种不同的窗口子类,比如IconWindow,TransientWindow等;另外,还需要一个WindowImp基类,用于提供接口的实现,从这个基类派生出两个子类,分别对应于不同的操作系统,两个子类分别根据自己的系统特性实现WindowImp的抽象接口。Window类里面只需保存一个WindowImp的实例,就可以将本类的一些接口的实现交给WindowImp所提供的接口去做。用这种方式,程序的扩展性就好多了。

使用继承方式的结构图大致如下:


使用Bridge Pattern以后,结构图大致如下:


Bridge Pattern的结构图大致如下:



Composite Pattern:

Composite Pattern(组合模式)将对象组合到树形结构中来表示所有对象的层次结构。在这种模式中,层次结构的使用者可以将对象以及多个对象的组合看成是一致的。

在数据结构中会讲到树这种数据结构,树主要由节点和边组成,边代表这节点之间的关系,而节点又分为叶子节点和非叶子节点。叶子节点没有孩子,而非叶子节点有孩子,非叶子节点的孩子可以是叶子节点也可以是非叶子节点。我们可以将上一段讲的对象看成叶子节点,对象的组合看成非叶子节点,对象的组合是有一个或多个对象(或对象的组合)组合而成,就像是非叶子节点有多个叶子节点或非叶子节点作为孩子。Composite Pattern就是要将对象以及对象的组合看成树的节点,使得对象之间的层次关系用树这种数据结构表现出来。

举个例子,在图形编辑器中,用户可以编辑一些基本的图形元素,也可以将这些图形元素组合成一个大的图形,还可以进一步把这些组合成的大的图形组合成更大的图形。在删除的时候,用户可以选择删除一个小的基本图形元素,也可以选择删除整个大的图形。这里面本身就包含着层次逻辑,大的图形包含基本图形元素,更大的图形可以包含大的图形以及基本图形元素。用树这种数据结构可以恰到好处地将这种关系表示出来。简单的实现,可以把基本图形元素和大的图形区分开来,将大的图形看成是一个容器,里面可以包含稍小的图形或者基本图形元素。这样实现的话,树里面包含了两种类型的节点,一种是基本图形元素,一种是元素的容器,这种方式虽然是能够实现这个功能,但是很麻烦。而如果用Composite Pattern来实现这一功能的话,就会将基本图形元素和元素的容器看成是同一类型的元素。首先定义一个基类Graphic,从这个基类可以派生出一些基本图形元素,比如直线类Line,文本类Text等;还可以派生出图形类Picture。而Picture类里面还会保存它的孩子节点,这些孩子节点可以是基本图形元素,也可以是Picture的实例。在这个结构中,Graphic子类之间的区别在于对Graphic提供的一些抽象接口有不同的实现。比如绘制功能Draw,基本图形元素就会直接调用各自的绘制函数将该图形元素绘制出来,而Picture就会将Draw的工作交给自己的孩子去做,调用孩子节点的Draw方法去绘制。具体关系结构如下图所示:


Composite Pattern的大致结构如下图所示:



Decorator Pattern:

Decorator Pattern(装饰器模式)可以动态地给对象(而不是整个类)添加属性或者功能,实现这一功能的常见办法就是通过继承并扩展功能形成新的子类,但是Decorator Pattern能够提供比继承的方式更加灵活的方式去扩展对象的功能。

如果用继承的方式实现对象属性或功能的扩展,当一个对象想添加一个属性的时候,就需要通过继承并扩展功能生成新的子类,而且一旦新的子类形成了,这个属性就永远地绑定在这个类了。如果有n个属性有待添加,但不一定完全要添加,有的对象可能不需要添加任何这n个属性,有的对象需要添加n个属性当中的一些,而有的对象需要添加这n个属性的全部。如果把这些情况都考虑进去的话,则需要通过继承生成2^n个子类,当n=10时,子类的个数就会达到1024个,而且随着n的增长,子类的个数会急剧增长。所以通过继承的方式来实现这个功能首先不够灵活,静态绑定属性;另外当有待添加的属性达到一定数量(其实这个数量非常少)时,就需要实现很多个类,有的时候这一工作甚至是不现实的。那么如果通过Decorator Pattern去实现这一功能的话,对于n个有待添加的属性,只需要实现n个类就行了,而且这些属性还是动态地绑定到对象上。具体地,Decorator Pattern首先会定义一个基类Component,从这个基类可以派生出具体子类ConcreteComponent以及抽象的Decorator类,而Decorator继续作为基类又会派生具体子类ConcreteDecorator。Component的子类分为两类,一类是具体数据类,另一类是用于添加属性或者功能的类。当一个ConcreteComponent被定义时,如果想给它添加一种属性,就把加到一个具体的ConcreteDecorator对象里面生成一个ConcreteDecorator对象,这个对象可以看成具有这个ConcreteDecorator属性的ConcreteComponent对象。而且在调用的时候,由于ConcreteComponent和ConcreteDecorator具有相同的接口,对外部调用是透明的,所以可以不用考虑他们的不同。当调用一个Operator操作时,对于ConcreteComponent就会直接调用自己的Operator进行操作;而对于ConcreteDecorator则不仅会调用加入到自己的Component对象的Operator操作(递归下去),自己还会额外做一些操作,这些操作对应的就是添加属性或者功能。

举一个例子,有图形界面的文本编辑器,最基本的有文本编辑部分,另外也可以有一些其他的属性,比如滚动条、边框等。前面已经说过,使用继承的方式去实现这一功能不够灵活,用Decorator Pattern去实现更加好。如果用Decorator Pattern去实现,则可以把最基本的文本编辑部分看成一个ConcreteComponent类,而滚动条和边框等属性或功能可以看成ConcreteDecorator。如果要生成一个具有边框、滚动条的文本编辑器,则首先需要定义一个基本的文本编辑框对象,加入到滚动条属性对象,再将有滚动条属性的文本编辑器加入到边框属性,就生成了一个有边框、有滚动条的文本编辑器。如果调用Draw(绘制)这个功能边框属性类首先会调用滚动条属性类的Draw功能,然后再绘制边框(或者将绘制边框放在调用滚动条属性类的Draw功能之前实现);然后滚动条类,类似地,调用文本编辑器类的Draw功能,然后再绘制滚动条,……,如此递归下去,就实现整个文本编辑器的绘制工作。调用者并不能区分到底调用了哪个类的Draw功能,这一切都是透明的。这个例子的结构图如下:


Decorator Pattern的大致结构如下:



Facade Pattern:

Facade Pattern(外观接口)为很多子系统的一些接口定义了统一的接口,这是一个更高层次的接口,通过统一接口,使用者可以更加容易地使用子系统的一些接口。

前面所讲过的设计模式都是致力于把一个大的系统不断地细分为很多个子系统、子模块,这样做的确可以使得降低系统的复杂性、降低模块之间的耦合性,但是当子模块很多的时候,对模块的使用就会变得较为麻烦。举个例子,编译器,程序语言分为编译语言(比如常用的C++和Java都是编译语言)和翻译语言(一些脚本语言,如Python,Perl就是解释语言),编译语言程序在运行之前需要先对代码编译。要完成这个工作程序员只需要输入编译指令就可以完成。而编译的整个过程是需要做很多事情的,但这些事情如果让程序员去care的话,将是一个很麻烦的事情。这就是Facade Pattern的一个例子,可以把编译器看作成Facade,提供了一个高层次的统一的接口----Compile(编译),使用者只需要调用Compile这个接口就可以完成程序编译工作。而Compile里面做的一些事情,就是协调编译器的一些子模块工作,这些子模块包括:Stream、Scanner、CodeGenerator等等。

Facade Pattern这个设计模式的思想还比较简单,它的结构大致如下:



Flyweight Pattern:

Flyweight Pattern(享元模式)利用共享机制使得大量的细粒度的对象高效地被使用。

当一个程序中需要用到很多细粒度对象时,简单地,可以在每个需要一个细粒度对象的地方新建一个。但是如果这个程序中需要大量的这种细粒度对象时,则需要占用很多的内存空间。如果使用到的这些细粒度重复性很高时,就可以用Flyweight Pattern来解决这个问题。Flyweight Pattern会将每个可能用到的细粒度对象保存起来,放在一个Pool里面,其他地方需要用到的时候直接从这里通过地址从这个Pool里面获得想要的细粒度对象,不需要新建对象,这样就节省了内存空间。

但是,这样做的话会有个问题,由于Pool里面的细粒度对象是共享的,独立于具体使用的上下文(比如一些其他的需要加到细粒度对象的属性),在从Pool里面取出对象的之后还可能需要计算上下文信息,如果说在这个问题中通过Flyweight Pattern可以节省空间的话,那么计算上下文信息就需要额外的花费时间,这就形成了一个trade-off,看怎么取舍了。通常来讲可以从两方面来衡量:1.通过Flyweight Pattern能够降低多少内存开销。如果说程序需要使用到大量的细粒度对象而且这些对象值的重复性也非常高的时候,用Flyweight Pattern能够很大程度地降低内存开销。2.计算上下文信息所需要花费多少时间。

Flyweight Pattern也分为不可共享的和可以共享的,不可共享的对象里面可以存储可共享对象,一般来说,不可共享的粒度相对较粗。

具体地,文本编辑器,每个字符可以看成一个细粒度对象,但是如果在编辑的时候没编辑一个字符就创建一个对象需要大量的内存空间,如果使用Flyweight Pattern则只需要建一个字符表,显然这个表的大小并不是很大。每次编辑一个字符的时候,就可以从字符表里面去取一个字符对象。

这个模式的结构图如下:


在这个模式中,Client在需要使用细粒度对象时,首先向FlyweightFactory(就是刚才提到的存储细粒度对象的Pool)发送请求,FlyweightFactory在收到请求时,就查看所需要的对象是否已经创建,如果创建了就直接返回,否则先创建再返回。


Proxy Pattern:

Proxy Pattern(代理模式)为其他对象提供一个“代理”,通过这个“代理”控制对其他对象的访问。

在代理模式中,会提供一个基类(Subject),在这个基类的基础上派生出一个真实对象类(RealSubject)和一个对这个真实对象类的代理(Proxy),Proxy中有一个真实对象类(RealSubject)的指针用于指向这个类的对象。由于RealSubject和Proxy公开出来的接口绝大部分都是一样的,所以通过代理Proxy来控制真实对象Realsubject的访问和直接访问真实对象几乎没有什么差异。既然通过代理Proxy访问对象和直接访问对象在形式上没有什么差别,而且还多了一些封装工作,为什么还要用Proxy Pattern呢?Proxy Pattern的第一个优点就是负责对真实对象的创建和删除,如果这些交给程序员去做,很可能会遗漏一些删除工作,而且对对象的创建时间也不一定可以控制好。

比如一个具体的例子,文本编辑器,在打开一个包含大量大图的文本文件,简单地,在文本文件打开的时候就将这些图片导入进来,这样做肯定需要花费很多的时间,而且那些图片很可能在打开的时候不需要完全显示出来。这个时候就可以通过Proxy Pattern来控制对图片的访问以及导入,在打开文本文件的时候,先给每一张图片新建一个Proxy对象,这个Proxy对象的创建只需要简单地一个图片名,相比完全导入一张大图,创建对象的时间显然要短很多。那么就可以很快地先将文本文件打开,在需要将图片显示出来的时候(比如翻到某一页含有图片的时候),就调用Proxy显示图片,如果这个时候图片还没导入进来则将图片导入进来。

Proxy Pattern的另外一个功能是控制对对象访问和删除,像智能指针一样,在不同的地方用到同一个对象时,就对这个对象的访问数计数,当某个地方放弃对这个对象的访问时,则访问数减一,当访问数为0时,才删除该对象。这个功能如果是直接访问对象的话,则较难做到。

通过上面那个问题还能引出三种不同类型的Proxy(也是Proxy Pattern的三个优点):

1.Remote Proxy(远程代理):使用者只需要创建一个对远程对象访问的Proxy,如何想远程服务器发送请求,如何将所需要的对象导入,已经封装到Proxy,使用者无需关注。

2.Virtual Proxy(虚拟代理):仅仅创建一个对象的Proxy,在对象未导入之前,只预先提供对象的一些信息,只有当需要真正用到对象的时候才将对象导入并且提供完整信息。比如刚才将的文本编辑器就是这个Proxy的一个例子,在将大图显示出来之前可以先提供图片的大致大小,当需要显示图片的时候才将图片导入并显示完整信息。

3.Protection Proxy(保护代理):这种Proxy主要是用来控制访问权限的,当使用者想访问某个对象时,Proxy首先会判断使用者是否有权限访问,如果有则可以返回真实对象信息,否则不能。

Proxy Pattern的结构图大致如下:




参考文献:

[1] Erich GammaRichard Helm,Ralph Johnson,John Vlissides. Design Patterns: Elements of Reusable Object Oriented Software.Pearson Education.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值