设计模式--适配器模式 (Adapter)
适配器模式通过创建适配器,将一个类的接口转换成客户端期望的接口,使原本不兼容的类能协同工作。
我们来详细、系统地讲解一下设计模式中的适配器模式(Adapter Pattern)。
我会通过 "是什么" -> "为什么" -> "怎么做" -> "优缺点" -> "实际案例" 的结构来为你解释。
1. 是什么:核心思想与生活中的比喻
适配器模式的核心思想非常简单,就像它的名字一样,它扮演着一个“适配器”或“转换器”的角色。
一句话定义:
将一个类的接口转换成客户端(Client)所期望的另一个接口。从而使得原本由于接口不兼容而不能在一起工作的类可以一起工作。
生活中的比喻:电源适配器
这是理解适配器模式最经典的例子:
- 你的笔记本电脑(客户端 Client): 它需要 19V 的直流电才能工作。这是它期望的“目标接口(Target)”。
- 墙上的插座(被适配者 Adaptee): 它提供的是 220V 的交流电。这是一个已经存在的、但接口不兼容的“服务”。
- 电源适配器(适配器 Adapter): 你把它一头插在墙上,另一头连接到笔记本电脑。它的作用就是将 220V 交流电转换成 19V 直流电。
在这个过程中:
- 你的笔记本电脑完全不知道墙上插座的存在,它只认识那个能提供 19V 直流电的插头(目标接口)。
- 电源适配器(Adapter)封装了转换的复杂逻辑,使得不兼容的两者能够协同工作。
2. 为什么:解决什么问题?
适配器模式主要解决以下问题:
- 复用现有代码: 当你有一个现成的类(Adaptee),它的功能很强大,完全符合你的需求,但它的接口(方法名、参数等)与你系统中其他部分所期望的接口(Target)不一致时,你不想重写这个类,而是想复用它。
- 统一接口: 当你需要使用多个具有不同接口的第三方库或遗留系统时,可以为它们分别创建适配器,将它们都适配成一个统一的接口,这样你的主系统就可以用同样的方式与它们交互,降低了系统的复杂性。
3. 怎么做:结构与实现
适配器模式通常涉及三个主要角色:
- Target (目标接口): 客户端代码所期望和直接调用的接口。
- Adaptee (被适配者): 已经存在的、拥有不兼容接口的类。
- Adapter (适配器): 实现 Target 接口,并内部包装一个 Adaptee 对象的实例。它负责将来自 Target 接口的请求转换成对 Adaptee 接口的调用。
适配器模式主要有两种实现方式:类适配器和对象适配器。
3.1 对象适配器模式 (Object Adapter Pattern) - 推荐使用
这是最常用、最灵活的方式,它基于对象组合(Composition)。
- 结构: Adapter 类实现 Target 接口,并且内部持有一个 Adaptee 类的实例。
- 工作方式: 当客户端调用 Adapter 的方法时,Adapter 内部会将这个调用委托(delegate)给它持有的 Adaptee 实例来完成实际工作。
UML 图:
+-------------+ +----------------+ +---------------+
| Client |----->| Target | | Adaptee |
+-------------+ +----------------+ +---------------+
^ | doSomething() |
| (implements) +---------------+
|
+----------------+
| Adapter |<>----(has a)----+
+----------------+
| - adaptee |
+----------------+
| + request() |
+----------------+
代码示例 (Java):
假设我们有一个播放器系统。
1. 目标接口 (Target): 我们的系统只认识 MediaPlayer 接口。
// 目标接口
public interface MediaPlayer {
void play(String audioType, String fileName);
}
2. 被适配者 (Adaptee): 我们现在有一个现成的、功能强大的 AdvancedMediaPlayer,但它的接口和我们的系统不兼容。
// 被适配者 - 高级播放器
interface AdvancedMediaPlayer {
void playMp4(String fileName);
void playVlc(String fileName);
}
class Mp4Player implements AdvancedMediaPlayer {
@Override
public void playMp4(String fileName) {
System.out.println("Playing mp4 file. Name: " + fileName);
}
@Override
public void playVlc(String fileName) {
// do nothing
}
}
class VlcPlayer implements AdvancedMediaPlayer {
@Override
public void playMp4(String fileName) {
// do nothing
}
@Override
public void playVlc(String fileName) {
System.out.println("Playing vlc file. Name: " + fileName);
}
}
3. 适配器 (Adapter): 创建一个适配器,让 AdvancedMediaPlayer 能被我们的系统使用。
// 适配器
public class MediaAdapter implements MediaPlayer {
private AdvancedMediaPlayer advancedMusicPlayer; // 持有被适配者实例
public MediaAdapter(String audioType) {
if (audioType.equalsIgnoreCase("mp4")) {
advancedMusicPlayer = new Mp4Player();
} else if (audioType.equalsIgnoreCase("vlc")) {
advancedMusicPlayer = new VlcPlayer();
}
}
@Override
public void play(String audioType, String fileName) {
// 将 MediaPlayer 的调用转换为 AdvancedMediaPlayer 的调用
if (audioType.equalsIgnoreCase("mp4")) {
advancedMusicPlayer.playMp4(fileName);
} else if (audioType.equalsIgnoreCase("vlc")) {
advancedMusicPlayer.playVlc(fileName);
}
}
}
4. 客户端 (Client): 客户端通过 MediaPlayer 接口来播放所有格式。
// 客户端播放器
public class AudioPlayer implements MediaPlayer {
MediaAdapter mediaAdapter;
@Override
public void play(String audioType, String fileName) {
// 内置支持播放 mp3
if (audioType.equalsIgnoreCase("mp3")) {
System.out.println("Playing mp3 file. Name: " + fileName);
}
// 使用适配器支持其他格式
else if (audioType.equalsIgnoreCase("vlc") || audioType.equalsIgnoreCase("mp4")) {
mediaAdapter = new MediaAdapter(audioType);
mediaAdapter.play(audioType, fileName);
} else {
System.out.println("Invalid media. " + audioType + " format not supported");
}
}
}
// 使用
public class AdapterPatternDemo {
public static void main(String[] args) {
AudioPlayer audioPlayer = new AudioPlayer();
audioPlayer.play("mp3", "beyond the horizon.mp3");
audioPlayer.play("mp4", "alone.mp4");
audioPlayer.play("vlc", "far far away.vlc");
audioPlayer.play("avi", "mind me.avi");
}
}
3.2 类适配器模式 (Class Adapter Pattern)
这种方式基于类继承(Inheritance)。
- 结构: Adapter 类同时继承 Adaptee 类并实现 Target 接口。这种方式在像 C++ 这样支持多重继承的语言中更直接,在 Java 中需要 Adaptee 是一个具体类,而 Target 是一个接口。
- 缺点:
- 耦合度高: Adapter 与一个具体的 Adaptee 类绑定,无法适配 Adaptee 的子类。
- 语言限制: Java 不支持类的多重继承,所以 Adapter 只能继承一个 Adaptee 类,限制了其使用场景。
UML 图:
+-------------+ +----------------+ +---------------+
| Client |----->| Target | | Adaptee |
+-------------+ +----------------+ +---------------+
^ ^
| (implements) | (extends)
| |
+---------------------------+
| Adapter |
+---------------------------+
| + request() |
+---------------------------+
由于其局限性,在实际开发中对象适配器更为常用和推荐。
4. 优缺点总结
优点:
- 更好的复用性: 可以透明地复用现有的类,而无需修改其源代码。
- 更好的解耦: 将客户端与具体实现解耦。客户端代码始终面向目标接口编程,无需关心背后实际工作的是哪个类,以及它是如何被适配的。
- 提高透明性: 客户端完全不知道适配器的存在,调用方式简单、统一。
- 灵活性高(特别是对象适配器): 一个适配器可以适配多个不同的被适配者(Adaptee 及其子类)。
缺点:
- 增加系统复杂性: 过多地使用适配器,会让系统中的类数量增加,代码的可读性在一定程度上会下降。如果仅仅是为了命名规范或微小差异而引入适配器,可能会得不偿失。
5. 实际案例
适配器模式在各种框架和库中被广泛应用。
Java IO 流:
InputStreamReader和OutputStreamWriterInputStream(Adaptee) 是字节流。Reader(Target) 是字符流。InputStreamReader(Adapter) 作为一个适配器,它接收一个InputStream对象,并将其适配成Reader接口,从而可以将字节流转换为字符流来读取。
java// FileInputStream是字节流 (Adaptee) FileInputStream fis = new FileInputStream("file.txt"); // InputStreamReader是适配器,将字节流适配为字符流 (Target) Reader reader = new InputStreamReader(fis, "UTF-8"); // 现在可以像操作字符流一样操作它 reader.read();日志框架门面:SLF4J (Simple Logging Facade for Java)
- SLF4J 定义了一套标准的日志接口(Target)。
- Log4j, Logback, java.util.logging 等是具体的日志实现(Adaptees)。
slf4j-log4j12.jar这样的桥接包就是适配器,它实现了 SLF4J 接口,内部则调用 Log4j 的 API。这使得应用程序可以统一使用 SLF4J 接口,而底层可以灵活切换不同的日志框架。
java.util.Arrays.asList()
这个方法也体现了适配器模式的思想。它接收一个数组(Adaptee),并返回一个实现了List接口(Target)的对象,让你能够以列表的方式操作数组。
总结
当你想使用一个已经存在的类,而它的接口不符合你的需求时,就应该首先考虑使用适配器模式。它是一种非常有用的“补救”模式,能够优雅地解决接口不兼容的问题,是连接新旧代码、异构系统的利器。在实践中,优先选择对象适配器,因为它更符合“组合优于继承”的设计原则,提供了更好的灵活性和解耦性。