设计模式预计阅读 14 分钟5381 字1578 次浏览

设计模式修炼之路 ⑥ 适配器模式——让不兼容的接口握手

类适配器 vs 对象适配器、在 Spring MVC 里找它

目录

上一篇《设计模式修炼之路-原型模式》收官了创建型模式:单例、工厂、建造者、原型,四种"造对象"的姿势全部到齐。从这篇起进入结构型模式——关注的不再是"怎么造对象",而是"类和对象之间怎么组装搭配",一共八位选手。开篇登场的,就是生活里最常见的那个小玩意:适配器模式(Adapter Pattern)。手里攥着三相插头、墙上却是两孔插座时,你第一个想到的就是它。

一、什么是适配器模式?

先看定义:

将一个类的接口转换成客户期望的另一个接口,使原本接口不兼容而不能一起工作的类可以一起工作。

翻译成人话:接口对不上时,谁也不改,中间加一层转换器。

生活类比不需要想象力:家里墙上插座输出 220V 交流电,手机电池只吃 5V 直流电。你不会去砸墙重新布线(改造供电方),也不会把手机拆开改电路(改造用电方),而是买一个充电头(适配器)夹在中间,一头插墙上、一头接手机,各说各的语言,中间有人翻译。GoF 给它起的别名就很直白——包装器(Wrapper)。

适配器模式有三个角色:

  • Target(目标接口):客户端想要的接口形态,上例里就是"输出 5V";
  • Adaptee(被适配者):已经存在、但接口对不上的类,上例里就是"输出 220V"的插座;
  • Adapter(适配器):中间那层转换器,把 Adaptee 的能力翻译成 Target 的样子。

按实现方式又分三种形态:类适配器(靠继承)、对象适配器(靠组合)、接口适配器(靠空实现兜底)。前两种是主角,第三种是衍生玩法,第四节全部实现。

二、适配器模式的优缺点

优点

  1. 两头都不改。Target 不动、Adaptee 不动,中间加一层就能协同工作,完美符合开闭原则——对扩展开放,对修改关闭。
  2. 复用存量代码。老系统的类、第三方的 SDK,包一层就能接入新体系,是"接盘"遗留代码改造成本最低的方式。
  3. 对客户端透明。客户端只认 Target 接口,Adaptee 被彻底藏在适配器背后,换实现客户端毫无感知。
  4. 对象适配器还支持运行时换被适配者,构造函数传谁就适配谁。

缺点

  1. 类适配器受单继承限制。Java 里 extends 名额只有一个,被 Adaptee 占了就不能再继承别的类;而且类适配器一次只能适配一个特定的 Adaptee。
  2. 对象适配器难以覆写被适配者的行为。毕竟是组合持有而非继承,想改造 Adaptee 内部逻辑有心无力。
  3. 中间层多了容易套娃。适配器用上瘾之后,系统里到处是"包一层再包一层",调用链变长,排查问题得多扒好几层皮。
  4. 滥用警告:如果接口不兼容是自家代码造成的,优先重构统一接口,而不是拿适配器当遮羞布——它是给"改不了、改不起"的代码准备的急救包,不是日常开发的主力武器。

三、适配器模式的应用场景

适配器模式在 JDK 和主流框架里无处不在:

  • JDK 里:InputStreamReader 把 InputStream(字节流)适配成 Reader(字符流)——你写过的 new BufferedReader(new InputStreamReader(System.in)) 就是两个适配器叠着用;Arrays.asList() 把数组适配成 List 视图。
  • Spring MVC 里:HandlerAdapter——名字直接写在脸上。DispatcherServlet 要面对多种形态的处理器(@Controller 方法、HttpRequestHandler、Servlet……),它不想为每种类型写一坨 if-else,于是给每种处理器配一个适配器,统一调 adapter.handle(...)。想支持新处理器类型?加个新 HandlerAdapter 就行,DispatcherServlet 一行不改。
  • 日志门面里:slf4j 的一堆桥接包(如 log4j-over-slf4j),把老日志框架的 API 调用"翻译"到新体系,老代码一行不改。
  • 日常业务里:对接第三方支付、短信、天气 SDK,对方的参数风格、错误码体系和你内部接口对不上——写个 Adapter 把它包成自家接口的样子,而不是把内部代码改成对方的风格。

