适配器模式

设计模式笔记:适配器模式 (Adapter Pattern)

一、一句话概括

将一个类的接口转换成客户端所期望的另一种接口,使得原本因接口不兼容而无法一起工作的类可以协同工作。

二、为什么需要它 (Why - The Pain Point)

在软件开发中,我们经常会遇到这样的情况:

  1. 复用现有组件:你发现一个非常有用的、经过充分测试的第三方库或旧系统模块,但它的接口(API)与你当前系统所期望的接口完全不同。
  2. 系统集成:需要将两个或多个独立的系统集成在一起,但它们的通信接口不匹配。例如,一个系统使用 XML 格式交换数据,而另一个系统使用 JSON。
  3. 接口演化:随着系统迭代,某个类的接口可能需要重构,但为了不破坏依赖于旧接口的客户端代码,你需要提供一个过渡方案。

痛点:

直接修改现有组件的源代码来匹配新接口通常是不可行或不明智的,原因如下:

  • 无法访问源码:对于第三方库,你可能没有源代码。
  • 风险高:修改经过稳定运行的代码可能会引入新的 bug。
  • 违反开闭原则:修改现有代码违背了对修改关闭、对扩展开放的原则。

核心问题:如何在不修改任何一方代码的前提下,让两个接口不兼容的类能够“对话”和协作?

三、它是什么 (What - The Solution)

适配器模式引入了一个中间层——适配器(Adapter),它像一个翻译官或转接头,负责在两种不兼容的接口之间进行转换。

核心思想和角色:

  1. **目标接口 (Target)**:
    • 这是客户端代码所期望依赖的接口。客户端通过这个接口与适配器进行交互。
  2. **被适配者 (Adaptee)**:
    • 这是那个已经存在但接口不兼容的类。它是我们需要去“适配”的对象。
  3. **适配器 (Adapter)**:
    • 这是模式的核心。它实现了 Target 接口,因此可以被客户端调用。
    • 同时,它内部持有(包装)一个 Adaptee 对象的引用
    • 当客户端调用 Adapter 的方法时,Adapter 会在内部将这个请求转换成对 Adaptee 相应方法的调用。

两种实现方式:

  • 对象适配器 (Object Adapter) - 推荐,更常用
    • 通过组合(Composition) 实现:适配器类持有一个被适配者类的实例。
    • 优点:更灵活。一个适配器可以适配一个类及其所有子类。
  • 类适配器 (Class Adapter)
    • 通过多重继承(Multiple Inheritance) 实现:适配器类同时继承 Target 类(或实现接口)和 Adaptee 类。
    • 在 Java 中,由于不支持类的多重继承,通常通过“实现一个接口,同时继承一个类”的方式来模拟。
    • 优点:可以直接重写 Adaptee 的方法。
    • 缺点:耦合度更高,不灵活。只能适配一个具体的 Adaptee 类,不能适配其子类。

C++ 背景对比 & Java 关键点:

  • 与C++的联系:C++ 可以同时支持类适配器(通过多重继承)和对象适配器(通过组合)。
  • Java 的实现精髓
    • 由于 Java 的单继承特性,对象适配器模式是 Java 世界中的绝对主流。它完美体现了“组合优于继承”的原则。
    • 类适配器在 Java 中使用较少,且有局限性(目标必须是接口)。

四、怎么做 (How - The Blueprint)

基本步骤(以对象适配器为例):

假设我们有一个旧的日志库 LegacyLogger,它有一个 logMessage(String msg) 方法。而我们的新系统要求所有日志组件都实现 ILogger 接口,该接口有一个 log(String level, String message) 方法。

  1. **确定目标接口 Target**:ILogger 接口。
  2. **确定被适配者 Adaptee**:LegacyLogger 类。
  3. 创建适配器类 LoggerAdapter:
    • 让它实现 ILogger 接口。
    • 在内部持有一个 LegacyLogger 的实例。
    • 在实现的 log(level, message) 方法中,将 levelmessage 组合成一个字符串,然后调用 LegacyLogger 实例的 logMessage() 方法。
  4. 客户端现在可以通过 ILogger 接口使用 LoggerAdapter,而 LoggerAdapter 在背后默默地使用了 LegacyLogger

Java 示例代码 (对象适配器)

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
// 1. 目标接口 (Target) - 新系统期望的接口
interface ILogger {
void log(String level, String message);
}

// 具体的目标接口实现,我们自己的新日志系统
class NewLogger implements ILogger {
@Override
public void log(String level, String message) {
System.out.println("NewLogger [" + level + "]: " + message);
}
}

