2012年9月26日星期三

适配器模式结构型模式(1)

适配器模式结构型模式(1)

前言

从今天开始,我们继续设计模式系列学习之旅,完成了对创建型模式的介绍和学习,接下来,我们将着重介绍各种结构型模式,希望大家能一起参与进来,学习、交流和思考。结构型模式涉及如何组合类和对象以获得更大的结构,同时,在各种结构型模式的实现上基本上遵循优先使用对象组合,而不是类继承原则,因为在运行时刻改变对象组合关系,可以使对象组合方式更具灵活性,这种机制用静态类组合(继承方式)是不可能实现的,这点,在我们学习各种结构型模式的过程中将会有较深刻的体味。好了,就让我们赶紧进入结构型模式的第一次探索之旅吧——适配器模式。

动机

在日常的系统开发中,我们往往需要借助第三方组件或者是中间件来帮助我们快速完成某一功能模块,避免重复造轮子。但是,由于系统本身所支持的接口与第三方组件或者中间件所提供的接口不兼容,因此即便它们的功能实现完全符合系统的开发需求,仍不能直接拿来使用。这时候,作为开发人员应该提供一种封装机制,对现存组件接口进行包装,使其完全符合我们当前系统的接口要求,这样便能达到重用已有组件的目的呢。这不仅避免了浪费人力物力的从头开发,同时还能极大地提高系统的开发效率。面对上述这样的一种需求,有一种结构型模式便是它的福星——适配器模式,它能够很好地解决由于接口不兼容而导致不能直接运用于新系统的棘手问题。

意图

将一个类的接口转换成客户希望的另外一个接口。适配器模式使复原本由于接口不兼容而不能一起工作的那些类可以一起工作。

结构图

适配器模式的实现有两种方式,一个是通过多重继承的方式,另外一个是通过对象组合的方式,因此,针对这两种不同的实现方式,就存在两种不同的结构图。

类适配器使用多重继承对一个接口与另一个接口进行匹配,结构图如下:

image

对象适配器依赖于对象组合,结构图如下:

image

  1. 目标(Target)角色:定义客户使用的与特定领域相关的接口,也是客户所期待的接口。
  2. 被适配对象(Adaptee)角色:一个已经存在的接口,同时实现了系统需要的功能,但是与当前系统接口不兼容,需要被适配的类。
  3. 适配器对象(Adapter)角色:把Adaptee接口转换成目标接口,也就是对Adaptee接口与Target接口进行适配的类。

代码示例

类适配器代码示例:

 1:  public abstract class Target{
 2:      public abstract void Request();
 3:  }
 4:   
 5:  public interface Adaptee{
 6:      public void SpecificRequest();
 7:  }
 8:   
 9:  public class Adapter extends Target implements Adaptee{
10:      public void Request(){
11:          SpecificRequest();
12:      }
13:   
14:      @Override
15:      public void SpecificRequest() {
16:          //省略了Adaptee相关的具体实现
17:          
18:      }
19:  }
20:   
21:  public class Client{
22:      public static void main(String[] args){
23:          Target target=new Adapter();
24:          target.Request();
25:      }
26:  }

 

注意,由于java语言不支持多继承,因此我们需要将Adaptee修改为接口类型。我们可以看到,通过继承的方式来实现类适配器,有较多的局限性,我们需要重新在Adapter类中重写Adaptee中对应的方法,但由于商业原因,我们无法获知其具体的实现源码,因此在java语言层面类适配器模式有一定的局限性,没有对象适配器使用的广泛和方便。

对象适配器代码示例:

 1:  public abstract class Target{
 2:      public abstract void Request();
 3:  }
 4:   
 5:  public class Adaptee{
 6:      public void SpecificRequest(){
 7:          //省略具体实现
 8:      }
 9:  }
10:   
11:  public class Adapter extends Target {
12:      private Adaptee adaptee=null;
13:      public Adapter(Adaptee adaptee){
14:          this.adaptee=adaptee;
15:      }
16:      public void Request(){
17:          //直接调用Adaptee对应的方法来完成对应的功能
18:          adaptee.SpecificRequest();
19:      }
20:  }
21:   
22:  public class Client{
23:      public static void main(String[] args){
24:          Target target=new Adapter(new Adaptee());
25:          target.Request();
26:      }
27:  }

根据上述对象适配器的示例代码,我们可以看到,在适配器类Adapter中的Request()方法中,我们通过对象组合的方式,将对Request()方法的请求直接委托给Adaptee对象的SpecificRequest()方法来完成。通过这样的方式,我们无需了解adaptee类的源码,就可以对其进行适配操作呢,相较于类适配器来说,对象适配器是一种更值得推荐的实现方式。下文还会对两种实现方式作进一步的比较阐述。

