跳到主要内容

适配器模式(Adapter)

适配器模式是一种结构型设计模式,它将一个类的接口转换成客户端期望的另一个接口,使得原本因接口不兼容而不能一起工作的类可以协同工作。

模式定义

适配器模式(Adapter Pattern):将一个类的接口转换成客户期望的另一个接口。适配器让那些接口不兼容的类可以合作无间。

核心要点

  1. 接口转换:将现有接口转换为目标接口
  2. 解耦合:客户端通过目标接口与被适配者交互,无需了解具体实现
  3. 复用性:可以复用现有的类,无需修改其代码

问题场景

假设我们有一个媒体播放器,只能播放 MP3 格式的音频文件。现在需要扩展它以支持其他格式(如 MP4、VLC),但现有的高级媒体播放器接口与我们系统的接口不兼容。

// 现有的媒体播放器接口
public interface MediaPlayer {
void play(String filename);
}

// 高级媒体播放器接口(第三方库,无法修改)
public interface AdvancedMediaPlayer {
void playVlc(String filename);
void playMp4(String filename);
}

// VLC 播放器实现
public class VlcPlayer implements AdvancedMediaPlayer {
public void playVlc(String filename) {
System.out.println("播放 VLC 文件: " + filename);
}

public void playMp4(String filename) {
// VLC 播放器不支持 MP4
}
}

// MP4 播放器实现
public class Mp4Player implements AdvancedMediaPlayer {
public void playVlc(String filename) {
// MP4 播放器不支持 VLC
}

public void playMp4(String filename) {
System.out.println("播放 MP4 文件: " + filename);
}
}

存在的问题

  • AdvancedMediaPlayer 接口与 MediaPlayer 接口不兼容
  • 无法直接在现有系统中使用高级播放器
  • 不能修改第三方库的代码

解决方案

使用适配器模式,创建一个适配器类,实现 MediaPlayer 接口,内部包装 AdvancedMediaPlayer 的实现:

// 媒体适配器
public class MediaAdapter implements MediaPlayer {
private AdvancedMediaPlayer advancedPlayer;

public MediaAdapter(String audioType) {
if (audioType.equalsIgnoreCase("vlc")) {
advancedPlayer = new VlcPlayer();
} else if (audioType.equalsIgnoreCase("mp4")) {
advancedPlayer = new Mp4Player();
}
}

@Override
public void play(String audioType, String filename) {
if (audioType.equalsIgnoreCase("vlc")) {
advancedPlayer.playVlc(filename);
} else if (audioType.equalsIgnoreCase("mp4")) {
advancedPlayer.playMp4(filename);
}
}
}

// 音频播放器(支持多种格式)
public class AudioPlayer implements MediaPlayer {
private MediaAdapter mediaAdapter;

@Override
public void play(String audioType, String filename) {
// 内置支持 MP3
if (audioType.equalsIgnoreCase("mp3")) {
System.out.println("播放 MP3 文件: " + filename);
}
// 使用适配器支持其他格式
else if (audioType.equalsIgnoreCase("vlc") ||
audioType.equalsIgnoreCase("mp4")) {
mediaAdapter = new MediaAdapter(audioType);
mediaAdapter.play(audioType, filename);
} else {
System.out.println("不支持的格式: " + audioType);
}
}
}

// 使用示例
public class Main {
public static void main(String[] args) {
AudioPlayer player = new AudioPlayer();

player.play("mp3", "song.mp3");
player.play("mp4", "video.mp4");
player.play("vlc", "movie.vlc");
player.play("avi", "clip.avi");
}
}

输出

播放 MP3 文件: song.mp3
播放 MP4 文件: video.mp4
播放 VLC 文件: movie.vlc
不支持的格式: avi

模式结构

适配器模式有两种实现方式:对象适配器和类适配器。

对象适配器(推荐)

使用组合方式,适配器持有一个被适配者的引用。

组成部分

  • Target(目标接口):客户端期望的接口
  • Adapter(适配器):实现目标接口,持有被适配者引用
  • Adaptee(被适配者):现有接口,需要被适配
  • Client(客户端):通过目标接口与适配器交互

类适配器

使用继承方式,适配器同时继承目标类和被适配者类。

注意:Java 不支持多重继承,类适配器只能通过继承一个类并实现接口的方式实现。

实现方式

1. 对象适配器(推荐)

// 目标接口
public interface Target {
void request();
}

// 被适配者
public class Adaptee {
public void specificRequest() {
System.out.println("被适配者的特定请求");
}
}

// 适配器(使用组合)
public class Adapter implements Target {
private Adaptee adaptee;

public Adapter(Adaptee adaptee) {
this.adaptee = adaptee;
}

@Override
public void request() {
// 转换接口调用
adaptee.specificRequest();
}
}

// 客户端
public class Client {
public static void main(String[] args) {
Adaptee adaptee = new Adaptee();
Target target = new Adapter(adaptee);
target.request();
}
}

优点

  • 灵活性高,可以在运行时更换被适配者
  • 符合组合复用原则
  • 可以适配多个被适配者

2. 类适配器

// 目标接口
public interface Target {
void request();
}

// 被适配者
public class Adaptee {
public void specificRequest() {
System.out.println("被适配者的特定请求");
}
}

// 适配器(使用继承)
public class Adapter extends Adaptee implements Target {
@Override
public void request() {
// 直接调用父类方法
specificRequest();
}
}

// 客户端
public class Client {
public static void main(String[] args) {
Target target = new Adapter();
target.request();
}
}