// 2. 被适配者 (Adaptee) - 已存在的、接口不兼容的类
class LegacyLogger {
public void logMessage(String message) {
System.out.println("LegacyLogger: " + message);
}

public void logError(String errorMessage, int errorCode) {
System.err.println("LegacyLogger ERROR " + errorCode + ": " + errorMessage);
}
}

// 3. 适配器 (Adapter)
class LoggerAdapter implements ILogger {
// 内部持有一个被适配者的实例
private final LegacyLogger legacyLogger;

public LoggerAdapter(LegacyLogger legacyLogger) {
this.legacyLogger = legacyLogger;
}

// 实现目标接口的方法
@Override
public void log(String level, String message) {
// 在这里进行转换逻辑
if ("ERROR".equalsIgnoreCase(level)) {
// 调用 Adaptee 的另一个方法
legacyLogger.logError(message, 500);
} else {
// 将新的调用方式适配到旧的接口上
String formattedMessage = level.toUpperCase() + " - " + message;
legacyLogger.logMessage(formattedMessage);
}
}
}

// 4. 客户端
public class AdapterPatternDemo {
public static void main(String[] args) {
// 客户端代码依赖于 ILogger 接口
ILogger newLogger = new NewLogger();
newLogger.log("INFO", "This is a standard log message.");

System.out.println("--- Integrating legacy logger ---");

// 我们想要复用 LegacyLogger,但它的接口不兼容
LegacyLogger legacy = new LegacyLogger();

// 使用适配器进行包装
ILogger adaptedLogger = new LoggerAdapter(legacy);

// 现在客户端可以用同样的方式使用适配器了
adaptedLogger.log("WARN", "This is a warning from the adapted logger.");
adaptedLogger.log("ERROR", "This is a critical error!");
}
}

五、UML 类图

下面是上述例子的 PlantUML 代码 (对象适配器)。

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
@startuml
skinparam classAttributeIconSize 0
hide empty members

' Target Interface
interface ILogger {
+ void log(String level, String message)
}

' Adaptee Class
class LegacyLogger {
+ void logMessage(String message)
+ void logError(String msg, int code)
}

' Adapter Class
class LoggerAdapter implements ILogger {
- LegacyLogger legacyLogger
+ LoggerAdapter(LegacyLogger logger)
+ void log(String level, String message)
}

' Client
class AdapterPatternDemo {
+ {static} void main(String[] args)
}

' Relationships
' Adapter HAS-A Adaptee (Composition)
LoggerAdapter o-- "1" LegacyLogger

' Client USES Target interface
AdapterPatternDemo ..> ILogger : uses

@enduml

图示解读:

  • LoggerAdapter 实现了 ILogger 接口(继承关系)。
  • LoggerAdapter 包含一个 LegacyLogger 的实例(组合关系 o--)。
  • 客户端 AdapterPatternDemo 只依赖于 ILogger 接口,对 LegacyLogger 的存在一无所知。

六、优缺点分析

优点 (Pros):

  1. 复用性:可以复用现有的类,而无需修改其源代码。
  2. 解耦:将客户端与被适配的类解耦,客户端无需知道被适配类的具体实现。
  3. 符合开闭原则:可以在不修改现有代码的情况下,引入新的适配器来集成新的组件。
  4. 单一职责:适配器只负责接口转换,职责单一明确。

缺点 (Cons):

  1. 增加复杂性:引入了额外的适配器类,增加了系统的复杂度和类的数量。对于简单的转换,可能会觉得有点“小题大做”。

七、在 Java 中的应用

适配器模式在 Java 中非常常见,是连接不同 API 的标准做法。

  • java.util.Arrays.asList(): 这个方法是一个绝佳的例子。它接受一个数组(Adaptee)并返回一个 ListTarget)。返回的 List 对象就是一个适配器,它包装了原始数组,使得你可以用 List 的接口来操作数组。但这个适配器有局限性,比如不支持 add()remove(),因为底层数组的长度是固定的。
  • java.io 中的 InputStreamReaderOutputStreamWriter:
    • InputStreamReader 是一个适配器,它将 InputStreamAdaptee,处理字节流)适配成 ReaderTarget,处理字符流)。
    • OutputStreamWriter 则是将 OutputStream 适配成 Writer
      它们是连接字节世界和字符世界的桥梁。
  • SLF4J (Simple Logging Facade for Java): SLF4J 的日志桥接包(如 log4j-to-slf4j)就是适配器模式的体现。它提供一个与旧日志库(如 Log4j)API 相同的适配器,但内部将调用转发到 SLF4J 的 API 上,从而实现日志框架的平滑迁移。