首页 > 代码库 > 设计模式(七):Adapter 适配器模式 -- 结构型模式

设计模式(七):Adapter 适配器模式 -- 结构型模式

 

1. 概述:

         接口的改变,是一个需要程序员们必须(虽然很不情愿)接受和处理的普遍问题。程序提供者们修改他们的代码;系统库被修正;各种程序语言以及相关库的发展和进化。

        例子1:iphone4,你即可以使用UBS接口连接电脑来充电,假如只有iphone没有电脑,怎么办呢?苹果提供了iphone电源适配器。可以使用这个电源适配器充电。这个iphone的电源适配器就是类似我们说的适配器模式。(电源适配器就是把电源变成需要的电压,也就是适配器的作用是使得一个东西适合另外一个东西。)

       例子2:最典型的例子就是很多功能手机,每一种机型都自带有从电器,有一天自带充电器坏了,而且市场没有这类型充电器可买了。怎么办?万能充电器就可以解决。这个万能充电器就是适配器。

 

2. 问题

 

     你如何避免因外部库的API改变而带来的不便?假如你写了一个库,你能否提供一种方法允许你软件的现有用户进行完美地升级,即使你已经改变了你的API?为了更好地适宜于你的需要,你应该如何改变一个对象的接口?

 

3. 解决方案

 

        适配器(Adapter)模式为对象提供了一种完全不同的接口。你可以运用适配器(Adapter)来实现一个不同的类的常见接口,同时避免了因升级和拆解客户代码所引起的纠纷。

 

    适配器模式(Adapter Pattern),把一个类的接口变换成客户端所期待的另一种接口, Adapter模式使原本因接口不匹配(或者不兼容)而无法在一起工作的两个类能够在一起工作。又称为转换器模式、变压器模式、包装(Wrapper)器模式(把已有的一些类包装起来,使之能有满足需要的接口)。

 

 

     考虑一下当(不是假设!)一个第三方库的API改变将会发生什么。过去你只能是咬紧牙关修改所有的客户代码,而情况往往还不那么简单。你可能正从事一项新的项目,它要用到新版本的库所带来的特性,但你已经拥有许多旧的应用程序,并且它们与以前旧版本的库交互运行地很好。你将无法证明这些新特性的利用价值,如果这次升级意味着将要涉及到其它应用程序的客户代码。

4. 分类

共有两类适配器模式:1.类的适配器模式(采用继承实现)2.对象适配器(采用对象组合方式实现)

1)类适配器模式    ——适配器继承自已实现的类(一般多重继承)。

 

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

2)对象适配器模式—— 适配器容纳一个它包裹的类的实例。在这种情况下,适配器调用被包裹对象的物理实体。

 

Adapter与Adaptee是委托关系
1、允许一个Adapter与多个Adaptee同时工作。Adapter也可以一次给所有的Adaptee添加功能
2、使用重定义Adaptee的行为比较困难
无论哪种适配器,它的宗旨都是:保留现有类所提供的服务,向客户提供接口,以满足客户的期望。
即在不改变原有系统的基础上,提供新的接口服务。

5. 适用性

 

以下情况使用Adapter模式:
1 • 你想使用一个已经存在的类,而它的接口不符合你的需求。
2 • 你想创建一个可以复用的类,该类可以与其他不相关的类或不可预见的类(即那些接口可能不一定兼容的类)协同工作。
3 •(仅适用于对象Adapter)你想使用一些已经存在的子类,但是不可能对每一个都进行子类化以匹配它们的接口。对象适配器可以适配它的父类接口。即仅仅引入一个对象,并不需要额外的指针以间接取得adaptee。

 

6. 结构

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


对象匹配器依赖于对象组合,如下图所示:


7. 构建模式的组成

•目标角色(Target):— 定义Client使用的与特定领域相关的接口。
• 客户角色(Client):与符合Target接口的对象协同。
• 被适配橘色(Adaptee):定义一个已经存在并已经使用的接口,这个接口需要适配。
• 适配器角色(Adapte) :适配器模式的核心。它将对被适配Adaptee角色已有的接口转换为目标角色Target匹配的接口。对Adaptee的接口与Target接口进行适配.

8. 效果

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

 

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

使用Adapter模式时需要考虑的其他一些因素有

