4.1-Adapter「wrapper」-适配器-结构型模式

又称包装器(wrapper)

用途

将一个类的接口转换成客户希望的另外一个接口。

  • Adapter 模式使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。
  • Adapter 经常还要负责提供那些被匹配的类所没有提供的功能

示例

已有类型 A 的接口和实现,考虑使之适配类型 B 的接口:

  • 类版本:继承 B 的接口和 A 的实现
  • 对象版本:类型 B 中存储一个类型 A 的实例,调用其接口实现类型 B 的自身接口

例如:有一个 TreeDisplay 窗口组件用于展示目录树,现在要展示继承层次树

结构

类适配器:Adapter 同时继承 Target 和 Adaptee 对象适配器

参与者

  • Target(Shape):定义 Client 使用的与特定领域相关的接口。
  • Client(DrawingEditor):与符合 Target 接口的对象协同。
  • Adaptee(TextView):定义一个已经存在的接口,这个接口需要适配。
  • Adapter(TextShape):对 Adaptee 的接口与 Target 接口进行适配。

Client 调用 Adapter 的接口,然后 Adapter 调用 Adaptee 接口实现请求

适用情形

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

优缺点

类适配器和对象适配器互有优缺点:

  • 类适配器
    • 优点:Adapter 可以重定义 Adaptee 的部分行为,因为 Adapter 是 Adaptee 的一个子类。
    • 缺点:无法兼容该 Adaptee 的子类型
  • 对象适配器
    • 可以兼容 Adaptee 的子类型
    • 重定义 Adaptee 的行为较为困难(需要构建 Adaptee 的新子类,并在 Adapter 中引用该类型)

实现

使用 C++实现适配器类

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

C++中公共继承和私有继承的区别

  • 公共继承:是指派生类以 public 的方式继承基类,这样,
    • 基类的 public 成员和 protected 成员在派生类中保持原有的访问属性,而基类的 private 成员在派生类中不可见。
    • 公共继承可以实现基类到派生类的 is-a 关系,即派生类是基类的一种特殊类型,可以使用基类的指针或引用来操作派生类的对象
  • 私有继承:是指派生类以 private 的方式继承基类,这样,
    • 基类的 public 成员和 protected 成员在派生类中都变成 private 属性,而基类的 private 成员在派生类中不可见。
    • 私有继承可以实现基类到派生类的 has-a 关系,即派生类包含了基类的一个对象作为自己的一部分,但不能使用基类的指针或引用来操作派生类的对象
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// 目标接口
class Target {
public:
    virtual ~Target() {}
    virtual void request() = 0; // 抽象方法:请求
};

// 被适配类
class Adaptee {
public:
    void specificRequest() {
        std::cout << "Specific request" << std::endl; // 特殊的请求
    }
};

// 适配器类
class Adapter : public Target, private Adaptee {
public:
    void request() override {
        specificRequest(); // 调用被适配类的特殊请求方法
    }
};

// 客户端
int main() {
    Target* target = new Adapter(); // 创建一个适配器对象
    target->request(); // 调用目标接口的请求方法
    delete target; // 释放资源
    return 0;
}

双向透明适配器

双向适配器是一种可以实现两个不兼容接口之间的双向转换的适配器,它可以让两个接口的对象互相调用对方的方法,从而提供透明的操作。 双向适配器的实现方法是,在适配器类中同时持有两个接口的引用,然后实现两个接口的所有方法,每个方法中都调用对应的另一个接口的方法

例如,假设有一个电源接口 Power 和一个电池接口 Battery ,它们分别有 charge 和 supply 方法,我们可以定义一个双向适配器类 PowerBatteryAdapter 来实现两个接口之间的转换,代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// 电源接口
public interface Power {
    void charge(); // 充电方法
}

// 电池接口
public interface Battery {
    void supply(); // 供电方法
}

// 双向适配器类
public class PowerBatteryAdapter implements Power, Battery {
    private Power power; // 持有电源对象的引用
    private Battery battery; // 持有电池对象的引用

    public PowerBatteryAdapter(Power power, Battery battery) {
        this.power = power;
        this.battery = battery;
    }

    @Override
    public void charge() {
        battery.supply(); // 调用电池的供电方法
    }

    @Override
    public void supply() {
        power.charge(); // 调用电源的充电方法
    }
}

这样,我们就可以通过双向适配器来实现电源和电池之间的互相转换

可插入的适配器

可插入的适配器可以让客户端方便地调用可以变化的接口。 可插入的适配器的核心思想是,通过一个变量来保存方法,从而实现动态地变化方法。拥有这个变量的类的不同对象就可以拥有不同的方法。

实现方法:以 TreeDisplay 窗口组件自动地布置和显示层次式结构(包括目录树和继承树等)为例

  • 首先(这也是所有这三种实现都要做的)是为 Adaptee 找到一个“窄”接口,即可用于适配的最小操作集。这有 3 种实现途径:
    • 使用抽象操作:在 TreeDisplay 类中定义窄 Adaptee 接口相应的抽象操作,由子类来实现这些抽象操作并匹配具体的树结构的对象(例如,DirectoryTreeDisplay 子类将通过访问目录结构实现这些操作)
    • 使用代理对象:TreeDisplay 将访问树结构的请求转发到代理对象(delegate:委托)。
    • 参数化的适配器:根据不同的参数来调整其行为的适配器。参数化的适配器可以实现更灵活和通用的适配功能,比如根据不同的目标接口、不同的转换规则、不同的策略等来适配不同的对象。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// 定义一个被适配对象的类
class Adaptee {
public:
    void specificRequest() {
        cout << "This is a specific request from Adaptee" << endl;
    }
};

// 定义一个目标接口的类
class Target {
public:
    virtual void request() = 0;
};

// 定义一个参数化的适配器模板类,T1被适配者,T2是Target
template<class T1, class T2>
class Adapter : public T2 {
private:
    T1 adaptee; // 被适配对象
public:
    // 构造函数,接受一个被适配对象作为参数,并赋值给成员变量
    Adapter(T1 a) : adaptee(a) {}
    // 重载虚函数,调用被适配对象的特有方法
    void request() override {
        adaptee.specificRequest();
    }
};

int main() {
    // 创建一个被适配对象
    Adaptee a;
    // 创建一个参数化的适配器对象,传入被适配对象作为参数
    Adapter<Adaptee, Target> adapter(a);
    // 调用目标接口的方法
    adapter.request(); // 输出 This is a specific request from Adaptee
    return 0;
}

相关模式

模式 Bridge(4.2)的结构与对象适配器类似,但两者的出发点不同:

  • Bridge 的目的是将接口部分和实现部分分离,从而可以对它们较为容易也相对独立地加以改变。
  • Adapter 则意味着改变一个已有对象的接口。

Decorator(4.4)模式增强了其他对象的功能而同时又不改变它的接口,因此 Decorator 对应用程序的透明性比适配器要好。

  • 例如 Decorator 支持递归组合,而纯粹使用适配器是不可能实现这一点的。

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