适配器模式

你挑的嘛,偶像

意图

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

动机

举个现实的例子,我有一个蓝牙耳机,可以连接手机,平板,电脑,因为他们都有一套规定好的蓝牙连接的规则
但是我用 switch游戏机 打游戏的时候也想用这个蓝牙耳机,但游戏机并没有蓝牙模块,接口不兼容,怎么办?
所以需要一个蓝牙适配器,把蓝牙适配器插在游戏机上音频输出的接口上,耳机通过蓝牙连接蓝牙适配器,耳机在结果上就是连接上了游戏机
这个时候,蓝牙适配器相对于把游戏机包装成了一个支持蓝牙功能的设备,所以 适配器模式 也被称为 包装器(Wrapeer)模式

这个蓝牙适配器就是 适配器(Adapter), 游戏机就是 适配者(Adaptee)

应用场景

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

主要解释一下第三条的意思:一般情况下,一个适配器适配一个对象的接口,但是当有一类对象(共同父类)都需要适配的时候,适配器可以直接适配它们的父类接口

UML

关于适配器的结构现在主要分为2种:类适配器对象适配器

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

class_uml

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

object_uml

  • Target : 定义 Client 使用的接口
  • Client : 调用 Target 提供的结构
  • Adaptee : 定义好的一个已经存在的接口,这个接口需要适配
  • Adapter : 对 Adaptee 的接口与 Target 接口进行适配

ClientAdapter 实例上调用一些操作。接着适配器调用 Adaptee 操作实现这个请求

注意区分一下这2种结构的不同,类适配器继承 被适配者的形式,而 对象适配器适配器对象 持有一个 适配者对象,也就是对象组合的形式

继承的方式避免不了的一个问题是,一旦 Adaptee 有更新,比如添加了抽象函数,客户端虽然不关心这些细节,但是 Adapter 还是需要改动,换句话说就是强耦合, 所以如果要推荐的话,可能还是使用 对象适配器 的方式比较好

在看 GoF 中还提到一点关于 类适配器 的细节

使用 C++ 实现类适配器时, Adapter 类应该采用 公共方式继承 Target 类,并且用 私有方式继承 Adaptee 类。因此,Adapter 类应该是 Target 的子类型,但不是 Adaptee 的子类型。

优缺点

上面介绍了很多,是不是有这样的一个感觉,为什么一开始设计的时候不直接让 Adaptee 继承 Target ? 那就不存在接口不一致的问题了~

其实在实际开发过程中,新旧代码接口不一致的问题可能大家都遇到过,虽然对旧代码进行重构是解决问题的最好方法,但是成熟的稳定的旧代码重构后是否会产生新问题,谁都不敢打保票,那就只能换个思路解决问题

所以适配器模式就是来解决问题,解决接口不兼容的问题,能解决问题就是它的优点,如果非要说缺点的话,一个适配器一次只能适配一个适配者类,写来写去有点累

来个例子

例子就用前面动机中提到的蓝牙耳机和游戏机这个例子,简单实现一下这个逻辑

首先先实现UML中 Target, 也就是这个例子中的支持蓝牙连接的设备

class BlueToothDevice
{
public:
    BlueToothDevice(){}
    virtual ~BlueToothDevice(){}
    virtual void connect() = 0;
};

一个正常的支持蓝牙功能的设备,比如 Ipad ,代码如下:

class Ipad : public BlueToothDevice
{
public:
    void connect() { std::cout << "IPad connected to Bluetooth" << std::endl;}
};

接着是我们的 NintendoSwitch 游戏机,它是没有蓝牙连接功能,只有插入耳机这种方式

class NintendoSwitch
{
public:
    void pluginHeadphones() { std::cout << "Nintendo Switch plug in headphones" << std::endl;}
};

接下来,我们分别按照上面我们提的 类适配器对象适配器 分别实现一下

// 类适配器
class ClassAdapter : public BlueToothDevice, private NintendoSwitch
{
public:
    void connect() { pluginHeadphones(); }
};

// 对象适配器
class ObjectAdapter : public BlueToothDevice
{
public:
    void connect() { m_ns.pluginHeadphones();}
private:
    NintendoSwitch m_ns;
};

适配器模式主要部分就实现了,接下来看一下 main() 函数中的调用

#define SAFE_DELETE(x) {if(x){ delete x; x= nullptr;}}
#define UNUSED(x) (void)x;

int main(int argc, char *argv[])
{
    UNUSED(argc);
    UNUSED(argv);

    BlueToothDevice *device1 = new Ipad;
    device1->connect();

    BlueToothDevice *device2 = new ObjectAdapter;
    device2->connect();

    BlueToothDevice *device3 = new ClassAdapter;
    device3->connect();

    SAFE_DELETE(device1);
    SAFE_DELETE(device2);
    SAFE_DELETE(device3);

    return 0;
}

最后看一下运行结果

IPad connected to Bluetooth
Nintendo Switch plug in headphones
Nintendo Switch plug in headphones

总结

适配器模式是一种 解决方案,它的目标是将一个类的接口转换成客户希望的另一个接口, 从而使得原本由于接口不兼容而不能一起工作的那些类可以一起工作

所以在设计的时候不要为了用而用~