适配器模式
适配器模式
Rif设计模式笔记:适配器模式 (Adapter Pattern)
一、一句话概括
将一个类的接口转换成客户端所期望的另一种接口,使得原本因接口不兼容而无法一起工作的类可以协同工作。
二、为什么需要它 (Why - The Pain Point)
在软件开发中,我们经常会遇到这样的情况:
- 复用现有组件:你发现一个非常有用的、经过充分测试的第三方库或旧系统模块,但它的接口(API)与你当前系统所期望的接口完全不同。
- 系统集成:需要将两个或多个独立的系统集成在一起,但它们的通信接口不匹配。例如,一个系统使用 XML 格式交换数据,而另一个系统使用 JSON。
- 接口演化:随着系统迭代,某个类的接口可能需要重构,但为了不破坏依赖于旧接口的客户端代码,你需要提供一个过渡方案。
痛点:
直接修改现有组件的源代码来匹配新接口通常是不可行或不明智的,原因如下:
- 无法访问源码:对于第三方库,你可能没有源代码。
- 风险高:修改经过稳定运行的代码可能会引入新的 bug。
- 违反开闭原则:修改现有代码违背了对修改关闭、对扩展开放的原则。
核心问题:如何在不修改任何一方代码的前提下,让两个接口不兼容的类能够“对话”和协作?
三、它是什么 (What - The Solution)
适配器模式引入了一个中间层——适配器(Adapter),它像一个翻译官或转接头,负责在两种不兼容的接口之间进行转换。
核心思想和角色:
- **目标接口 (Target)**:
- 这是客户端代码所期望和依赖的接口。客户端通过这个接口与适配器进行交互。
- **被适配者 (Adaptee)**:
- 这是那个已经存在但接口不兼容的类。它是我们需要去“适配”的对象。
- **适配器 (Adapter)**:
- 这是模式的核心。它实现了
Target接口,因此可以被客户端调用。 - 同时,它内部持有(包装)一个
Adaptee对象的引用。 - 当客户端调用
Adapter的方法时,Adapter会在内部将这个请求转换成对Adaptee相应方法的调用。
- 这是模式的核心。它实现了
两种实现方式:
- 对象适配器 (Object Adapter) - 推荐,更常用
- 通过组合(Composition) 实现:适配器类持有一个被适配者类的实例。
- 优点:更灵活。一个适配器可以适配一个类及其所有子类。
- 类适配器 (Class Adapter)
- 通过多重继承(Multiple Inheritance) 实现:适配器类同时继承
Target类(或实现接口)和Adaptee类。 - 在 Java 中,由于不支持类的多重继承,通常通过“实现一个接口,同时继承一个类”的方式来模拟。
- 优点:可以直接重写
Adaptee的方法。 - 缺点:耦合度更高,不灵活。只能适配一个具体的
Adaptee类,不能适配其子类。
- 通过多重继承(Multiple Inheritance) 实现:适配器类同时继承
C++ 背景对比 & Java 关键点:
- 与C++的联系:C++ 可以同时支持类适配器(通过多重继承)和对象适配器(通过组合)。
- Java 的实现精髓:
- 由于 Java 的单继承特性,对象适配器模式是 Java 世界中的绝对主流。它完美体现了“组合优于继承”的原则。
- 类适配器在 Java 中使用较少,且有局限性(目标必须是接口)。
四、怎么做 (How - The Blueprint)
基本步骤(以对象适配器为例):
假设我们有一个旧的日志库 LegacyLogger,它有一个 logMessage(String msg) 方法。而我们的新系统要求所有日志组件都实现 ILogger 接口,该接口有一个 log(String level, String message) 方法。
- **确定目标接口
Target**:ILogger接口。 - **确定被适配者
Adaptee**:LegacyLogger类。 - 创建适配器类
LoggerAdapter:- 让它实现
ILogger接口。 - 在内部持有一个
LegacyLogger的实例。 - 在实现的
log(level, message)方法中,将level和message组合成一个字符串,然后调用LegacyLogger实例的logMessage()方法。
- 让它实现
- 客户端现在可以通过
ILogger接口使用LoggerAdapter,而LoggerAdapter在背后默默地使用了LegacyLogger。
Java 示例代码 (对象适配器)
1 | // 1. 目标接口 (Target) - 新系统期望的接口 |
五、UML 类图
下面是上述例子的 PlantUML 代码 (对象适配器)。
1 | @startuml |
图示解读:
LoggerAdapter实现了ILogger接口(继承关系)。LoggerAdapter包含一个LegacyLogger的实例(组合关系o--)。- 客户端
AdapterPatternDemo只依赖于ILogger接口,对LegacyLogger的存在一无所知。
六、优缺点分析
优点 (Pros):
- 复用性:可以复用现有的类,而无需修改其源代码。
- 解耦:将客户端与被适配的类解耦,客户端无需知道被适配类的具体实现。
- 符合开闭原则:可以在不修改现有代码的情况下,引入新的适配器来集成新的组件。
- 单一职责:适配器只负责接口转换,职责单一明确。
缺点 (Cons):
- 增加复杂性:引入了额外的适配器类,增加了系统的复杂度和类的数量。对于简单的转换,可能会觉得有点“小题大做”。
七、在 Java 中的应用
适配器模式在 Java 中非常常见,是连接不同 API 的标准做法。
java.util.Arrays.asList(): 这个方法是一个绝佳的例子。它接受一个数组(Adaptee)并返回一个List(Target)。返回的List对象就是一个适配器,它包装了原始数组,使得你可以用List的接口来操作数组。但这个适配器有局限性,比如不支持add()和remove(),因为底层数组的长度是固定的。java.io中的InputStreamReader和OutputStreamWriter:InputStreamReader是一个适配器,它将InputStream(Adaptee,处理字节流)适配成Reader(Target,处理字符流)。OutputStreamWriter则是将OutputStream适配成Writer。
它们是连接字节世界和字符世界的桥梁。
- SLF4J (Simple Logging Facade for Java): SLF4J 的日志桥接包(如
log4j-to-slf4j)就是适配器模式的体现。它提供一个与旧日志库(如 Log4j)API 相同的适配器,但内部将调用转发到 SLF4J 的 API 上,从而实现日志框架的平滑迁移。