适配器模式(Adapter)
适配器模式是一种结构型设计模式,它将一个类的接口转换成客户端期望的另一个接口,使得原本因接口不兼容而不能一起工作的类可以协同工作。
模式定义
适配器模式(Adapter Pattern):将一个类的接口转换成客户期望的另一个接口。适配器让那些接口不兼容的类可以合作无间。
核心要点
- 接口转换:将现有接口转换为目标接口
- 解耦合:客户端通过目标接口与被适配者交互,无需了解具体实现
- 复用性:可以复用现有的类,无需修改其代码
问题场景
假设我们有一个媒体播放器,只能播放 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 的 InputStreamReader 和 OutputStreamWriter 就是适配器模式的典型应用,它们将字节流转换为字符流:
// 字节流到字符流的适配
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 外观模式
| 特性 | 适配器模式 | 外观模式 |
|---|---|---|
| 目的 | 接口转换 | 简化接口 |
| 接口数量 | 一个接口 | 多个接口 |
| 复杂度 | 解决接口不兼容 | 隐藏系统复杂性 |
| 设计方式 | 现有接口适配 | 定义新接口 |
优缺点分析
优点
- 单一职责原则:将接口转换代码从业务逻辑中分离
- 开闭原则:不修改现有代码就能引入新的适配器
- 提高复用性:复用现有的类,无需修改
- 灵活性:可以在运行时切换不同的适配器实现
缺点
- 增加复杂性:引入新的类和接口
- 过度使用:如果接口设计合理,不需要适配器
- 性能开销:多一层间接调用
使用建议
何时使用适配器
- 需要使用现有类,但其接口与需要的接口不匹配
- 想要创建一个可以复用的类,该类与其他不相关的类或不可预见的类协同工作
- 需要使用几个现有的子类,但通过对每个子类进行子类化来适配它们的接口是不现实的
何时避免使用适配器
- 可以直接修改现有类的接口
- 系统处于设计阶段,可以统一接口规范
- 适配器会导致系统过于复杂
最佳实践
- 优先使用对象适配器:组合比继承更灵活
- 保持适配器简单:只做接口转换,不添加业务逻辑
- 命名清晰:适配器类名应体现其适配作用
- 考虑双向适配:如果两个方向都需要转换
小结
适配器模式是一种简单但非常实用的结构型模式:
| 实现方式 | 关键特点 | 推荐场景 |
|---|---|---|
| 对象适配器 | 组合,灵活 | 大多数场景 |
| 类适配器 | 继承,简洁 | 单一继承场景 |
| 双向适配器 | 双向转换 | 需要双向适配 |
练习
- 实现一个适配器,将
Enumeration接口适配为Iterator接口 - 实现一个日志适配器,将不同日志框架的接口统一
- 分析 Java 中
Collections.enumeration()方法的设计 - 实现一个缓存适配器,支持多种缓存框架(Redis、Memcached)