什么时候该用:想复用一个现成类,但它的接口和你需要的不一致,而且这个类改不了(第三方)或改不起(老系统在跑、牵一发动全身)。

四、适配器模式的实现方式

示例代码在 design-pattern 项目的 adapter 包里:

design-pattern/src/main/java
└── adapter                      // 适配器模式
    ├── Voltage220V.java         // 被适配者:家用电
    ├── Voltage5V.java           // 目标接口:手机要的 5V
    ├── ClassAdapter.java        // 类适配器(继承)
    ├── ObjectAdapter.java       // 对象适配器(组合)
    ├── Phone.java               // 手机:只认 5V
    ├── AdapterTest.java         // 测试入口
    └── interfaceadapter         // 接口适配器
        ├── Animal.java          // 方法太多的接口
        ├── AnimalAdapter.java   // 全部空实现的抽象类
        └── Duck.java            // 只覆写关心的方法

场景:手机只认 5V,墙上插座输出 220V——别砸墙,也别拆手机,加个充电头。

1. 类适配器:继承 + 实现

先看两位"接口对不上"的当事人。被适配者,墙上的电:

public class Voltage220V {

    public int output220V() {
        int src = 220;
        System.out.println("被适配者输出电压:" + src + "V");
        return src;
    }
}

目标接口,手机要的电:

public interface Voltage5V {

    int output5V();
}

类适配器的写法:继承被适配者,同时实现目标接口,在接口方法里调用继承来的能力做转换:

// 类适配器:继承被适配者,实现目标接口
public class ClassAdapter extends Voltage220V implements Voltage5V {

    @Override
    public int output5V() {
        int srcV = output220V();
        int dstV = srcV / 44; // 220V 转 5V,模拟变压器
        System.out.println("类适配器转换后输出电压:" + dstV + "V");
        return dstV;
    }
}

用电方手机,它眼里只有 Voltage5V:

public class Phone {

    public void charging(Voltage5V voltage5V) {
        if (voltage5V.output5V() == 5) {
            System.out.println("电压正常,手机开始充电");
        } else {
            System.out.println("电压异常,拒绝充电");
        }
    }
}

测试代码:

private static void classAdapter() {
    Phone phone = new Phone();
    phone.charging(new ClassAdapter());
}

运行结果:

===== 类适配器 =====
被适配者输出电压:220V
类适配器转换后输出电压:5V
电压正常,手机开始充电

通了。ClassAdapter 既是 220V(继承来的),又是 5V(实现来的),output5V() 内部调 output220V() 完成降压——手机从头到尾没见过 220V。

但痛点马上浮出来:Java 单继承。这个 extends 名额被 Voltage220V 占死后,适配器就不能再继承别的类了;而且继承是"类级绑定",Adaptee 被写死在 extends 后面——哪天要适配美标 110V,只能另写一个适配器类。僵。

2. 对象适配器:组合 + 实现

于是教科书和《Effective Java》口径一致:用组合代替继承。不再 extends 被适配者,而是把它当字段持有:

// 对象适配器:组合被适配者,实现目标接口
public class ObjectAdapter implements Voltage5V {

    private final Voltage220V voltage220V;

    public ObjectAdapter(Voltage220V voltage220V) {
        this.voltage220V = voltage220V;
    }

    @Override
    public int output5V() {
        int srcV = voltage220V.output220V();
        int dstV = srcV / 44; // 220V 转 5V,模拟变压器
        System.out.println("对象适配器转换后输出电压:" + dstV + "V");
        return dstV;
    }
}

测试代码:

private static void objectAdapter() {
    Phone phone = new Phone();
    phone.charging(new ObjectAdapter(new Voltage220V()));
}

运行结果:

===== 对象适配器 =====
被适配者输出电压:220V
对象适配器转换后输出电压:5V
电压正常,手机开始充电

