概述

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

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

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

问题

你如何避免因外部库的API改变而带来的不便?
例如,你写了一个Target类提供以下API

class Target
{
public:
    void hello(){}
    void world(){}
};

client调用了这些API

Target target();  
target.hello();  
target.world();

之后Target类更新了,一个全新的greet()方法代替了hello()。这时候client调用就会出错

class Target
{
public:
    void greet(){}
    void world(){}
};

这时候你能否提供一种方法让client尽量少修改代码,或者不用修改完美地升级呢?

再考虑一种情况,作为client调用第三方API,如果试验新版本API库所带来的特性,但是又不想改动旧的程序。这时候你会怎么做?

解决方案

适配器(Adapter)模式可以把一个类的接口(Adaptee)通过适配器(Adapter)变换成客户端所期待的另一种接口(Target), 使原本因接口不匹配(或者不兼容)而无法在一起工作的两个类能够在一起工作,避免了因升级和拆解客户代码所引起的纠纷。

分类

共有两类适配器模式:

  1. 类适配器模式
    采用继承实现,适配器继承自已实现的类(一般多重继承)。Adapter与Adaptee是继承关系

  2. 对象适配器模式
    采用对象组合方式实现,适配器容纳Adaptee类的实例。Adapter与Adaptee是委托关系

无论哪种适配器,它的宗旨都是:保留现有类所提供的服务,向客户提供接口,以满足客户的期望。,即在不改变原有系统的基础上,提供新的接口服务。

适用性

以下情况使用Adapter模式:

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

结构

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

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

适配器模式的组成

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

效果

类适配器和对象适配器有不同的权衡。

类适配器

  1. 用一个具体的Adapter类和Target进行匹配,但是是Adaptor不能匹配一个类以及所有它的子类时
  2. 使得Adapter可以重定义Adaptee的部分行为,因为Adapter是Adaptee的一个子集
  3. 仅仅引入一个对象,并不需要额外的指针以间接取得Adaptee

对象适配器

  1. 允许一个Adapter与多个Adaptee同时工作。Adapter也可以一次给所有的Adaptee添加功能
  2. 使用重定义Adaptee的行为比较困难

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

  1. Adapter的匹配程度
    对Adaptee的接口与Target的接口进行匹配的工作量各个Adapter可能不一样。工作范围可能是,从简单的接口转换(例如改变操作名 )到支持完全不同的操作集合。Adapter的工作量取决于Target接口与Adaptee接口的相似程度

  2. 可插入的Adapter
    当其他的类使用一个类时,如果所需的假定条件越少,这个类就更具可复用性。如果将接口匹配构建为一个类,就不需要假定对其他的类可见的是一个相同的接口。也就是说,接口匹配使得我们可以将自己的类加入到一些现有的系统中去,而这些系统对这个类的接口可能会有所不同.

  3. 使用双向适配器提供透明操作
    使用适配器的一个潜在问题是,它们不对所有的客户都透明。被适配的对象不再兼容 Adaptee的接口,因此并不是所有 Adaptee对象可以被使用的地方它都可以被使用。双向适配器提供了这样的透明性。在两个不同的客户需要用不同的方式查看同一个对象时,双向适配器尤其有用。

实现

让我们看看当API改变时,如何保护应用程序不受影响。
类适配器使用的是继承

#include <iostream>
using namespace std;

/**
 * 目标角色
 */
class Target
{
public:
    void hello(){}
    void world(){}
};

/**
 * 源角色:被适配的角色
 */
class Adaptee
{
public:
    void greet()
    {
        cout << "Greet" << endl;
    }

    void world()
    {
        cout << "World" << endl;
    }
};

/**
 * 类适配器角色
 */
class Adaptor: public Target, public Adaptee
{
public:
    void hello()
    {
        greet();
    }

    void world()
    {
        Adaptee::world();
    }
};

int main()
{
    Adaptor adaptor;
    adaptor.hello();
    adaptor.world();
    return 0;
} 

对象适配器使用的是委派

#include <iostream>
using namespace std;

/**
 * 目标角色
 */
class Target
{
public:
    void hello(){}
    void world(){}
};

/**
 * 源角色:被适配的角色
 */
class Adaptee
{
public:
    void greet()
    {
        cout << "Greet" << endl;
    }

    void world()
    {
        cout << "World" << endl;
    }
};

/**
 * 类适配器角色
 */
class Adaptor: public Target
{
public:
    explicit Adaptor(Adaptee adaptee)
    {
        _adaptee = adaptee;
    }

    void hello()
    {
        _adaptee.greet();
    }

    void world()
    {
        _adaptee.world();
    }

private:
    Adaptee _adaptee;
};

int main()
{
    Adaptee adaptee;
    Adaptor adaptor(adaptee);
    adaptor.hello();
    adaptor.world();
    return 0;
}

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

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

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

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

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

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

  • 外观模式(Facade):适配器模式的重点是改变一个单独类的API。Facade的目的是给由许多对象构成的整个子系统,提供更为简洁的接口。而适配器模式就是封装一个单独类,适配器模式经常用在需要第三方API协同工作的场合,设法把你的代码与第三方库隔离开来。
    适配器模式与外观模式都是对现相存系统的封装。但这两种模式的意图完全不同,前者使现存系统与正在设计的系统协同工作而后者则为现存系统提供一个更为方便的访问接口。简单地说,适配器模式为事后设计,而外观模式则必须事前设计,因为系统依靠于外观。总之,适配器模式没有引入新的接口,而外观模式则定义了一个全新的接口。

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

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

  • 装饰者模式的话,它并不会改变接口,而是将一个一个的接口进行装饰,也就是添加新的功能。
  • 适配器模式是将一个接口通过适配来间接转换为另一个接口。
  • 外观模式的话,其主要是提供一个整洁的一致的接口给客户端。

参考链接