适配器模式:让不兼容的接口“握手言和”
说实话,适配器模式是我在日常开发中用得最多的模式之一。为什么?因为现实世界里的代码,从来不会按照你理想中的接口来设计。你接手一个老系统,或者要对接第三方库,总会遇到“接口对不上”的尴尬局面。
适配器模式,说白了就是做“翻译”的。它把一个类的接口,转换成客户端期望的另一个接口。让原本不兼容的类,能在一起愉快地工作。
我当年刚入行时,有一次要对接一个老旧的短信发送系统。对方只提供了一种很原始的接口,而我们系统里已经封装好了统一的发送接口。改老系统?风险太大。改我们自己的接口?那所有调用方都得改。怎么办?适配器模式就是最好的答案。
适配器模式的三种形态
适配器模式在Java里,主要有三种实现方式。我按自己的习惯,把它们分成:类适配器、对象适配器、接口适配器。每种都有各自的适用场景。
类适配器(基于继承)
类适配器通过继承来实现。适配器类同时继承目标接口和源类。这种方式比较直接,但Java是单继承,所以灵活性受限。
举个例子。假设我们有一个220V的电压源,但客户端需要的是5V电压。
// 源类:220V电压
public class Voltage220V {
public int output220V() {
return 220;
}
}
// 目标接口:5V电压
public interface Voltage5V {
int output5V();
}
// 类适配器:继承源类,实现目标接口
public class VoltageAdapter extends Voltage220V implements Voltage5V {
@Override
public int output5V() {
int src = output220V();
int dst = src / 44; // 降压处理
return dst;
}
}
// 客户端使用
public class Phone {
public void charge(Voltage5V v) {
System.out.println("手机充电,电压:" + v.output5V() + "V");
}
}
// 测试
public class Client {
public static void main(String[] args) {
Phone phone = new Phone();
phone.charge(new VoltageAdapter());
}
}
你看,适配器把220V转成了5V。客户端只认5V接口,根本不知道背后有220V的存在。这就是适配器的核心价值——解耦。
注意:类适配器用了继承,所以它只能适配一个源类。如果你需要适配多个源,或者源类已经被其他类继承了,这条路就走不通了。
对象适配器(基于组合)
对象适配器是我个人最推荐的方式。它用组合代替继承,把源对象作为适配器的一个成员变量。这样更灵活,也符合“组合优于继承”的设计原则。
// 对象适配器:持有源对象,实现目标接口
public class VoltageAdapter2 implements Voltage5V {
private Voltage220V voltage220V;
public VoltageAdapter2(Voltage220V voltage220V) {
this.voltage220V = voltage220V;
}
@Override
public int output5V() {
int src = voltage220V.output220V();
int dst = src / 44;
return dst;
}
}
// 客户端使用
public class Client2 {
public static void main(String[] args) {
Phone phone = new Phone();
phone.charge(new VoltageAdapter2(new Voltage220V()));
}
}
我在项目中遇到过类似场景。当时要对接三个不同的支付渠道,每个渠道的接口都不一样。我写了一个统一的支付接口,然后为每个渠道写了一个对象适配器。这样,业务层只认我的统一接口,切换渠道时只需要换适配器就行。维护起来特别轻松。
我的建议:能用对象适配器就别用类适配器。组合比继承灵活得多,而且不会破坏封装性。
接口适配器(默认适配模式)
接口适配器,也叫“默认适配模式”。它解决的是另一个问题:当一个接口定义了太多方法,而你只需要用到其中几个时,怎么办?
你想想看,如果直接实现这个接口,你就得把所有方法都实现一遍,哪怕大部分都是空方法。代码里全是空的实现,看着就烦。
接口适配器的做法是:写一个抽象类,把接口里的所有方法都提供默认实现(通常是空实现)。然后你的业务类只需要继承这个抽象类,重写你需要的方法就行。
// 一个有很多方法的接口
public interface ManyMethods {
void methodA();
void methodB();
void methodC();
void methodD();
void methodE();
}
// 接口适配器:提供默认空实现
public abstract class ManyMethodsAdapter implements ManyMethods {
@Override
public void methodA() {}
@Override
public void methodB() {}
@Override
public void methodC() {}
@Override
public void methodD() {}
@Override
public void methodE() {}
}
// 业务类:只关心自己需要的方法
public class MyBusiness extends ManyMethodsAdapter {
@Override
public void methodA() {
System.out.println("我只实现了methodA");
}
}
我曾经在做一个事件监听系统时,就用到了接口适配器。事件接口有十几个方法,但每个监听器只关心其中一两个事件。如果没有适配器,每个监听器类里都会有一堆空方法,代码可读性极差。
三种适配器对比
| 类型 | 实现方式 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| 类适配器 | 继承 | 代码简单,直接 | 单继承限制,不够灵活 | 源类单一,且未被继承 |
| 对象适配器 | 组合 | 灵活,可适配多个源 | 代码稍多 | 大多数场景,推荐使用 |
| 接口适配器 | 抽象类+默认实现 | 减少冗余代码 | 增加了一个抽象类 | 接口方法过多,只需部分实现 |
适配器模式的核心逻辑
下面这张图,是我自己画的适配器模式结构图。它清晰地展示了三种适配器之间的关系。
实际项目中的避坑指南
适配器模式看起来简单,但用不好也会踩坑。我分享几个自己踩过的坑。
坑一:过度适配。我曾经在一个项目里,为了追求“完美设计”,给每个接口都加了适配器。结果代码量翻倍,维护成本剧增。适配器模式是用来解决“接口不兼容”问题的,不是用来“预防”的。不要为了用模式而用模式。
坑二:适配器里做太多事。适配器的职责很单一:做接口转换。不要在适配器里加业务逻辑、加缓存、加日志。我见过有人把适配器写成了“万能处理器”,结果出了问题都不知道是适配器的问题还是业务逻辑的问题。
坑三:忽略性能。适配器会引入一层间接调用。如果调用非常频繁,比如在循环里每次都要经过适配器,那就要考虑性能损耗了。我一般会在适配器里加个简单的缓存,或者直接优化调用方式。
核心要点:适配器模式的核心是“转换”而非“增强”。它只是让接口匹配,不改变原有功能。记住这一点,你就不会用错。
什么时候用适配器模式?
我总结了几条经验,供你参考:
- 你想用一个已有的类,但它的接口不符合你的需求
- 你想创建一个可复用的类,让它与不相关的类协同工作
- 你想使用一些已有的子类,但不可能对每个子类都进行适配
- 你需要在多个不同的接口之间做转换
嗯,适配器模式就讲到这里。它不复杂,但很实用。你想想看,我们每天都在和各种接口打交道,适配器模式就是那个让一切变得顺畅的“润滑剂”。
公众号:蓝海资料掘金营,微信deep3321