适配器模式
1. 什么是适配器模式?
想象一下,你有一个欧标的电器插头(比如两孔圆形),但你家的插座是美标的(比如两孔扁平或三孔)。你不能直接把欧标插头插到美标插座里。这时候你需要一个"转换插头"或"适配器",这个转换插头一端可以接欧标插头,另一端可以插到美标插座上。
适配器模式 就是这样一种设计模式,它的核心思想是:将一个类的接口转换成客户端所期望的另一种接口。 使得原本由于接口不兼容而不能一起工作的类可以协同工作。
它充当两个不兼容接口之间的桥梁。
2. 适配器模式的结构 (主要角色):
-
Target (目标接口): 这是客户端代码期望使用的接口。客户端通过这个接口与适配后的对象进行交互。
-
Adaptee (源接口 / 被适配者): 这是已存在的、但其接口与 Target 接口不兼容的类。它是需要被适配的类。
-
Adapter (适配器): 这是核心角色。它实现了 Target 接口,并且内部持有一个 Adaptee 对象的引用(或者继承 Adaptee 类)。它的任务是将对 Target 接口的调用转换成对 Adaptee 接口的调用。
-
Client (客户端): 使用 Target 接口与适配器进行交互的类。客户端并不知道也不关心它实际使用的是哪个 Adaptee。
3. 适配器模式的两种主要实现方式:
a) 类适配器模式 (Class Adapter Pattern):
-
实现方式: Adapter 类通过多重继承(在 Java 中通过继承 Adaptee 类并实现 Target 接口)来实现。
-
结构图示:
+----------------+ +----------------+ | Client |------>| Target | (Interface or Abstract Class) +----------------+ +----------------+ ^ | (implements/extends) +----------------+ | Adapter | (Class) +----------------+ ^ | (extends) +----------------+ | Adaptee | (Class) +----------------+
-
特点:
-
适配器直接继承了 Adaptee 类,因此可以重写 Adaptee 的部分方法。
-
由于 Java 不支持多重类继承,如果 Adaptee 是一个具体的类(而不是接口),并且 Target 也是一个具体的类(通常 Target 是接口),这种方式在 Java 中可以实现(Adapter 继承 Adaptee,实现 Target 接口)。
-
缺点: 耦合度较高。Adapter 必须是 Adaptee 的子类,这限制了其灵活性。如果 Adaptee 是 final 类,则无法使用类适配器。
-
b) 对象适配器模式 (Object Adapter Pattern):
-
实现方式: Adapter 类实现 Target 接口,并且在其内部持有一个 Adaptee 对象的实例引用。所有对 Target 接口的调用都会被委派给这个 Adaptee 实例。
-
结构图示:
+----------------+ +----------------+ | Client |------>| Target | (Interface or Abstract Class) +----------------+ +----------------+ ^ | (implements/extends) +----------------+ | Adapter | (Class) +----------------+ | | (has-a / composition) V +----------------+ | Adaptee | (Class or Interface) +----------------+
-
特点:
-
更灵活: Adapter 和 Adaptee 之间的关系是组合/聚合关系,耦合度相对较低。
-
Adapter 可以适配 Adaptee 类及其所有子类。
-
可以在运行时动态地改变被适配的对象。
-
是更常用和推荐的方式。
-
4. 适配器模式的优缺点:
优点:
-
提高类的复用性: 可以让现有的、接口不兼容的类在新的环境中使用,而无需修改其源代码。
-
增加类的透明度(对于客户端): 客户端代码只需要与 Target 接口交互,无需关心具体的 Adaptee 实现。
-
更好的灵活性和扩展性(特别是对象适配器): 可以很容易地替换或增加新的适配器来适配不同的 Adaptee。
-
解耦: 将客户端与具体的 Adaptee 实现解耦。
缺点:
-
过多的使用适配器会使系统变得零碎和复杂: 每引入一个适配器都会增加一个类。
-
类适配器模式的限制:
-
只能适配一个具体的 Adaptee 类。
-
在支持单继承的语言(如 Java)中,如果 Adaptee 本身就是 Target 的一个不兼容的子类,可能会比较棘手。
-
-
可能增加代码的间接性: 调用需要通过适配器进行转发,可能会有轻微的性能影响(通常可忽略不计)。
5. 适配器模式的应用场景:
-
系统需要使用现有的类,而这些类的接口不符合系统的需要时。
-
想要创建一个可以复用的类,该类可以与其他不相关的类或不可预见的类协同工作。 (例如,一个通用的数据转换工具)
-
(对象适配器)需要适配一个类的多个子类时,或者需要在运行时动态选择适配的 Adaptee 时。
-
在不同模块或系统之间进行集成,而它们定义的接口不一致时。
Java 中的例子:
-
java.io.InputStreamReader 和 java.io.OutputStreamWriter:
-
InputStreamReader 是一个适配器,它将字节输入流 (InputStream - Adaptee) 转换为字符输入流 (Reader - Target)。
-
OutputStreamWriter 是一个适配器,它将字符输出流 (Writer - Target) 转换为字节输出流 (OutputStream - Adaptee)。 它们解决了字节操作和字符操作之间的不兼容问题,并处理了字符编码。
-
-
java.util.Arrays.asList():
-
这个方法可以将一个数组 (T[] - Adaptee) 适配成一个 List<T> (List - Target) 接口。
-
返回的 List 是一个固定大小的列表,它内部仍然依赖于原始数组。
-
-
SLF4J (Simple Logging Facade for Java) 和其他日志框架 (Logback, Log4j) 的桥接包:
- 例如 log4j-over-slf4j 或 jul-to-slf4j。这些桥接包充当适配器,将对旧日志 API (如 Log4j 1.x 或 java.util.logging) 的调用,适配到 SLF4J 接口,进而路由到 SLF4J 底层绑定的具体日志实现。
代码示例 (对象适配器模式):
假设我们有一个旧的音频播放器 OldAudioPlayer,它只能播放 .mp3 文件。我们希望我们的新播放器 AudioPlayer (Target) 能够播放 .mp4 和 .vlc 文件,同时也能利用 OldAudioPlayer 来播放 .mp3。
// Adaptee (被适配者)
class OldAudioPlayer {
public void playMp3(String fileName) {
System.out.println("Playing mp3 file: " + fileName);
}
}
// Target (目标接口)
interface MediaPlayer {
void play(String audioType, String fileName);
}
// Adapter (适配器)
class MediaAdapter implements MediaPlayer {
OldAudioPlayer oldAudioPlayer; // 持有 Adaptee 的引用
public MediaAdapter(String audioType) {
if (audioType.equalsIgnoreCase("mp3")) {
oldAudioPlayer = new OldAudioPlayer();
}
// 如果需要适配其他类型,可以在这里实例化其他 Adaptee
}
@Override
public void play(String audioType, String fileName) {
if (audioType.equalsIgnoreCase("mp3")) {
oldAudioPlayer.playMp3(fileName);
}
// 对于其他类型,这个适配器目前不处理,或者可以扩展
// else if (audioType.equalsIgnoreCase("vlc")) { ... }
// else if (audioType.equalsIgnoreCase("mp4")) { ... }
}
}
// Client (客户端期望使用的播放器)
class AudioPlayer implements MediaPlayer {
MediaAdapter mediaAdapter; // 客户端可以使用适配器
@Override
public void play(String audioType, String fileName) {
// 内建支持播放 mp4 (假设)
if (audioType.equalsIgnoreCase("mp4")) {
System.out.println("Playing mp4 file: " + fileName);
}
// 使用适配器播放其他格式
else if (audioType.equalsIgnoreCase("vlc") || audioType.equalsIgnoreCase("mp3")) {
// 这里为了简单,直接 new。实际项目中可能通过工厂或依赖注入获取适配器实例
if (audioType.equalsIgnoreCase("mp3")) {
mediaAdapter = new MediaAdapter("mp3"); // 创建针对 mp3 的适配器
mediaAdapter.play(audioType, fileName);
} else if (audioType.equalsIgnoreCase("vlc")) {
// 假设我们有另一个 AdvancedMediaPlayer 接口和其实现
// AdvancedMediaPlayer advancedMediaPlayer = new VlcPlayer();
// mediaAdapter = new AdvancedMediaAdapter(advancedMediaPlayer); // 创建另一个适配器
// mediaAdapter.play(audioType, fileName);
System.out.println("Playing vlc file using a different adapter (concept): " + 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");
}
}
总结:
适配器模式是一种非常有用的结构型设计模式,它专注于解决接口不兼容的问题,使得原本无法协同工作的组件能够一起工作。在选择类适配器还是对象适配器时,通常对象适配器因其更高的灵活性而被优先考虑。记住那个"转换插头"的例子,就能很好地理解适配器模式的核心思想了。