摘要生成中...
AI 摘要
Hunyuan-lite

image.png

适配器模式也被称为 Wrapper 模式。Wrapper 有「包装器」的意思,就像用精美的包装纸将普通商品包装成礼物那样,替我们把某样东西包起来,使其能够用于其他用途的东西就被称为「包装器」或是「适配器」。

登场角色:

  • Target(对象):定义所需的方法。
  • Client(请求者):负责使用 Target 角色所定义的方法进行具体处理。
  • Adaptee(被适配):持有既定方法的角色。如果 Adaptee 角色中的方法与 Target 角色的方法相同,就不需要 Adapter 角色了。
  • Adapter(适配):使用 Adaptee 角色的方法来满足 Target 角色的需求。在类适配器中,Adapter 角色通过继承来使用 Adaptee 角色,而在对象适配器模式中,Adapter 角色通过委托来使用 Adaptee 角色。

Adaptee 角色和 Target 角色的功能完全不同时,适配器模式是无法使用的。就如同我们无法用交流 100 伏特电压让自来水管出水一样。

适配器模式会对现有的类进行适配,生成新的类。通过该模式可以很方便地创建我们需要的方法群。当出现 Bug 时,由于我们很明确地知道 Bug 不在现有的类(Adaptee 角色)中,所以只需调查扮演 Adapter 角色的类即可。这样一来,代码问题的排查就会变得非常简单。

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

两种适配器模式

Adapter 模式有以下两种。

  • 类适配器模式(使用继承的适配器)。Adapter 自己成了 Adaptee 的子类,实现 Target 接口。
  • 对象适配器模式(使用委托的适配器)。Adaptee 成了 Target 的子类,委托 Adaptee

类适配器模式的类图(使用继承)

image.png

类适配器模式中,如果 Target 接口中定义了一些 Adaptee 原本就有的方法(且这些方法不需要改动),Adapter 可以不实现这些方法。

对象适配器模式的类图(使用委托)

image.png

Client 类中的使用:Target t=new Adapter()

适配器模式类型的选用

判断的标准主要有两个,一个是 Adaptee 接口的个数,另一个是 AdapteeTarget 的契合程度。

  • 如果 Adaptee 接口并不多,那两种实现方式都可以。
  • 如果 Adaptee 接口很多,而且 AdapteeTarget 接口定义大部分都相同,那就推荐使用类适配器,因为 Adaptor 复用父类 Adaptee 的接口,比起对象适配器的实现方式,Adaptor 的代码量要少一些。
  • 如果 Adaptee 接口很多,而且 AdapteeTarget 接口定义大部分都不相同,那就推荐使用对象适配器,因为组合结构相对于继承更加灵活。

应用场景

适配器模式可以看作一种「补偿模式」,用来补救设计上的缺陷。应用这种模式算是无奈之举。如果在设计初期,我们就能协调规避接口不兼容的问题,那这种模式就没有应用的机会了。

封装有缺陷的接口设计

使用适配器模式可以在完全不改变现有代码的前提下使现有代码适配于新的接口(API)。此外,在适配器模式中,并非一定需要现成的代码。只要知道现有类的功能,就可以编写出新的类。

