适配器模式:让不兼容的接口“握手言和”

说实话,适配器模式是我在日常开发中用得最多的模式之一。为什么?因为现实世界里的代码,从来不会按照你理想中的接口来设计。你接手一个老系统,或者要对接第三方库,总会遇到“接口对不上”的尴尬局面。

适配器模式,说白了就是做“翻译”的。它把一个类的接口,转换成客户端期望的另一个接口。让原本不兼容的类,能在一起愉快地工作。

我当年刚入行时,有一次要对接一个老旧的短信发送系统。对方只提供了一种很原始的接口,而我们系统里已经封装好了统一的发送接口。改老系统?风险太大。改我们自己的接口?那所有调用方都得改。怎么办?适配器模式就是最好的答案。

适配器模式的三种形态

适配器模式在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");
    }
}

我曾经在做一个事件监听系统时,就用到了接口适配器。事件接口有十几个方法,但每个监听器只关心其中一两个事件。如果没有适配器,每个监听器类里都会有一堆空方法,代码可读性极差。

三种适配器对比

类型 实现方式 优点 缺点 适用场景
类适配器 继承 代码简单,直接 单继承限制,不够灵活 源类单一,且未被继承
对象适配器 组合 灵活,可适配多个源 代码稍多 大多数场景,推荐使用
接口适配器 抽象类+默认实现 减少冗余代码 增加了一个抽象类 接口方法过多,只需部分实现

适配器模式的核心逻辑

下面这张图,是我自己画的适配器模式结构图。它清晰地展示了三种适配器之间的关系。

适配器模式三种形态结构图 客户端 (Client) 目标接口 (Target) 依赖 类适配器 (继承源类 + 实现目标接口) 对象适配器 (持有源对象 + 实现目标接口) 接口适配器 (抽象类 + 默认空实现) 源类 (Adaptee) 继承 源对象 (Adaptee) 持有 接口 (Interface) 实现 三种适配器都实现了目标接口,但适配源的方式不同 类适配器用继承,对象适配器用组合,接口适配器用默认实现

实际项目中的避坑指南

适配器模式看起来简单,但用不好也会踩坑。我分享几个自己踩过的坑。

坑一:过度适配。我曾经在一个项目里,为了追求“完美设计”,给每个接口都加了适配器。结果代码量翻倍,维护成本剧增。适配器模式是用来解决“接口不兼容”问题的,不是用来“预防”的。不要为了用模式而用模式。

坑二:适配器里做太多事。适配器的职责很单一:做接口转换。不要在适配器里加业务逻辑、加缓存、加日志。我见过有人把适配器写成了“万能处理器”,结果出了问题都不知道是适配器的问题还是业务逻辑的问题。

坑三:忽略性能。适配器会引入一层间接调用。如果调用非常频繁,比如在循环里每次都要经过适配器,那就要考虑性能损耗了。我一般会在适配器里加个简单的缓存,或者直接优化调用方式。

核心要点:适配器模式的核心是“转换”而非“增强”。它只是让接口匹配,不改变原有功能。记住这一点,你就不会用错。

什么时候用适配器模式?

我总结了几条经验,供你参考:

  • 你想用一个已有的类,但它的接口不符合你的需求
  • 你想创建一个可复用的类,让它与不相关的类协同工作
  • 你想使用一些已有的子类,但不可能对每个子类都进行适配
  • 你需要在多个不同的接口之间做转换

嗯,适配器模式就讲到这里。它不复杂,但很实用。你想想看,我们每天都在和各种接口打交道,适配器模式就是那个让一切变得顺畅的“润滑剂”。

公众号:蓝海资料掘金营,微信deep3321