现实场景

在实现的生活中,我们处处可以看到适配的场景。比如说我们使用的日用的家用电器,由于美国的家用标准电压是110v,因此其家电用器的标准电压也是110v,但是在中国,家用标准电压却是220v,如果不”适配“,显然无法在国内使用美国生产的电器,此时我们通常会使用变压器,将标准的220v电压调低到110v状态,这样就能正常使用美国国内生产的电器呢,从适配器的角度来看,其实这就是一种适配的过程,通过变压器将电压降低,致使原本由于使用电压不同而不能在国内正常使用的家电现在也能够很好地使用呢,这其实就是典型的适配器模式。再比如就是外文翻译,如果你是不懂英文,又需要和美国人交流,那么这时候你必须寻求翻译家的帮助,将自己的中文表达通过翻译家转译为相应的英文表达,这样对方才能听懂接受自己的表达,反之也一样,同理,此时翻译家其实就充当着一个适配器的角色,将原本不相通的语言表达进行翻译促使彼此都能听懂彼此的表达,更形象点的说就是将彼此的语言表达进行了一层本地化包装,这样就能让原本语言不相通的彼此进行无障碍地交流呢,这是不是也是一种适配器模式的体现呢?

上面所举的例子是我们日常生活中能够碰到的例子,作为程序员,在我们日常的开发过程中,适配器模式自然也是家常便饭。就拿java语言来说,我们几乎每天都和数据库打交道,但是数据库类型迥异,每个厂家都会有自己的一套实现标准,如果我们大家需要根据自己当前使用的数据库类型而选择对应接口的操作api的话,大家会不会有烦燥到不行呢?至少我是会这样的。但是事实的情况并并非我刚刚描述的那个样子,不管我们使用何种数据库,mysql还是oracle好,我们需要改动的只是替换相应的数据库驱动程序包而已,但是对各种数据库操作的接口不管使用哪个数据库驱动程序都是一致的,这自然省去了我们很多的没必要的再学习时间。讲到这里,大家应该能猜到正是通过使用适配器模式,致使各大厂商提供的数据库驱动程序对各个数据库操作的api接口都完全一致,完全屏蔽了具体数据库底层的实现细节,这些细节我们也无需了解。说了这么多,接下来,还是让我们通过代码的形式来演绎下上面所介绍的场景吧!

 1:  public abstract class Target{
 2:      public abstract void select();
 3:      public abstract void insert();
 4:      public abstract void update();
 5:      public abstract void delete();
 6:  }
 7:   
 8:  public class MySql{
 9:      public void select_mysql(){}//mysql底层对select操作的实现api
10:      public void insert_mysql(){}//mysql底层对insert操作的实现api
11:      public void update_mysql(){}//mysql底层对update操作的实现api
12:      public void delete_mysql(){}//mysql底层对delete操作的实现api
13:  }    
14:   
15:  public class MySqlAdapter extends Target {
16:      private MySql adaptee=null;
17:      public  MySqlAdapter(MySql adaptee){
18:          this.adaptee=adaptee;
19:      }
20:      public void select(){
21:          //直接调用Adaptee对应的方法来完成对应的功能
22:          adaptee.select_mysql();
23:      }
24:      public void insert(){
25:          adaptee.insert_mysql();
26:      }
27:      public void update(){
28:          adaptee.update_mysql();
29:      }
30:      public void delete(){
31:          adaptee.delete_mysql();
32:      }
33:  }
34:   
35:  public class Client{
36:      public static void main(String[] args){
37:          Target target=new MySqlAdapter(new MySql());
38:          target.select();
39:          target.insert();
40:          target.update();
41:          target.delete();
42:      }
43:  }    

 上述示例代码中,我们通过适配器来适配mysql底层操作api,也就是crud操作,将它们包装成统一规范的接口形式,方便客户端调用。同样的道理,如果是oracle数据库,也尽可以通过对象适配器模式来对其底层api进行适配包装成符合统一规范的数据库驱动程序。好呢,对适配器模式的现实场景就讲解到这呢,大家都发挥聪明才智,尽情联想生活中的种种符合适配器模式的场景,也欢迎留言交流!