假设我们依赖的外部系统在接口设计方面有缺陷(比如包含大量静态方法),引入之后会影响到我们自身代码的可测试性。为了隔离设计上的缺陷,我们希望对外部系统提供的接口进行二次封装,抽象出更好的接口设计,这个时候就可以使用适配器模式了。

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
38
public class CD { //这个类来自外部sdk,我们无权修改它的代码
//...
public static void staticFunction1() { //... }

public void uglyNamingFunction2() { //... }

public void tooManyParamsFunction3(int paramA, int paramB, ...) { //... }

public void lowPerformanceFunction4() { //... }
}

// 使用适配器模式进行重构
public class ITarget {
void function1();
void function2();
void fucntion3(ParamsWrapperDefinition paramsWrapper);
void function4();
//...
}
// 注意:适配器类的命名不一定非得末尾带Adaptor
public class CDAdaptor extends CD implements ITarget {
//...
public void function1() {
super.staticFunction1();
}

public void function2() {
super.uglyNamingFucntion2();
}

public void function3(ParamsWrapperDefinition paramsWrapper) {
super.tooManyParamsFunction3(paramsWrapper.getParamA(), ...);
}

public void function4() {
//...reimplement it...
}
}

统一多个类的接口设计

某个功能的实现依赖多个外部系统(或者说类)。通过适配器模式,将它们的接口适配为统一的接口定义,然后我们就可以使用多态的特性来复用代码逻辑。

假设我们的系统要对用户输入的文本内容做敏感词过滤,为了提高过滤的召回率,我们引入了多款第三方敏感词过滤系统,依次对用户输入的内容进行过滤,过滤掉尽可能多的敏感词。但是,每个系统提供的过滤接口都是不同的。这就意味着我们没法复用一套逻辑来调用各个系统。这个时候,我们就可以使用适配器模式,将所有系统的接口适配为统一的接口定义,这样我们可以复用调用敏感词过滤的代码。

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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
public class ASensitiveWordsFilter { // A敏感词过滤系统提供的接口
//text是原始文本,函数输出用***替换敏感词之后的文本
public String filterSexyWords(String text) {
// ...
}

public String filterPoliticalWords(String text) {
// ...
}
}

public class BSensitiveWordsFilter { // B敏感词过滤系统提供的接口
public String filter(String text) {
//...
}
}

public class CSensitiveWordsFilter { // C敏感词过滤系统提供的接口
public String filter(String text, String mask) {
//...
}
}

// 未使用适配器模式之前的代码:代码的可测试性、扩展性不好
public class RiskManagement {
private ASensitiveWordsFilter aFilter = new ASensitiveWordsFilter();
private BSensitiveWordsFilter bFilter = new BSensitiveWordsFilter();
private CSensitiveWordsFilter cFilter = new CSensitiveWordsFilter();

public String filterSensitiveWords(String text) {
String maskedText = aFilter.filterSexyWords(text);
maskedText = aFilter.filterPoliticalWords(maskedText);
maskedText = bFilter.filter(maskedText);
maskedText = cFilter.filter(maskedText, "***");
return maskedText;
}
}

// 使用适配器模式进行改造
public interface ISensitiveWordsFilter { // 统一接口定义
String filter(String text);
}

public class ASensitiveWordsFilterAdaptor implements ISensitiveWordsFilter {
private ASensitiveWordsFilter aFilter;
public String filter(String text) {
String maskedText = aFilter.filterSexyWords(text);
maskedText = aFilter.filterPoliticalWords(maskedText);
return maskedText;
}
}
//...省略BSensitiveWordsFilterAdaptor、CSensitiveWordsFilterAdaptor...

// 扩展性更好,更加符合开闭原则,如果添加一个新的敏感词过滤系统,
// 这个类完全不需要改动;而且基于接口而非实现编程,代码的可测试性更好。
public class RiskManagement {
private List<ISensitiveWordsFilter> filters = new ArrayList<>();

public void addSensitiveWordsFilter(ISensitiveWordsFilter filter) {
filters.add(filter);
}

public String filterSensitiveWords(String text) {
String maskedText = text;
for (ISensitiveWordsFilter filter : filters) {
maskedText = filter.filter(maskedText);
}
return maskedText;
}
}

替换依赖的外部系统

当我们把项目中依赖的一个外部系统替换为另一个外部系统的时候,利用适配器模式,可以减少对代码的改动。

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
// 外部系统A
public interface IA {
//...
void fa();
}

public class A implements IA {
//...
public void fa() { //... }
}

// 在我们的项目中,外部系统A的使用示例
public class Demo {
private IA a;
public Demo(IA a) {
this.a = a;
}
//...
}
Demo demo = new Demo(new A());

// 将外部系统A替换成外部系统B
public class BAdaptor implemnts IA {
private B b;
public BAdaptor(B b) {
this.b= b;
}
public void fa() {
//...
b.fb();
}
}
// 借助BAdaptor,Demo的代码中,调用IA接口的地方都无需改动,
// 只需要将BAdaptor如下注入到Demo即可。
Demo demo = new Demo(new BAdaptor(new B()));

兼容老版本接口

在做版本升级的时候,对于一些要废弃的接口,我们不直接将其删除,而是暂时保留,并且标注为 deprecated,并将内部实现逻辑委托为新的接口实现。这样做的好处是,让使用它的项目有个过渡期,而不是强制进行代码修改。这也可以粗略地看作适配器模式的一个应用场景。

image.png

适配不同格式的数据

适配器模式除了用于接口的匹配,它还可以用在不同格式的数据之间的适配。比如,Java 中的 Arrays.asList() 也可以看作一种数据适配器,将数组类型的数据转化为集合容器类型。

1
List<String> stooges = Arrays.asList("Larry", "Moe", "Curly");

Slf4j 中的适配器模式

Java 中有很多日志框架,在项目开发中,我们常常用它们来打印日志信息。其中,比较常用的有 log4j、logback,以及 JDK 提供的 JUL(java.util.logging)和 Apache 的 JCL(Jakarta Commons Logging)等。

大部分日志框架都提供了相似的功能,比如按照不同级别(debug、info、warn、error……)打印日志等,但它们却并没有实现统一的接口。这主要是历史的原因,它不像 JDBC 那样,一开始就制定了数据库操作的接口规范。

Slf4j 日志框架相当于 JDBC 规范,提供了一套打印日志的统一接口规范。不过,它只定义了接口,并没有提供具体的实现,需要配合其他日志框架(log4j、logback……)来使用。

Slf4j 的出现晚于 JUL、JCL、log4j 等日志框架,所以,这些日志框架也不可能牺牲掉版本兼容性,将接口改造成符合 Slf4j 接口规范。Slf4j 也事先考虑到了这个问题,所以,它不仅仅提供了统一的接口定义,还提供了针对不同日志框架的适配器。

在开发业务系统或者开发框架、组件的时候,我们统一使用 Slf4j 提供的接口来编写打印日志的代码,具体使用哪种日志框架实现(log4j、logback……),是可以动态地指定的(比如使用 Java 的 SPI 技术),只需要将相应的 SDK 导入到项目中即可。

如果一些老的项目没有使用 Slf4j,而是直接使用比如 JCL 来打印日志,那如果想要替换成其他日志框架,比如 log4j,也是可以的。Slf4j 不仅仅提供了从其他日志框架到 Slf4j 的适配器,还提供了反向适配器,即从 Slf4j 到其他日志框架的适配。我们可以先将 JCL 切换为 Slf4j,然后再将 Slf4j 切换为 log4j。经过两次适配器的转换,我们就能成功将 log4j 切换为了 logback。

设计模式比较

相关设计模式:

  • 站内文章门面模式:外观模式为现有对象定义了一个新接口,适配器则会试图运用已有的接口。适配器通常只封装一个对象,外观通常会作用于整个对象子系统上。

代理、桥接、装饰器、适配器的区别

站内文章代理模式站内文章桥接模式站内文章装饰器模式、适配器模式,这 4 种模式是比较常用的结构型设计模式,它们的代码结构非常相似,都使用了组合。笼统来说,它们都可以称为 Wrapper 模式,也就是通过 Wrapper 类二次封装原始类。

尽管代码结构相似,但这 4 种设计模式的用意完全不同,也就是不同模式要解决的问题、应用场景不同,这也是它们的主要区别。

设计模式 代理模式 桥接模式 装饰器模式 适配器模式
模式概览 不改变原始类接口的条件下,为原始类定义一个代理类。控制访问,而非加强功能。 将接口部分和实现部分分离,提高类的可扩展性 不改变原始类接口的情况下,对原始类功能进行增强,并且支持多个装饰器的嵌套使用 事后的补救策略。适配器提供跟原始类不同的接口。
提供的接口和原始类是否相同 ✅但是附加的是和原始类无关的功能 - ✅对原始类相关功能进行增强 ❌对已有对象的接口进行修改,为被封装对象提供不同的接口
关于不同接口的连接 - 用于连接类的功能层次结构与实现层次结构 - 用于连接接口(API)不同的类,填补不同接口之间的缝隙
开发时机 开发前期进行设计,将程序的各个部分独立开来以便开发。 通常在已有程序中使用,让相互不兼容的类能很好地合作。
递归组合 -

装饰器模式和代理模式这两个模式的构建都基于组合原则,也就是说一个对象应该将部分工作委派给另一个对象。不同之处核心点在于设计初衷和设计目的:

  • 装饰器模式的生成总是由客户端进行控制。装饰器模式旨在增强 / 扩展目标对象的核心功能,关注对目标对象自身业务逻辑的补充、修改或强化。
  • 代理通常自行管理其服务对象的生命周期,控制对目标对象的访问,关注在调用目标对象前后做 「非业务性的通用处理」,目标对象的核心业务逻辑完全不变。

image.png

桥接模式、状态模式和策略模式(在某种程度上包括适配器)模式的接口非常相似。 实际上,它们都基于 站内文章组合模式——即将工作委派给其他对象, 不过也各自解决了不同的问题。模式并不只是以特定方式组织代码的配方,我们还可以使用它们来和其他开发者讨论模式所解决的问题。

本文 PlantUML 归档

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Client

interface Target{
{method} {abstract} targetMethod1
{method} {abstract} targetMethod2
}

class Adapter{
{method} targetMethod1
{method} targetMethod2
}

class Adaptee{
{method} methodA
{method} methodB
{method} methodC
}

Client --> Target : Uses
Target <|. Adapter : implements
Adapter -|> Adaptee : extends

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Client

abstract class Target{
{method} {abstract} targetMethod1
{method} {abstract} targetMethod2
}

class Adapter{
adaptee
{method} targetMethod1
{method} targetMethod2
}

class Adaptee{
{method} methodA
{method} methodB
{method} methodC
}

Client --> Target : Uses
Target <|- Adapter : extends
Adapter o-> Adaptee : has

本文参考