适配器模式
适配器模式(Adapter Pattern)是一种结构型设计模式,它的作用是将一个类的接口转换为客户端所期望的另一种接口。适配器模式让原本接口不兼容的类能够合作无间,常用于将新系统集成到旧系统中。
形象的例子:电源插座适配器
设想一下,你去旅游的时候发现你带的电子设备插头是两插的,而当地的插座是三插的,这时候你就需要一个插头适配器,它能够将你的两插头转换为三插头,从而使设备能够正常使用。
在这个例子中:
- 两插头:代表一个已经存在的接口(老接口)。
- 三插头插座:代表一个新系统的接口(新接口)。
- 插头适配器:是适配器,将老接口转化为新接口,使得设备能够正常工作。
类适配器
适配器类继承了要适配的类,并且实现了目标接口。
优点:由于使用了继承,可以直接重写父类中的方法,代码简洁。
缺点:Java 中不支持多继承,所以如果适配的类已经继承了其他类,类适配器就无法使用。
// 目标接口:三插电器
interface ThreePlugDevice {
void powerWithThreePlug();
}
// 被适配的类:两插电器
class TwoPlugDevice {
public void powerWithTwoPlug() {
System.out.println("使用两插电源供电");
}
}
// 类适配器:两插适配三插
class TwoToThreePlugAdapter extends TwoPlugDevice implements ThreePlugDevice {
@Override
public void powerWithThreePlug() {
// 调用两插电器的供电方法
this.powerWithTwoPlug();
}
}
// 测试类
public class ClassAdapterTest {
public static void main(String[] args) {
ThreePlugDevice device = new TwoToThreePlugAdapter();
device.powerWithThreePlug(); // 输出:使用两插电源供电
}
}
类图:
对象适配器
实现方式:适配器类持有一个要适配的对象,并通过调用这个对象的接口来实现目标接口。
优点:不依赖于多继承,适配器可以适配多个类,只需要持有不同的对象即可,灵活性较高。
缺点:需要通过对象调用接口,层次较深,代码可能稍微复杂一些。
// 目标接口:三插电器
interface ThreePlugDevice {
void powerWithThreePlug();
}
// 被适配的类:两插电器
class TwoPlugDevice {
public void powerWithTwoPlug() {
System.out.println("使用两插电源供电");
}
}
// 对象适配器:两插适配三插
class TwoToThreePlugAdapter implements ThreePlugDevice {
private TwoPlugDevice twoPlugDevice;
public TwoToThreePlugAdapter(TwoPlugDevice twoPlugDevice) {
this.twoPlugDevice = twoPlugDevice;
}
@Override
public void powerWithThreePlug() {
// 调用两插电器的供电方法
twoPlugDevice.powerWithTwoPlug();
}
}
// 测试类
public class ObjectAdapterTest {
public static void main(String[] args) {
TwoPlugDevice twoPlug = new TwoPlugDevice();
ThreePlugDevice adapter = new TwoToThreePlugAdapter(twoPlug);
adapter.powerWithThreePlug(); // 输出:使用两插电源供电
}
}
类图:
接口适配器
接口适配器模式(也叫作缺省适配器模式或默认适配器模式)是适配器模式的另一种变体,适用于当我们只需要实现接口的一部分功能时,而不想为接口中的每个方法都提供实现。
场景举例
比如,你有一个接口定义了多个方法,但是你只对其中几个方法感兴趣,其他的方法对你来说并不需要。那么,如果直接实现接口,你就必须实现所有的方法,即使有的方法是空实现。这时接口适配器模式就很有用,它可以提供一个默认的实现,这样你就不需要为每个方法都写实现,只需要覆盖你关心的那些方法
形象的例子:活动监听器
设想一下,你在编写一个窗口应用程序,用户可以通过鼠标或键盘进行操作。你需要监听这些事件,但你只对鼠标点击感兴趣,而鼠标移动、键盘按键等事件对你无关紧要。这时,如果你实现一个包含所有事件监听方法的接口,你需要实现很多和你不相关的方法。接口适配器模式就像是一个“监听器适配器”,帮你提供了默认的实现,让你只需要专注于鼠标点击事件
接口适配器模式实现方式
接口适配器模式通过创建一个抽象类,这个抽象类实现接口,并为接口中的所有方法提供一个默认实现(通常是空实现)。然后,具体的子类可以根据需要选择性地覆盖其中的某些方法。
// 定义一个接口,包含多个用户行为的方法
interface UserActionListener {
void onClick();
void onDoubleClick();
void onRightClick();
void onMove();
}
// 定义一个抽象类,实现接口并提供空的默认实现
abstract class UserActionAdapter implements UserActionListener {
@Override
public void onClick() {}
@Override
public void onDoubleClick() {}
@Override
public void onRightClick() {}
@Override
public void onMove() {}
}
// 创建一个子类,只关心 onClick() 事件
class ClickAction extends UserActionAdapter {
@Override
public void onClick() {
System.out.println("鼠标单击事件被触发");
}
}
// 测试类
public class InterfaceAdapterTest {
public static void main(String[] args) {
UserActionListener action = new ClickAction();
action.onClick(); // 输出:鼠标单击事件被触发
action.onMove(); // 什么也不会发生,因为没有重写 onMove()
}
}
类图:
优点
简化类的实现:通过提供一个抽象适配器类,避免实现类必须覆盖所有接口方法,只需要关注自己感兴趣的部分即可。
灵活性高:子类可以按需选择性地覆盖接口中的某些方法,适应不同的需求。
易于扩展:如果接口中方法增多,通过适配器可以更方便地实现
使用场景
- 多方法接口的部分实现:当我们需要实现一个包含很多方法的接口,但只关注其中少部分方法时,接口适配器模式非常有用。
- 简化回调类:在事件驱动编程中,尤其是像 GUI 或监听器编程,适配器可以简化代码,使我们只关心自己需要处理的事件。
三种适配器模式的区别对比
比较维度 | 类适配器 | 对象适配器 | 接口适配器 |
---|---|---|---|
实现方式 | 通过继承来实现适配 | 通过组合来实现适配 | 通过抽象类提供默认实现适配 |
适用场景 | 当需要适配的类和目标接口有较强的关联 | 当需要适配的类和目标接口没有关联,且需要灵活适配多个类 | 当接口有多个方法,而子类只需实现部分方法 |
灵活性 | 较低,因为 Java 不支持多继承 | 较高,可以适配多个不同类 | 非常高,子类可选择实现任意接口方法 |
代码复杂度 | 较低,代码直接继承和重写 | 较高,需要组合对象并实现接口 | 适中,使用抽象类提供空实现 |
是否支持多继承 | 不支持(因为 Java 不支持多继承) | 支持(通过组合方式实现) | 不涉及多继承问题,通过抽象类适配 |
实现对象 | 适配一个类 | 可以适配多个类 | 适配接口,允许选择性实现部分方法 |
是否修改原类代码 | 不需要修改原类代码,但使用继承 | 不需要修改原类代码,使用对象持有 | 不修改原类或接口,只需继承抽象类 |
适配器本质 | 适配器是被适配类的子类 | 适配器是独立的类,通过组合持有被适配类 | 适配器是抽象类的子类 |
适配器模式选择的场景总结:
- 类适配器:
- 当你只需要适配一个类,并且你可以使用继承进行扩展时,这种方式最简单。
- 缺点是受限于 Java 的单继承限制,无法同时继承多个类。
- 对象适配器:
- 当你需要适配多个类,或适配类之间没有继承关系时,使用对象适配器更加灵活。
- 通过组合方式,可以避免多继承的局限性,适应复杂场景。
- 接口适配器:
- 当接口中有多个方法,而你只需要实现其中一部分时,这种方式非常有用。
- 通过抽象类提供默认实现,子类可以选择性实现感兴趣的方法,而不必关心其他方法。
源码中的应用
Java I/O (InputStream 和 Reader)
Java 的 I/O 库广泛使用了适配器模式来适配不同的输入输出流。
例子: InputStreamReader
类
- 作用:
InputStreamReader
将InputStream
(字节流)适配为Reader
(字符流),这是典型的适配器模式。 - 实现原理:
InputStreamReader
通过组合InputStream
对象,并将字节流转换为字符流,适配了字符流的接口
代码示例:
InputStream input = new FileInputStream("input.txt");
Reader reader = new InputStreamReader(input, "UTF-8");
SpringMVC 框架
例子 1:HandlerAdapter
- 作用:在 Spring MVC 中,
HandlerAdapter
是典型的适配器模式,用于将不同类型的处理器(如Controller
)适配为统一的Handler
接口,使得框架可以使用统一的方式处理多种类型的请求。 - 实现原理:不同的控制器类型(如
Controller
、HttpRequestHandler
等)通过对应的HandlerAdapter
实现适配,屏蔽了多种控制器处理请求的差异。
例子 2:DispatcherServlet
- 作用:
DispatcherServlet
作为 Spring MVC 的核心,将各种请求路由到不同的控制器,而这些控制器可能具有不同的接口和功能。Spring 通过适配器模式来统一处理这些控制器请求
JDBC
JDBC 是 Java 访问数据库的 API,提供了统一的接口来处理不同的数据库系统。
例子:DriverManager
- 作用:JDBC 的
DriverManager
是通过适配器模式将不同数据库驱动的实现类适配为统一的Driver
接口。通过这种方式,不同的数据库(如 MySQL、PostgreSQL、Oracle)可以通过相同的 JDBC API 进行访问。
实现原理:不同的数据库厂商提供自己的 Driver
实现,通过 JDBC 的适配机制,开发者可以用相同的 API 进行操作
代码示例:
Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/mydb", "user", "password");
SLF4J(Simple Logging Facade for Java)
SLF4J 是另一个常用的日志框架,类似于 Apache Commons Logging,但更为简洁和高效。
例子:LoggerAdapter
- 作用:SLF4J 通过适配器模式,将不同的日志实现(如
Log4j
、Logback
等)统一适配为一个通用的日志接口Logger
。开发者通过统一的接口进行日志调用,而不需要关心底层实现。 - 实现原理:不同的日志系统提供各自的实现,SLF4J 通过
LoggerAdapter
进行适配,从而实现日志系统的无缝切换。