优点

  • 代码简洁,不需要持有被适配者引用
  • 可以重写被适配者的行为

缺点

  • 灵活性低,编译时确定被适配者
  • Java 不支持多重继承,限制了使用场景

3. 双向适配器

适配器可以同时支持两个方向的转换:

// 目标接口 A
public interface TargetA {
void requestA();
}

// 目标接口 B
public interface TargetB {
void requestB();
}

// 双向适配器
public class TwoWayAdapter implements TargetA, TargetB {
private TargetA targetA;
private TargetB targetB;

public TwoWayAdapter(TargetA targetA) {
this.targetA = targetA;
}

public TwoWayAdapter(TargetB targetB) {
this.targetB = targetB;
}

@Override
public void requestA() {
if (targetB != null) {
System.out.println("将 B 转换为 A");
targetB.requestB();
} else {
targetA.requestA();
}
}

@Override
public void requestB() {
if (targetA != null) {
System.out.println("将 A 转换为 B");
targetA.requestA();
} else {
targetB.requestB();
}
}
}

实际应用案例

1. Java I/O 流

Java 的 InputStreamReaderOutputStreamWriter 就是适配器模式的典型应用,它们将字节流转换为字符流:

// 字节流到字符流的适配
InputStream inputStream = new FileInputStream("file.txt");
Reader reader = new InputStreamReader(inputStream, "UTF-8");

// 使用字符流读取
BufferedReader bufferedReader = new BufferedReader(reader);
String line = bufferedReader.readLine();

解释

  • InputStream 是字节流接口(被适配者)
  • Reader 是字符流接口(目标接口)
  • InputStreamReader 是适配器,将字节流转换为字符流

2. Arrays.asList()

Java 的 Arrays.asList() 方法将数组适配为 List:

public class Arrays {
public static <T> List<T> asList(T... a) {
return new ArrayList<>(a);
}

// 内部的 ArrayList 是一个适配器
private static class ArrayList<E> extends AbstractList<E> {
private final E[] a;

ArrayList(E[] array) {
a = Objects.requireNonNull(array);
}

@Override
public E get(int index) {
return a[index];
}

@Override
public int size() {
return a.length;
}
}
}

// 使用示例
String[] array = {"a", "b", "c"};
List<String> list = Arrays.asList(array);

3. Spring MVC HandlerAdapter

Spring MVC 使用适配器模式来支持多种类型的处理器:

// 处理器适配器接口
public interface HandlerAdapter {
boolean supports(Object handler);
ModelAndView handle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception;
}

// 控制器适配器
public class SimpleControllerHandlerAdapter implements HandlerAdapter {
@Override
public boolean supports(Object handler) {
return handler instanceof Controller;
}

@Override
public ModelAndView handle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {
return ((Controller) handler).handleRequest(request, response);
}
}

4. 数据库驱动适配

JDBC 驱动是适配器模式的应用,将不同数据库的接口适配为统一的 JDBC 接口:

// 客户端使用统一的 JDBC 接口
Connection conn = DriverManager.getConnection(url, username, password);
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery("SELECT * FROM users");

// 不同数据库厂商提供各自的适配器实现
// MySQL: com.mysql.cj.jdbc.Connection
// PostgreSQL: org.postgresql.jdbc.PgConnection
// Oracle: oracle.jdbc.OracleConnection

适配器模式 vs 装饰器模式

特性适配器模式装饰器模式
目的接口转换功能增强
接口改变接口保持接口不变
关注点接口兼容性功能扩展
应用时机已有类接口不兼容需要动态添加功能

适配器模式 vs 外观模式

特性适配器模式外观模式
目的接口转换简化接口
接口数量一个接口多个接口
复杂度解决接口不兼容隐藏系统复杂性
设计方式现有接口适配定义新接口

优缺点分析

优点

  1. 单一职责原则:将接口转换代码从业务逻辑中分离
  2. 开闭原则:不修改现有代码就能引入新的适配器
  3. 提高复用性:复用现有的类,无需修改
  4. 灵活性:可以在运行时切换不同的适配器实现

缺点

  1. 增加复杂性:引入新的类和接口
  2. 过度使用:如果接口设计合理,不需要适配器
  3. 性能开销:多一层间接调用

使用建议

何时使用适配器

  • 需要使用现有类,但其接口与需要的接口不匹配
  • 想要创建一个可以复用的类,该类与其他不相关的类或不可预见的类协同工作
  • 需要使用几个现有的子类,但通过对每个子类进行子类化来适配它们的接口是不现实的

何时避免使用适配器

  • 可以直接修改现有类的接口
  • 系统处于设计阶段,可以统一接口规范
  • 适配器会导致系统过于复杂

最佳实践

  1. 优先使用对象适配器:组合比继承更灵活
  2. 保持适配器简单:只做接口转换,不添加业务逻辑
  3. 命名清晰:适配器类名应体现其适配作用
  4. 考虑双向适配:如果两个方向都需要转换

小结

适配器模式是一种简单但非常实用的结构型模式:

实现方式关键特点推荐场景
对象适配器组合,灵活大多数场景
类适配器继承,简洁单一继承场景
双向适配器双向转换需要双向适配

练习

  1. 实现一个适配器,将 Enumeration 接口适配为 Iterator 接口
  2. 实现一个日志适配器,将不同日志框架的接口统一
  3. 分析 Java 中 Collections.enumeration() 方法的设计
  4. 实现一个缓存适配器,支持多种缓存框架(Redis、Memcached)

参考资源