实现要点

  1. 适配器模式主要有两种实现结构:类适配器模式和对象适配器模式。类适配器模式通过多重继承的方式来实现,容易产生紧耦合,一般不推荐使用,而对象适配器械采用对象组合的方式,具有更弱的耦合性,有利于代码的可扩展性和可维护性。
  2. 适配器模式主要应用于”希望复用一些已经存在的类,而接口又与复用环境所要求的接口不一致的情况“,在遗留代码复用、类库迁移等方面非常有用。
  3. 想要真正地发挥适配器模式魔力,本身就要求我们在编程的过程中尽量遵循”面向接口编程“原则,这样在后期或者今后的适配上才更具灵活性。
  4. 适配器模式在实现上非常灵活,我们完全可以将现在的对象作为复用环境下新接口的参数,在实现体里完成对现存对象的适配过程。

运用效果

针对类适配器(C++更适合实现类适配器,因为它支持多继承,java或者.net并不擅长):

  1. 用一个具体的Adapter类对Adaptee和Target进行匹配。结果是当我们想要匹配一个类以及它的子类时,类Adapter将不能胜任工作。
  2. 使得Adapter可以重定义Adapteee的部分行为,因为Adapter是Adaptee的一个子类。
  3. 仅仅引入了一个对象,并不需要额外的指针来间接得到adaptee。

针对对象适配器

  1. 允许一个Adapter与多个Adaptee——即Adaptee本身以及它的所有子类(如果有子类 的话)同时工作。Adapter也可以一次给所有的Adaptee添加功能。
  2. 使得重定义Adaptee的行为比较困难。这就需要生成Adaptee的子类并且便得Adapter引用这个子类而不是引用Adaptee本身。

总得说来,使用适配器模式会带来更好的复用性可扩展性;但如果过度使用适配器的话,也会让系统非常零乱,不容易整体进行把握,过犹则不及嘛!

适用性

  1. 你想使用一个已经存在的类,而它的接口并不符合你当前系统接口需要时。
  2. 你想创建一个可以复用的类,该类可以与其他不相关的类或者不可预见的类(即那些接口可能不一定兼容的类)协同工作时。
  3. (仅适用于对象适配器)你想使用一些已经存在的子类,但是不可能对每一个都进行子类化以匹配它们的的接口时。对象适配器可以适配它的父类接口。

相关模式

  1. 适配器模式和桥接模式:两者结构比较相似,功能却完全不同。适配器是把两个或者多个接口的功能进行转换匹配;而桥接模式是让接口与实现相分离,以便它们可以相对独立地变化(这里不理解的同学,可以暂时略过,下篇即将重点介绍桥接模式)。
  2. 适配器模式与装饰模式:两者都是使用对象组合的方式来实现,但是两者的形式却完全不一样:一般来说适配器适配之后是需要改变接口的,否则就没有适配的必要呢;而装饰模式是不会改变接口的,不管出现多少层装饰都是一个接口,这样带来的一个好处便是很容易做到递归组合,对一个对象进行多次装饰便其具有多重装饰功能(这里不理解的同学,可以暂时略过,后面会有对装饰模式的详细介绍)。
  3. 适配器模式和代理模式:在实现适配器的时候,可以通过代理来调用adaptee,这样可以获得更大的灵活性。
  4. 适配器和抽象工厂模式:在对象适配器模式的实现中,我们需要引用adaptee对象,而这个对象的创建工作,我们可以利用创建型模式来完成,比如工厂方法模式、抽象工厂或者单例模式等。

总结

适配器模式的本质是:转换匹配,复用功能。适配器通过转换调用已有对象的实现,从而把已有的功能匹配成需要的接口形式,这样便满足复用环境接口需要。也就是说转换匹配是手段,而复用已有的功能才是目的。最后,需要提醒大家的是,适配器模式是事后补救的一种措施,如果是为了对其三方组件功能的复用,这不可厚非,但是如果只是在项目后期使用适配器模式对系统某个功能模块进行适配的话,或许大家就应该静下心来,好好思考下,是否在系统设计上本身就存在缺陷或者说考虑不够周到的地方,因为毕竟事先做好设计,总比事后修改要来得轻松方便些,运用适配器模式并不是解决所有遗留问题。不过总的说来,适配器模式确实不失为一个达到代码功能复用的好方法,好手段,大家说呢?个中利弊就仁者见仁,智者见智呢!适配器模式就介绍到这吧,下篇继续讲解另一个结构型模式——桥接模式,敬请期待!

 

参考资料

  1. 程杰著《大话设计模式》一书
  2. 陈臣等著《研磨设计模式》一书
  3. GOF著《设计模式》一书
  4. Terrylee .Net设计模式系列文章
  5. 吕震宇老师 设计模式系列文章

TAG: