适配器 Adapter

适配器模式是一种结构型设计模式,它能使接口不兼容的对象能够相互合作。


为什么要使用?

适配器模式的对象职责:

使接口不兼容的对象能够相互合作。适配器可以让不兼容的两个类一起协同工作,将类的接口转换为客户期望的另一个接口。

在实际开发中,我们可能会遇到一个无法修改的功能性服务类,如第三方、遗留系统或存在众多已有依赖的类。同时我们有一个或多个需要使用服务类的客户端类。我们需要让客户端类能够使用服务类的功能。但是客户端类和服务类不兼容。这时我们就可以使用适配器模式。

我们让客户端使用客户端接口来使用适配器服务类进行通信。这样一来就可以让我们不需要变更服务类的代码和不影响客户端代码的情况下修改扩展适配器。

举个例子:如果现在有一个充电宝🔋(客户端类),你要用这个充电宝去给苹果手机📱(被适配/ 服务类)充电,不过充电宝的 USB 接口🔌(客户端接口)和苹果接口无法兼容。你也无法修改苹果手机内部的充电接口。

那么这时候你可以用一条 USB 接口转苹果充电接口(适配器)的数据线来为手机充电。如果我们有新的充电接口呢?如 Type-C 接口。那么我们只需要用一条 USB 接口转 Type-C 接口的数据线即可。我们只需要更换不同数据线(适配器)就可以让充电宝给不同充电口的手机充电。


模式结构

  • 客户端 (Client) 是包含当前程序业务逻辑的类。

  • 客户端接口 (Client Interface) 描述了其他类与客户端代码合作时必须遵循的协议。

  • 服务 (Service) 中有一些功能类 (通常来自第三方或遗留系统)。 客户端与其接口不兼容, 因此无法直接调用其功能。

  • 适配器 (Adapter) 是一个可以同时与客户端和服务交互的类: 它在实现客户端接口的同时封装了服务对象。 适配器接受客户端通过适配器接口发起的调用, 并将其转换为适用于被封装服务对象的调用。

对象适配器

对象适配器的实现是让服务类作为适配器的属性,属于组合关系。所有流行的编程语言都可以实现适配器。

适配器模式的类图:

类适配器

类适配器的实现是让适配器类继承服务类,属于继承关系。这种方式仅能在支持多重继承的编程语言中实现, 例如 C++。

适配器模式的类图:


模式实现

该示例分别使用对象适配器和类适配器来获取时间类型(LongDate)。原来的接口都是返回数值,使用适配器模式将它们的调用方式改为直接打印在控制台。

示例程序的类图

代码实现

客户端接口
1
2
3
4
5
6
7
8
9
10
package example;

/** 客户端接口 */
public interface ClientInterface {
/**
* 根据类型获取时间并输出
* @param type “Long”,“Date”
*/
void print(String type);
}
适配器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package example;

/** 适配器 */
public class Adapter extends DateTypeTime implements ClientInterface {
/** 被适配器-组合关系 */
private LongTypeTime longTypeTime = new LongTypeTime();

/**
* 根据类型获取时间并输出
* @param type “Long”,“Date”
*/
@Override
public void print(String type) {
if ("Long".equals(type)) {
System.out.println(longTypeTime.getNowTime());
} else if ("Date".equals(type)) {
System.out.println(super.getNowTime());
} else {
System.out.println("无效类型");
}
}
}
服务(继承)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
package example;

import java.util.Date;

/** “Date”类型时间服务类,被适配类,使用继承的方式实现适配器模式 */
public class DateTypeTime {
/**
* 获取“Date”类型的当前时间
* @return 当前时间
*/
public Date getNowTime() {
return new Date(System.currentTimeMillis());
}
}
服务(组合/ 委托)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
package example;

import java.util.Date;

/** “Long”类型时间服务类,被适配类,使用组合/ 委托的方式实现适配器模式 */
public class LongTypeTime {
/**
* 获取“Long”类型的当前时间
* @return 当前时间
*/
public Long getNowTime() {
return new Date(System.currentTimeMillis()).getTime();
}
}

代码测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import example.Adapter;
import example.ClientInterface;

/** 测试适配器模式 */
public class Test {
public static void main(String[] args) {
ClientInterface adapter = new Adapter();

System.out.println("使用继承的方式实现适配器模式");
adapter.print("Date");

System.out.println("\n------------------- 分割线 -------------------\n");

System.out.println("使用组合/ 委托的方式实现适配器模式");
adapter.print("Long");
}
}

输出结果

1
2
3
4
5
6
7
使用继承的方式实现适配器模式
Sun Mar 05 16:10:59 CST 2023

------------------- 分割线 -------------------

使用组合/ 委托的方式实现适配器模式
1678003859670

对象适配器和类适配器都能实现适配器模式,不过在单继承的编程语言里,只能使用对象适配器。


常用场景和解决方案

  • 当你希望使用某个类,但是其接口与其他代码不兼容时,可以使用适配器类。或原有接口功能不满足现有要求,需要在兼容老接口的同时做适当的扩展。
  • 如果您需要复用这样一些类,他们处于同一个继承体系,并且他们又有了额外的一些共同的方法,但是这些共同的方法不是所有在这一继承体系中的子类所具有的共性。或将有相似性的多个不同接口之间做功能的统一。
  • 旧版本和新版本相互兼容。

模式的优缺点

优点 缺点
单一职责原则你可以将接口或数据转换代码从程序主要业务逻辑中分离。 代码整体复杂度增加,因为你需要新增一系列接口和类。有时直接更改服务类使其与其他代码兼容会更简单。
开闭原则。只要客户端代码通过客户端接口与适配器进行交互,你就能在不修改现有客户端代码的情况下在程序中添加新类型的适配器。
里氏替换原则。适配器类只要不影响客户端接口功能,具体被适配类无论使用什么新功能,都能很方便进行替换。

使用适配器模式的优势

  • 将目标类(客户端接口)和具体适配者类解耦。可以通过更换不同适配器去使用更多类。
  • 增加了类的透明性。修改的只是适配器类,客户端的调用逻辑不会被影响。换句话说适配器根据客户端接口进行变更,不会影响到客户端。
  • 统一多个类或接口。一个适配器类可以把多个不同的具体被适配类和子类都适配到同一个目标类上,如果目标类是新类,那么它就统一多个类或接口。

使用适配器模式的劣势

  • 一次只能适配一个抽象类或接口。面对单继承编程语言,一次就只能适配一个被适配者类。
  • 过度嵌套会导致接口臃肿,例如不断在一个目标类上适配适配器,就会让接口变得臃肿。
  • 目标接口依赖太多适配接口,修改目标接口会导致所以适配器都需要定制修改。

拓展知识

  • 适配器模式的适配器类最好采用私有继承的方式,以起到限定接口功能范围的作用。
  • 适配器可以对已有对象的接口进行修改,装饰模式则能在不改变对象接口的前提下强化对象功能。此外,装饰还支持递归组合,适配器则无法实现。
  • 适配器能为被封装对象提供不同的接口,代理模式能为对象提供相同的接口,装饰则能为对象提供加强的接口。


🔙 设计模式

📌最后:希望本文能够给您提供帮助,文章中有不懂或不正确的地方,请在下方评论区💬留言!

🔗参考文献:

🌐 设计模式 –refactoringguru

▶️ bilibili-趣学设计模式;黄靖锋. –拉勾教育

📖 图解设计模式 /(日)结城浩著;杨文轩译. –北京:人民邮电出版社,2017.1