深入浅出设计模式【六、适配器模式】
一、适配器模式介绍
适配器模式,又称为包装器(Wrapper),是一种结构型设计模式。它旨在将一个类的接口转换成客户端期望的另一个接口。
在软件开发中,我们经常遇到这样的问题:我们想使用一个现有的类,它的功能完全符合需求,但它的接口(方法名、参数列表等)与客户端期望使用的接口不兼容。如果直接修改这个现有类的接口,可能会破坏其现有使用者,并且违背“开闭原则”。适配器模式正是在不修改现有代码的前提下,通过增加一个中间层(适配器)来解决接口不兼容的问题。
二、核心概念与意图
-
核心概念:
- 目标接口 (Target): 客户端期望使用的接口。它定义了客户端需要调用的方法。
- 被适配者 (Adaptee): 已经存在的、功能强大但接口不兼容的类或组件。它是需要被“包装”和“转换”的对象。
- 适配器 (Adapter): 模式的核心。它实现了目标接口,并持有一个被适配者的实例。适配器通过调用被适配者的方法,并在其上进行转换和包装,使得客户端可以按照目标接口的方式调用被适配者的功能。
-
意图:
- 将一个类的接口转换成客户希望的另外一个接口。
- 使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。
- 促进代码复用,允许那些接口不兼容的现有类参与到新的系统中来。
三、适用场景剖析
适配器模式在以下场景中非常有效:
- 集成遗留系统或第三方库: 当需要使用一个现有的类,但其接口与你的系统其他部分不兼容时。修改遗留代码或第三方库的代码通常是高风险或不可行的,适配器是完美的解决方案。
- 系统升级和重构: 在系统升级过程中,新版本组件的接口可能与老版本不同。为了保持向后兼容性,可以编写适配器,让新组件呈现出老组件一样的接口,从而平滑过渡。
- 统一多个类的接口: 当系统需要依赖多个功能相似但接口不同的类时,可以使用多个适配器将它们转换成统一的接口,从而简化客户端的调用逻辑。这为未来可能使用外观模式或策略模式奠定了基础。
- 接口最小化: 有时一个类提供的接口比我们实际需要的要多。可以创建一个适配器,只暴露我们需要的接口,起到简化和安全的作用(有时也称为“接口最小化”模式)。
四、UML 类图解析
以下Mermaid类图清晰地展示了适配器模式(对象适配器)的结构和角色间的关系:
Target: 目标接口,定义了客户端期望的方法(如request())。Client: 与符合Target接口的对象协同工作的类。Adaptee: 被适配者,包含真正有用的功能,但接口(如specificRequest())与Target不兼容。Adapter: 适配器类。它实现了Target接口,并持有一个Adaptee对象的引用。当客户端调用adapter.request()时,适配器内部会调用adaptee.specificRequest(),并可能进行一些数据格式的转换,最终将结果返回给客户端。
调用流程: Client -> Target.request() -> (Adapter.request() -> Adaptee.specificRequest())。
五、各种实现方式及其优缺点
适配器模式主要有两种实现方式:类适配器和对象适配器。
1. 对象适配器(组合方式,优先使用)
如上图所示,适配器通过组合(持有引用) 的方式持有被适配者。这是更灵活、更常用的方式,也是GoF推荐的方式。
// 目标接口
public interface MediaPlayer {
void play(String audioType, String fileName);
}
// 被适配者
public class AdvancedMediaPlayer {
public void playMp4(String fileName) {
System.out.println("Playing mp4 file: " + fileName);
}
public void playVlc(String fileName) {
System.out.println("Playing vlc file: " + fileName);
}
}
// 适配器
public class MediaAdapter implements MediaPlayer {
// 组合:持有被适配者的引用
private AdvancedMediaPlayer advancedMusicPlayer;
public MediaAdapter(AdvancedMediaPlayer advancedMusicPlayer) {
this.advancedMusicPlayer = advancedMusicPlayer;
}
@Override
public void play(String audioType, String fileName) {
if (audioType.equalsIgnoreCase("vlc")) {
advancedMusicPlayer.playVlc(fileName); // 转换调用
} else if (audioType.equalsIgnoreCase("mp4")) {
advancedMusicPlayer.playMp4(fileName); // 转换调用
}
}
}
// 客户端
public class Client {
public static void main(String[] args) {
AdvancedMediaPlayer adaptee = new AdvancedMediaPlayer();
MediaPlayer adapter = new MediaAdapter(adaptee); // 注入被适配者
adapter.play("mp4", "movie.mp4"); // 客户端使用目标接口调用
}
}
- 优点:
- 更灵活: 一个适配器可以适配一个类及其所有子类。
- 符合组合优于继承原则: 使用组合,降低了与适配类的耦合度。
- 缺点:
- 需要编写少量额外的代码来维护对被适配者的引用。
2. 类适配器(继承方式)
适配器通过继承被适配者类来实现。这种方式需要支持多重继承的语言(如C++),在Java中无法实现真正的类适配器(因为Java是单继承),除非适配器同时继承被适配者和实现目标接口,但这要求目标必须是类而不是接口,限制了灵活性。
// 假设Java支持多重继承(实际不支持,此处为概念演示)
public class MediaAdapter extends AdvancedMediaPlayer implements MediaPlayer {
@Override
public void play(String audioType, String fileName) {
if (audioType.equalsIgnoreCase("vlc")) {
super.playVlc(fileName); // 直接调用父类方法
} else if (audioType.equalsIgnoreCase("mp4")) {
super.playMp4(fileName); // 直接调用父类方法
}
}
}
- 优点:
- 由于直接继承,适配器可以重写被适配者的行为。
- 缺点:
- 不灵活: 只能适配一个特定的类,不能适配其子类。
- 高耦合: 适配器与被适配者紧密耦合。
结论: 在Java中,始终优先使用对象适配器(组合)。它更符合设计原则,也更加灵活。
六、最佳实践
- 遵循“组合优于继承”原则: 始终使用对象适配器,它更灵活,并能降低系统的耦合度。
- 保持适配器职责单一: 一个适配器最好只用于适配一个特定的被适配者到一个特定的目标接口。不要试图创建一个“万能适配器”。
- 接口设计应保持稳定:
Target接口应该是稳定且设计良好的,因为它是客户端直接依赖的契约。 - 可考虑双向适配器: 在极少数需要双向转换的场景下,可以创建一个适配器同时实现两个接口,使其既能当作A接口使用,也能当作B接口使用。
- 与外观模式区分:
- 适配器模式: 主要目的是转换接口,解决接口不兼容的问题。它通常包装一个对象。
- 外观模式: 主要目的是简化接口,提供一个更高层次、更简单的接口来访问子系统中的一组接口。它通常包装一组对象。
七、在开发中的演变和应用
适配器模式的思想在现代开发中无处不在,并演变为各种形式:
- 依赖注入(DI)与适配器: 在现代框架如Spring中,适配器通常被声明为Bean,并通过依赖注入容器将其注入到需要
Target接口的客户端中。这使得配置和替换适配器变得非常容易。 - 微服务网关/API网关: 在微服务架构中,API网关 本质上是一个巨大的适配器。它将外部各种不同的客户端请求(HTTP, gRPC, WebSocket等)转换成内部各个微服务能理解的协议和格式。
- 防腐层(Anti-Corruption Layer, ACL): 在领域驱动设计(DDD)中,防腐层用于隔离外部系统或遗留系统对核心领域模型的侵蚀。防腐层的实现常常大量使用适配器模式,将外部模型转换和适配成领域内部模型。
八、真实开发案例(Java语言内部、知名开源框架、工具)
-
Java Collections API -
Arrays.asList():String[] array = {"a", "b", "c"}; List<String> list = Arrays.asList(array);这里的
Arrays.asList()返回的List就是一个适配器!它将一个数组(Adaptee)适配成了List接口(Target)。注意,这个适配器是“视图”,对列表的操作会直接反映到数组上。 -
Java I/O API:
// InputStreamReader 是适配器,InputStream 是被适配者,Reader 是目标接口。 InputStream inputStream = new FileInputStream("file.txt"); Reader reader = new InputStreamReader(inputStream, "UTF-8"); // 现在可以使用Reader接口来读取字节流了InputStreamReader适配了InputStream(字节流)到Reader(字符流)的接口。
同样,OutputStreamWriter是OutputStream到Writer的适配器。 -
Spring MVC -
HandlerAdapter:
这是框架中极其经典的适配器应用。Spring MVC 的DispatcherServlet并不直接处理各种控制器(Controller)。因为控制器有多种实现方式(如基于@Controller注解、实现Controller接口、基于HttpRequestHandler等),它们的接口各不相同。
HandlerAdapter接口(Target)定义了统一的处理方式。对于每种控制器类型,都有一个对应的适配器实现(如AnnotationMethodHandlerAdapter,SimpleControllerHandlerAdapter)。
DispatcherServlet(Client)通过HandlerAdapter接口调用handle()方法,而适配器负责调用具体控制器的不同方法。这完美体现了适配器模式的价值。 -
Spring AOP -
MethodInterceptor:
Spring AOP 中的通知(Advice)(如MethodBeforeAdvice)需要被适配到 AOP 联盟标准的MethodInterceptor接口。Spring 使用了一系列适配器(如MethodBeforeAdviceAdapter)来完成这个转换。 -
JPA(Hibernate):
JPA 是一个标准接口(Target),而 Hibernate 是一个具体实现(Adaptee)。应用程序代码基于 JPA 接口编写,Hibernate 的 JPA 实现本质上就是一个庞大的适配器层,将 JPA 的调用适配成 Hibernate 自身的原生 API。
九、总结
| 方面 | 总结 |
|---|---|
| 模式类型 | 结构型设计模式 |
| 核心意图 | 将一个接口转换成客户希望的另一个接口,使接口不兼容的类可以一起工作。 |
| 关键角色 | 目标接口 (Target)、被适配者 (Adaptee)、适配器 (Adapter) |
| 主要优点 | 1. 提高类的复用性和透明度:复用现有类,客户端调用统一透明。 2. 解耦性强:将目标接口与现有接口解耦。 3. 符合开闭原则:无需修改现有代码即可引入新适配器。 |
| 主要缺点 | 1. 增加复杂性:增加了很多额外的类和接口。 2. 可能降低效率:一次调用可能需要多次转发。 |
| 适用场景 | 1. 想使用一个现有类,但其接口不符合需求。 2. 创建一个可复用的类,与不相关或不可预见的类协同工作。 3. 统一多个子系统的接口。 |
| 实现方式 | 对象适配器(组合): 更灵活,Java中首选。 类适配器(继承): Java中难以实现,不推荐。 |
| 现代应用 | Spring框架的核心组件(如MVC、AOP)大量使用适配器模式来整合多种实现技术。微服务网关是分布式系统层面的适配器。 |
适配器模式是架构师和开发者工具箱中不可或缺的工具。它是解决接口不兼容问题、集成遗留代码、统一异构系统接口的标准解决方案。其“转换器”的思想不仅体现在代码层面,也广泛应用于系统架构层面,是保证系统具有良好的扩展性、维护性和兼容性的关键技术。
本文来自博客园,作者:NeoLshu,转载请注明原文链接:https://www.cnblogs.com/neolshu/p/19120797

浙公网安备 33010602011771号