1) Adapter的匹配程度 对Adaptee的接口与Target的接口进行匹配的工作量各个Adapter可能不一样。工作范围可能是,从简单的接口转换(例如改变操作名 )到支持完全不同的操作集合。Adapter的工作量取决于Target接口与Adaptee接口的相似程度
2) 可插入的Adapter   当其他的类使用一个类时,如果所需的假定条件越少,这个类就更具可复用性。如果将接口匹配构建为一个类,
就不需要假定对其他的类可见的是一个相同的接口。也就是说,接口匹配使得我们可以将自己的类加入到一些现有的系统中去,
而这些系统对这个类的接口可能会有所不同。 
3) 使用双向适配器提供透明操作 使用适配器的一个潜在问题是,它们不对所有的客户都透明。被适配的对象不再兼容 Adaptee的接口,
因此并不是所有 Adaptee对象可以被使用的地方它都可以被使用。双向适配器提供了这样的透明性。
在两个不同的客户需要用不同的方式查看同一个对象时,双向适配器尤其有用。


9. 实现

类适配器使用的是继承

让我们看看当API改变时,如何保护应用程序不受影响。

 

  1. <?php  
  2. /** 
  3.  * 类适配器模式 
  4.  * @author guisu 
  5.  *  
  6.  */  
  7.    
  8. /** 
  9.  * 目标角色 
  10.  * @version 1.0 
  11.  */  
  12. class Target {  
  13.    
  14.     /** 
  15.      * 这个方法将来有可能改进 
  16.      */  
  17.     public function hello(){  
  18.         echo ‘Hello ‘;  
  19.     }  
  20.    
  21.     /** 
  22.      * 目标点 
  23.      */  
  24.     public function world(){  
  25.         echo ‘world‘;  
  26.     }  
  27. }  
  28.    
  29. /** 
  30.  * Client 程序 
  31.  * 
  32.  */  
  33. class Client {  
  34.   
  35.     /** 
  36.      * Main program. 
  37.      */  
  38.     public static function main() {  
  39.         $Target = new Target();  
  40.         $Target->hello();  
  41.         $Target->world();  
  42.    
  43.     }  
  44.    
  45. }  
  46. Client::main();  
  47. ?>  

 

 

 

我们Target已经明确指出hello()方法会在未来的版本中改进,甚至不被支持或者淘汰。接下来,现在假设第二版的Target已经发布。一个全新的greet()方法代替了hello()。

 

 

 

 

 

  1. <?php  
  2. /** 
  3.  * 类适配器模式 
  4.  * @author guisu 
  5.  *  
  6.  */  
  7.    
  8. /** 
  9.  * 目标角色 
  10.  * @version 2.0 
  11.  */  
  12. class Target {  
  13.    
  14.     /** 
  15.      * 这个方法将来有可能继续改进 
  16.      */  
  17.     public function greet(){  
  18.         echo ‘Greet ‘;  
  19.     }  
  20.    
  21.     /** 
  22.      * 目标点 
  23.      */  
  24.     public function world(){  
  25.         echo ‘world‘;  
  26.     }  
  27. }  
如果我们继续使用原来的client代码,肯定会报错,找不到hello方法。

 

 

针对API“升级”的解决办法就是创建一个适配器(Adapter)。

 

类适配器使用的是继承

  1. <?php  
  2. /** 
  3.  * 类适配器模式 
  4.  * @author guisu 
  5.  *  
  6.  */  
  7.    
  8. /** 
  9.  * 目标角色 
  10.  * @version 2.0 
  11.  */  
  12. interface Target {  
  13.    
  14.     /** 
  15.      * 源类的方法:这个方法将来有可能继续改进 
  16.      */  
  17.     public function hello();  
  18.    
  19.     /** 
  20.      * 目标点 
  21.      */  
  22.     public function world();  
  23. }  
  24.    
  25. /** 
  26.  * 源角色:被适配的角色 
  27.  */  
  28. class Adaptee {  
  29.     /** 
  30.      * 源类含有的方法 
  31.      */  
  32.     public function world() {  
  33.         echo ‘ world <br />‘;  
  34.     }  
  35.    
  36.     /** 
  37.      * 加入新的方法 
  38.      */  
  39.     public function greet() {  
  40.         echo ‘ Greet ‘;  
  41.     }  
  42. }  
  43.    
  44. /** 
  45.  * 类适配器角色 
  46.  */  
  47. class Adapter extends Adaptee implements Target {  
  48.    
  49.     /** 
  50.      * 源类中没有world方法,在此补充 
  51.      */  
  52.     public function hello() {  
  53.        parent::greet();  
  54.     }  
  55.    
  56. }  
  57. /** 
  58.  * 客户端程序 
  59.  * 
  60.  */  
  61. class Client {  
  62.    
  63.     /** 
  64.      * Main program. 
  65.      */  
  66.     public static function main() {  
  67.         $adapter = new Adapter();  
  68.         $adapter->hello();  
  69.         $adapter->world();  
  70.     }  
  71. }  
  72. Client::main();  
  73. ?>  