效果一模一样,内部结构完全不同。变化全在组合带来的自由度上:

  • 适配器不再占用继承名额,该 extends 谁 extends 谁;
  • 被适配者通过构造函数注入,传 Voltage220V 还是它的任何子类都行——不再绑定一个类,而是适配"一族"对象;
  • 运行时想换被适配者实例,传新的就是;
  • 甚至可以在一个适配器里组合多个被适配者一起干活。

这是"组合优于继承"在结构型模式里的第一次大显身手,后面桥接、装饰器、代理还会反复请它出场。另外注意 Phone.charging(Voltage5V) 的参数是接口——手机依赖的是抽象而不是具体电源,这正是适配器能成立的根基(第六篇单例时埋的"依赖倒置"伏笔,又应验了一次)。

3. 接口适配器:空实现兜底

第三种形态换个场景:接口方法太多,实现类用不上几个,却被迫给每个方法写空实现。比如一个 Animal 接口要你实现飞、游、跑、叫四件事:

public interface Animal {

    void fly();

    void swim();

    void run();

    void bark();
}

鸭子不会飞也不会叫(至少我们的鸭子不会),直接 implements 就得陪跑两个空方法。解法是先来一个"全空实现"的抽象类:

// 接口适配器:抽象类把全部方法给出空实现
public abstract class AnimalAdapter implements Animal {

    @Override
    public void fly() {
    }

    @Override
    public void swim() {
    }

    @Override
    public void run() {
    }

    @Override
    public void bark() {
    }
}

鸭子只继承这个抽象类,覆写关心的方法:

// 只覆写关心的方法,其余方法不用陪跑
public class Duck extends AnimalAdapter {

    @Override
    public void swim() {
        System.out.println("鸭子在游泳");
    }

    @Override
    public void run() {
        System.out.println("鸭子摇摇摆摆地跑");
    }
}

测试代码:

private static void interfaceAdapter() {
    Duck duck = new Duck();
    duck.swim();
    duck.run();
}

运行结果:

===== 接口适配器 =====
鸭子在游泳
鸭子摇摇摆摆地跑

JDK 里最典型的就是 WindowAdapter:MouseListener 接口有 5 个方法,你只想处理点击,剩下四个空着——继承适配器类只覆写一个就行。老 Swing 代码里它和匿名内部类是黄金搭档。

注意它的本质:前两种适配器是"把 A 的接口翻译成 B",接口适配器是"把一个太重的接口稀释成你要的浓度",严格说只是借了个名分,但面试常放在一起问,值得知道。

五、类适配器 vs 对象适配器怎么选

两种形态一字之差(extends 换成了字段),气质却大不相同:

类适配器 对象适配器
耦合方式 继承(类级绑定) 组合(对象级持有)
占用继承名额 占 不占
适配范围 一个特定的 Adaptee Adaptee 及其所有子类
覆写 Adaptee 行为 方便(直接重写) 困难
运行时换被适配者 不行 可以(构造注入)
Java 里的推荐度 受单继承限制 **首选**

结论一句话:默认对象适配器,只有当你确实需要改造(覆写)被适配者的部分行为时,才考虑类适配器。它俩的区别,本质上就是"继承 vs 组合"这场百年之争在一个模式上的投影。

写在最后

回顾这一篇:适配器模式在 Target 和 Adaptee 都不动的前提下,用中间一层转换器让不兼容的接口协同工作,是把"改不了、改不起"的存量代码接入新体系的急救包;类适配器靠继承,受单继承限制;对象适配器靠组合,灵活能换,是 Java 首选;接口适配器用全空实现的抽象类解决"接口方法太多"的陪跑之苦;Spring MVC 的 HandlerAdapter 则是它在真实框架里的高光时刻——新增处理器类型不用改 DispatcherServlet。

下一篇继续结构型模式,第二位选手:桥接模式(Bridge)。同样是"中间加一层",适配器治的是"接口对不上",桥接治的是"两个维度一起膨胀"——当形状要乘以颜色、品牌要乘以型号,继承图开始爆炸时,就该请它出场了。

花

花开空白

开发者 · 西安

相关文章

评论(0)

还没有评论,来抢沙发吧

发表评论