设计模式--适配器模式
前言
想象这个真实场景:
你有一个新买的、超酷的蓝牙音箱(目标接口:蓝牙连接)。 你只想用手机蓝牙轻松连上它放音乐。
你还有一个珍藏多年的老式磁带播放机(需要适配的对象:Adaptee)。 它音质很棒,但它只有3.5mm耳机孔(旧接口),没有蓝牙功能。
问题来了: 你的手机(只有蓝牙)想播放音乐,但无法直接连接老式磁带机(只有3.5mm孔)。你想用磁带机放手机里的歌,怎么办?
解决方案(没有适配器):
方案A(改磁带机): 你是个电子高手,拆开磁带机,焊上一个蓝牙模块。缺点: 风险大(可能弄坏)、成本高、复杂、破坏了原始设备。
方案B(换手机): 买个带3.5mm耳机孔的老手机。缺点: 为了用旧设备换新手机?荒谬!成本更高,牺牲了新手机的功能。
方案C(不用磁带机): 放弃你心爱的磁带机。缺点: 浪费资源,你失去了一个好音源。
解决方案(使用适配器):
你买了一个“蓝牙音频接收器”(这就是适配器!)。 这个小设备:
有一头是 蓝牙(实现目标接口),可以被你的新手机搜索并连接。
另一头是 3.5mm耳机孔(连接Adaptee),可以插到你老磁带机的“AUX IN”或“LINE IN”接口上。
核心思想
适配器模式的核心目的是 解决接口不兼容的问题。它允许两个原本因为接口不同而无法协同工作的类能够一起工作。
为了便于理解我们把上边的例子换成插头:你有一个欧洲标准的插头(三脚圆形【EuropeanPlug】),但你的插座是美标的(两脚扁平【AmericanSocket】)。为了让插头能插进插座正常工作,你需要一个转换插头(适配器【Adapter】)。适配器模式解决的问题与此类似:它充当两个不兼容接口之间的桥梁,将一个类的接口转换成客户期望的另一个接口。它让原本由于接口不兼容而无法一起工作的类可以协同工作。
在软件设计中
目标接口 (Target Interface): 你希望客户端使用的接口。它定义了客户端期望的方法。(对应
AmericanSocket
)需要适配的类 (Adaptee): 已经存在的类,它拥有客户端需要的功能,但它的接口与目标接口不兼容。(对应
EuropeanPlug
)适配器 (Adapter): 一个类,它实现了目标接口,并持有一个需要适配的类(Adaptee)的实例。适配器将目标接口的调用转换(适配) 成对 Adaptee 已有方法的调用。(对应电源转换器本身)
两种主要实现方式
适配器模式在 Java 中主要有两种实现方式:类适配器(通过继承)和对象适配器(通过组合)。对象适配器更常用且更灵活,是推荐的实践。
1. 对象适配器 (推荐 - 使用组合)
// 1. 目标接口 (Target Interface) - 客户端期望的接口 public interface AmericanSocket {void plugIntoAmericanOutlet(); }// 2. 添加原生美式插头实现 public class AmericanPlug implements AmericanSocket {@Overridepublic void plugIntoAmericanOutlet() {System.out.println("美式插头直接插入美式插座...");} }// 3. 需要适配的类 (Adaptee) - 已有的功能,但接口不兼容 public class EuropeanPlug {public void plugIntoEuropeanOutlet() {System.out.println("欧洲插头插入了欧洲插座...");} }// 4. 适配器 (Adapter) - 实现目标接口,持有Adaptee实例,进行转换 public class EuropeanToAmericanAdapter implements AmericanSocket {private EuropeanPlug europeanPlug; // 组合 - 持有Adaptee实例public EuropeanToAmericanAdapter() {this.europeanPlug = new EuropeanPlug();}@Overridepublic void plugIntoAmericanOutlet() {System.out.println("适配器将美式插座接口转换为欧式接口...");europeanPlug.plugIntoEuropeanOutlet(); // 调用Adaptee的方法实现功能} }// 5. 客户端代码 (Client) - 只依赖目标接口 客户端现在可以自由选择 public class Client {public static void main(String[] args) {// 使用原生美式插头AmericanSocket american = new AmericanPlug();american.plugIntoAmericanOutlet();// 使用欧式插头(通过适配器)AmericanSocket europeanAdapter = new EuropeanToAmericanAdapter();europeanAdapter.plugIntoAmericanOutlet();// 甚至可以混合使用List<AmericanSocket> devices = Arrays.asList(new AmericanPlug(), // 原生美式new EuropeanToAmericanAdapter() // 适配的欧式);devices.forEach(AmericanSocket::plugIntoAmericanOutlet);} }
输出:
美式插头直接插入美式插座... 适配器将美式插座接口转换为欧式接口... 欧洲插头插入了欧洲插座... 美式插头直接插入美式插座... 适配器将美式插座接口转换为欧式接口... 欧洲插头插入了欧洲插座...关键点:
适配器
EuropeanToAmericanAdapter
实现了AmericanSocket
接口。适配器持有一个
EuropeanPlug
实例(通过组合)。当客户端调用
plugIntoAmericanOutlet()
(目标接口方法)时,适配器内部调用europeanPlug.plugIntoEuropeanOutlet()
(Adaptee的方法)来完成实际工作,并可能进行必要的转换逻辑(这里是打印一条消息)。2. 类适配器 (使用继承 - 在Java中通常指通过继承Adaptee并实现Target接口)
// 1. 目标接口 (Target Interface) - 客户端期望的接口 public interface AmericanSocket {void plugIntoAmericanOutlet(); }// 2. 添加原生美式插头实现 public class AmericanPlug implements AmericanSocket {@Overridepublic void plugIntoAmericanOutlet() {System.out.println("美式插头直接插入美式插座...");} }// 3. 需要适配的类 (Adaptee) - 已有的功能,但接口不兼容 public class EuropeanPlug {public void plugIntoEuropeanOutlet() {System.out.println("欧洲插头插入了欧洲插座...");} }// 4. 适配器 (Adapter) - 实现目标接口,持有Adaptee实例,进行转换 public class EuropeanToAmericanAdapter extends EuropeanPlug implements AmericanSocket {@Overridepublic void plugIntoAmericanOutlet() {System.out.println("适配器将美式插座接口转换为欧式接口...");super.plugIntoEuropeanOutlet(); // 直接调用继承自Adaptee的方法} }// 5. 客户端代码 (Client) - 只依赖目标接口 客户端现在可以自由选择 public class Client {public static void main(String[] args) {// 使用原生美式插头AmericanSocket american = new AmericanPlug();american.plugIntoAmericanOutlet();// 使用欧式插头(通过适配器)AmericanSocket europeanAdapter = new EuropeanToAmericanAdapter();europeanAdapter.plugIntoAmericanOutlet();// 甚至可以混合使用List<AmericanSocket> devices = Arrays.asList(new AmericanPlug(), // 原生美式new EuropeanToAmericanAdapter() // 适配的欧式);devices.forEach(AmericanSocket::plugIntoAmericanOutlet);} }
关键点:
适配器
EuropeanToAmericanAdapter
继承了EuropeanPlug
(Adaptee) 并 实现了AmericanSocket
(Target) 接口。适配器直接重用了
EuropeanPlug
的方法(通过继承)。当客户端调用
plugIntoAmericanOutlet()
时,适配器调用其继承来的plugIntoEuropeanOutlet()
方法。为什么对象适配器更常用?
灵活性: 对象适配器使用组合,一个适配器可以适配任何
EuropeanPlug
的子类(只要接口一致)。类适配器在编译时就固定了它只能适配EuropeanPlug
或它的特定子类。解耦: 对象适配器将适配器与 Adaptee 的实现解耦。适配器只依赖于 Adaptee 的接口。类适配器直接继承了 Adaptee 的实现,耦合度更高。
遵循“组合优于继承”原则: 组合通常比继承提供更大的灵活性和更少的副作用。
Adaptee 是类或接口均可: 对象适配器也能适配实现了某个接口的 Adaptee。类适配器要求 Adaptee 必须是具体类(Java 不支持多继承,所以 Adaptee 必须是类)。
适配器模式的应用场景
集成第三方库或遗留代码: 当你需要使用一个功能强大的类库,但其接口与你项目的现有接口不匹配时,创建一个适配器来封装库的调用。
系统升级/重构: 新版本组件接口改变了,但旧客户端代码仍需调用新组件。可以编写适配器让旧接口调用新组件。
统一多个类的接口: 系统中存在多个功能类似但接口不同的类,客户端希望用统一的接口调用它们。可以为每个不同的类创建适配器,让它们都实现同一个目标接口。
创建可复用的类: 设计一个类,期望它能与未来可能出现的、接口未知的类协同工作。可以先定义好目标接口,未来通过适配器来适配新类。
适配器模式的优缺点
优点:
提高类的复用性: 让原本不兼容的类可以一起工作,复用已有的功能。
提高灵活性: 通过更换不同的适配器,可以灵活地使用不同的 Adaptee。
目标与实现解耦: 客户端代码只依赖目标接口,与 Adaptee 的具体实现解耦。
符合开闭原则: 引入新的 Adaptee 类型时,只需添加新的适配器类,无需修改现有客户端代码和目标接口。
缺点:
增加复杂性: 引入了额外的适配器类,增加了系统的类和对象的数量。
过度使用可能导致混乱: 如果系统中适配器过多,可能会使代码变得难以理解和维护。
可能降低效率 (微乎其微): 多了一层间接调用,理论上会有轻微性能开销(但在绝大多数场景下可忽略不计)。
总结
适配器模式是 Java 中解决接口不兼容问题的强大工具,它像一座桥梁连接了两个不匹配的世界。对象适配器(使用组合) 是更通用、更推荐的方式。在需要集成旧系统、使用第三方库或统一不同接口时,适配器模式能显著提高代码的复用性、灵活性和可维护性。理解其核心思想(转换接口)和两种实现方式的区别(组合 vs 继承)是应用好该模式的关键。