对象适配器使用的是委派

 

 

 

  1. <?php  
  2. /** 
  3.  * 类适配器模式 
  4.  * @author guisu 
  5.  *  
  6.  */  
  7.    
  8. /** 
  9.  * 目标角色 
  10.  * @version 2.0 
  11.  */  
  12. interface Target {  
  13.    
  14.     /** 
  15.      * 源类的方法:这个方法将来有可能继续改进 
  16.      */  
  17.     public function hello();  
  18.    
  19.     /** 
  20.      * 目标点 
  21.      */  
  22.     public function world();  
  23. }  
  24.    
  25. /** 
  26.  * 源角色:被适配的角色 
  27.  */  
  28. class Adaptee {  
  29.     /** 
  30.      * 源类含有的方法 
  31.      */  
  32.     public function world() {  
  33.         echo ‘ world <br />‘;  
  34.     }  
  35.    
  36.     /** 
  37.      * 加入新的方法 
  38.      */  
  39.     public function greet() {  
  40.         echo ‘ Greet ‘;  
  41.     }  
  42. }  
  43.    
  44. /** 
  45.  * 类适配器角色 
  46.  */  
  47. class Adapter  implements Target {  
  48.   
  49.     private $_adaptee;  
  50.     /** 
  51.      * construct 
  52.      * 
  53.      * @param Adaptee $adaptee 
  54.      */  
  55.     public function __construct(Adaptee $adaptee) {  
  56.         $this->_adaptee = $adaptee;  
  57.     }  
  58.    
  59.     /** 
  60.      * 源类中没有world方法,在此补充 
  61.      */  
  62.     public function hello() {  
  63.        $this->_adaptee->greet();  
  64.     }  
  65.    
  66.     /** 
  67.      * 源类中没有world方法,在此补充 
  68.      */  
  69.     public function world() {  
  70.        $this->_adaptee->world();  
  71.     }  
  72. }  
  73. /** 
  74.  * 客户端程序 
  75.  * 
  76.  */  
  77. class Client {  
  78.    
  79.     /** 
  80.      * Main program. 
  81.      */  
  82.     public static function main() {  
  83.         $adaptee = new Adaptee();  
  84.         $adapter = new Adapter($adaptee);  
  85.         $adapter->hello();  
  86.         $adapter->world();  
  87.     }  
  88. }  
  89.   
  90. Client::main();  
  91. ?>  

 

如例中代码所示,你可以运用适配器(Adapter)模式来避免因外部库改变所带来的不便——倘若向上兼容。作为某个库的开发者,你应该独立编写适配器,使你的用户更简便地使用新版本的库,而不用去修改他们现有的全部代码。

     GoF书中提出的适配器(Adapter)模式更倾向于运用继承而不是组成。这在强类型语言中是有利的,因为适配器(Adapter)事实上是一个目标类的子类,因而能更好地与类中方法相结合。

了更好的灵活性,我个人比较倾向于组成的方法(特别是在结合了依赖性倒置的情况下);尽管如此,继承的方法提供两种版本的接口,或许在你的实际运用中反而是一个提高灵活性的关键。

10.适配器模式与其它相关模式

桥梁模式(bridge模式):桥梁模式与对象适配器类似,但是桥梁模式的出发点不同:桥梁模式目的是将接口部分和实现部分分离,从而对它们可以较为容易也相对独立的加以改变。而对象适配器模式则意味着改变一个已有对象的接口

装饰器模式(decorator模式):装饰模式增强了其他对象的功能而同时又不改变它的接口。因此装饰模式对应用的透明性比适配器更好。结果是decorator模式支持递归组合,而纯粹使用适配器是不可能实现这一点的。

Facade(外观模式):适配器模式的重点是改变一个单独类的API。Facade的目的是给由许多对象构成的整个子系统,提供更为简洁的接口。而适配器模式就是封装一个单独类,适配器模式经常用在需要第三方API协同工作的场合,设法把你的代码与第三方库隔离开来。

适配器模式与外观模式都是对现相存系统的封装。但这两种模式的意图完全不同,前者使现存系统与正在设计的系统协同工作而后者则为现存系统提供一个更为方便的访问接口。简单地说,适配器模式为事后设计,而外观模式则必须事前设计,因为系统依靠于外观。总之,适配器模式没有引入新的接口,而外观模式则定义了一个全新的接口。

 

代理模式(Proxy )在不改变它的接口的条件下,为另一个对象定义了一个代理。

 

装饰者模式,适配器模式,外观模式三者之间的区别:

装饰者模式的话,它并不会改变接口,而是将一个一个的接口进行装饰,也就是添加新的功能。

适配器模式是将一个接口通过适配来间接转换为另一个接口。

外观模式的话,其主要是提供一个整洁的一致的接口给客户端。

 

 

参考:

1. http://blog.csdn.net/hguisu/article/details/7527842;

2. http://www.cnblogs.com/steven_oyj/archive/2010/06/17/1759324.html。