C++设计模式-适配器模式


一、概念

适配器模式(Adapter Pattern)充当两个不兼容接口之间的桥梁,属于结构型设计模式。它通过一个中间件(适配器)将一个类的接口转换成客户期望的另一个接口,使原本不能一起工作的类能够协同工作。
举个真实的例子,一个笔记本电脑不支持插入存储卡,那有什么办法可以在笔记本上读取存储卡的数据呢,这时你会想到使用读卡器。读卡器就是作为存储卡和笔记本之间的适配器。将存储卡插入读卡器,再将读卡器插入笔记本,这样就可以通过笔记本来访问存储卡。
相比于这个例子,我们在程序开发中,可能笔记本电脑的接口已经是通用接口,无法改变了,而存储卡的接口也无法改变,此时我们还要实现笔记本电脑去访问存储卡,而且可能还需要访问很多不同的存储卡,每种存储卡的接口都不一样,这时我们需要开发的就是笔记本电脑和存储卡之间的适配器。

二、应用场景

  • 需要使用现有类,但其接口不符合系统需求。
  • 希望创建一个可复用的类,与多个不相关的类(包括未来可能引入的类)一起工作,这些类可能没有统一的接口。
  • 通过接口转换,将一个类集成到另一个类系中。

三、定义方式

  • 目标接口(Target):定义客户需要的接口。
  • 适配者类(Adaptee):定义一个已经存在的接口,这个接口需要适配。
  • 适配器类(Adapter):实现目标接口,并通过组合或继承的方式调用适配者类中的方法,从而实现目标接口。

四、实现方式

以下举两个例子

4.1 基础方式

按定义实现一个例子:

#include <iostream>
#include <memory>

/**
 * @brief 目标接口
 */
class Target {
 public:
  Target(){};
  virtual ~Target(){};

 public:
  // 统一的目标接口
  virtual void todo() = 0;
};

/**
 * @brief 适配者类
 */
class Adaptee {
 public:
  // 需要适配的接口
  void dosomething() {
    // 适配者类的具体实现
    std::cout << "Adaptee::dosomething()" << std::endl;
  }
};

/**
 * @brief 适配器类
 */
class Adapter : public Target {
 public:
  Adapter() : adaptee(std::make_shared<Adaptee>()) {}  // 在构造时初始化适配者
  ~Adapter() override {}

 public:
  // 实现目标接口的方法,实际调用了适配者内的接口,实现适配
  void todo() override {
    adaptee->dosomething();
  }

 private:
  std::shared_ptr<Adaptee> adaptee;  // 使用智能指针持有适配者对象
};

// 客户端代码
int main() {
  std::shared_ptr<Target> target = std::make_shared<Adapter>();
  target->todo();   // 输出 "Adaptee::dosomething()"

  return 0;
}

4.2 通用方式

举一个常用的场景:
有一个设备管理类,可以动态增加和删除设备,并且还可以控制设备打开或者关闭,这个类实现之后,不用管设备具体类型,后期有新的奇奇怪怪的设备都可以兼容而不需要修改这个设备管理类的代码。

/**
 * @brief 设备控制统一接口
 */
class DeviceControl {
 public:
  DeviceControl() {}
  virtual ~DeviceControl() {}

 public:
  virtual void TurnOn() = 0;	// 统一的设备打开接口
  virtual void TurnOff() = 0;   // 统一的设备关闭接口
};

现在有一个电风扇设备,其接口假如是厂家提供的SDK,我们自己没法修改

/**
 * @brief 风扇设备控制接口
 */
class Fan {
 public:
  Fan() {}
  ~Fan(){};

 public:
  void Open() { std::cout << "open fan" << std::endl; };
  void Close() { std::cout << "close fan" << std::endl; };
};

还有一个电视机设备,其接口假如是厂家提供的SDK,我们自己没法修改

/**
 * @brief 电视机设备控制接口
 */
class TV {
 public:
  TV() {}
  ~TV(){};

 public:
  void Switch(bool value) {
    if (value == true)
      std::cout << "open tv" << std::endl;
    else
      std::cout << "close tv" << std::endl;
  };
};

现在要通过设备控制统一接口去控制上面的风扇和电视机,就需要实现两个适配器
风扇适配器:

/**
 * @brief 风扇适配器
 */
class FanAdapter : public DeviceControl {
 public:
  FanAdapter() : fan_(std::make_shared<Fan>()) {}
  virtual ~FanAdapter() override {}

 public:
  void TurnOn() override { fan_->Open(); };		// 风扇适配器中实际调用了风扇厂家提供的接口
  void TurnOff() override { fan_->Close(); };   // 风扇适配器中实际调用了风扇厂家提供的接口

 private:
  std::shared_ptr<Fan> fan_;
};

电视机适配器:

/**
 * @brief 电视机适配器
 */
class TVAdapter : public DeviceControl {
 public:
  TVAdapter() : tv_(std::make_shared<TV>()) {}
  virtual ~TVAdapter() override {}

 public:
  void TurnOn() override { tv_->Switch(true); };    // 电视机适配器中实际调用了电视机厂家提供的接口
  void TurnOff() override { tv_->Switch(false); };  // 电视机适配器中实际调用了电视机厂家提供的接口

 private:
  std::shared_ptr<TV> tv_;
};

客户端测试程序:

// 客户端测试程序
int main() {
  std::vector<std::shared_ptr<DeviceControl>> devices;	// 设备列表
  devices.emplace_back(std::make_shared<FanAdapter>()); // 添加设备
  devices.emplace_back(std::make_shared<TVAdapter>());  // 添加设备

  std::cout << "open all devices" << std::endl;
  for (const auto& v : devices) {
    v->TurnOn();
  }

  std::cout << "close all devices" << std::endl;
  for (const auto& v : devices) {
    v->TurnOff();
  }

  return 0;
}

运行结果:

open all devices
open fan
open tv
close all devices
close fan
close tv

在上面这个例子中,设备控制统一接口中,有设备打开和关闭两个统一接口,而风扇和电视机也有其打开和关闭的接口,不过他们的接口并不统一,电视机的打开和关闭甚至使用了同一个接口,通过传入不同参数来区分是打开还是关闭,而通过给每个设备实现一个适配器,这样就可以将他们的接口统一起来,如果后面还要控制更多的其他设备,只需要给每个设备都实现一个适配器即可,无需修改已有的代码,这也符合开闭原则。

总结

适配器模式的优点主要包括:
(1)提高类的透明性和复用性: 适配器模式可以让类在现有的基础上进行复用,而不需要做出任何改变,这有助于避免大规模改写现有代码。
(2)解耦目标类和适配器类: 通过使用适配器模式,目标类和适配器类可以实现解耦,从而提高程序的扩展性。这符合开闭原则,即对扩展开放,对修改关闭。
(3)更好的扩展性: 在实现适配器功能的时候,可以调用自己开发的功能,从而自然地扩展系统的功能。
(4)灵活性好: 适配器并没有影响原有功能,如果不想使用,可以直接删除。

然而,适配器模式也存在一些缺点:
(1)增加系统复杂性: 适配器编写过程需要全面考虑,可能会增加系统的复杂性。
(2)降低代码可读性: 过多地使用适配器可能会使系统代码变得凌乱,增加代码阅读难度,降低代码可读性。比如,明明看到调用的是 A 接口,其实内部被适配成了 B 接口的实现,如果系统中有太多这种情况,可能会使系统难以理解和维护。

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