适配器模式:让不兼容的接口握手合作

适配器模式,说白了就是「转接头」。你想想看,Type-C 转 USB、三孔转两孔、HDMI 转 VGA——生活中到处都是适配器。代码里也一样,两个接口不匹配,中间加一层适配器,问题就解决了。

我最早接触这个模式是在做一个遗留系统重构的项目。老系统用 XML 传数据,新系统只认 JSON。不改老系统,也不能让新系统妥协,怎么办?加个适配器呗。嗯,这就是适配器模式的典型场景。

适配器模式的三种形态

适配器模式在 GoF 书里其实只分了两种:类适配器和对象适配器。但实际工作中,我习惯把「接口适配器」也单独拎出来讲,因为它在 Java 里太常用了。下面一个个说。

1. 类适配器(通过继承实现)

类适配器用多重继承来实现。Java 不支持多继承,所以类适配器在 Java 里用得少。C++ 倒是可以,但我个人不太推荐,因为继承耦合太强。

先看一个场景:我有一个 Volt220 类,输出 220V 电压。客户端需要 5V 电压。我需要一个适配器,把 220V 转成 5V。

// 目标接口:5V
public interface IVolt5 {
    int output5V();
}

// 被适配者:220V
public class Volt220 {
    public int output220V() {
        return 220;
    }
}

// 类适配器:继承被适配者,实现目标接口
public class ClassAdapter extends Volt220 implements IVolt5 {
    @Override
    public int output5V() {
        int volt = output220V();
        // 降压处理
        return volt / 44;
    }
}

// 客户端使用
public class Client {
    public static void main(String[] args) {
        IVolt5 adapter = new ClassAdapter();
        System.out.println("输出电压:" + adapter.output5V() + "V");
    }
}

这段代码逻辑很简单。但注意,类适配器把 Volt220 的所有方法都继承下来了。如果 Volt220 还有其他方法,客户端也能调用到,这可能不是我们想要的。

避坑指南:我曾经在一个项目中用类适配器,结果被适配的类有几十个 protected 方法,子类全暴露了。后来重构时改成了对象适配器,才把接口收窄。所以,类适配器适合被适配者接口简单、且你确实需要继承其行为的情况。

2. 对象适配器(通过组合实现)

对象适配器用组合代替继承。适配器里持有被适配者的引用,调用时委托给它。这是我最推荐的方式,也是实际项目中最常见的。

// 对象适配器:持有被适配者,实现目标接口
public class ObjectAdapter implements IVolt5 {
    private Volt220 volt220;

    public ObjectAdapter(Volt220 volt220) {
        this.volt220 = volt220;
    }

    @Override
    public int output5V() {
        int volt = volt220.output220V();
        return volt / 44;
    }
}

// 客户端使用
public class Client {
    public static void main(String[] args) {
        Volt220 volt220 = new Volt220();
        IVolt5 adapter = new ObjectAdapter(volt220);
        System.out.println("输出电压:" + adapter.output5V() + "V");
    }
}

你看,对象适配器更灵活。被适配者可以运行时传入,甚至可以通过工厂方法动态选择不同的被适配者。我在项目中遇到过需要适配多种数据库的场景,用对象适配器,一个适配器类就能搞定 MySQL、Oracle、PostgreSQL 的差异。

我的习惯:除非有明确的继承需求,否则一律用对象适配器。组合优于继承,这话在适配器模式里体现得最明显。

3. 接口适配器(缺省适配模式)

接口适配器,也叫缺省适配模式。它的场景是这样的:一个接口定义了太多方法,但客户端只需要其中几个。你让客户端直接实现接口?那得写一堆空方法,太烦了。

解决方案:写一个抽象类,把接口所有方法都实现一遍(方法体为空)。客户端只需要继承这个抽象类,重写自己需要的方法即可。

// 一个有很多方法的接口
public interface IWorker {
    void work();
    void eat();
    void sleep();
    void rest();
    void meeting();
}

// 接口适配器:空实现所有方法
public abstract class WorkerAdapter implements IWorker {
    @Override
    public void work() {}

    @Override
    public void eat() {}

    @Override
    public void sleep() {}

    @Override
    public void rest() {}

    @Override
    public void meeting() {}
}

// 客户端只需要实现 work 方法
public class Programmer extends WorkerAdapter {
    @Override
    public void work() {
        System.out.println("写代码...");
    }
}

Java 的 java.awt.event.MouseAdapter 就是典型的接口适配器。你写鼠标事件时,只需要继承 MouseAdapter,重写 mouseClicked 就行了,不用把 mousePressedmouseReleased 等七八个方法全实现一遍。

Spring MVC 中的适配器模式

说到适配器模式的实际应用,Spring MVC 的 HandlerAdapter 是个经典案例。我当年看 Spring 源码时,看到这块忍不住拍了一下大腿——原来适配器还能这么用。

Spring MVC 里,处理器(Handler)有多种类型:

  • Controller 接口(实现 handleRequest 方法)
  • HttpRequestHandler 接口(实现 handleRequest 方法,但参数不同)
  • Servlet 接口(实现 service 方法)
  • 注解驱动的 @RequestMapping 方法

这些处理器的调用方式各不相同。如果没有适配器,DispatcherServlet 得写一堆 if-else 来判断处理器类型,然后分别调用。代码会变成这样:

if (handler instanceof Controller) {
    ((Controller) handler).handleRequest(request, response);
} else if (handler instanceof HttpRequestHandler) {
    ((HttpRequestHandler) handler).handleRequest(request, response);
} else if (handler instanceof Servlet) {
    ((Servlet) handler).service(request, response);
} // ... 越来越长

Spring 的做法是定义一个 HandlerAdapter 接口:

public interface HandlerAdapter {
    boolean supports(Object handler);
    ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception;
}

每种处理器对应一个适配器实现:

  • SimpleControllerHandlerAdapter —— 适配 Controller 接口
  • HttpRequestHandlerAdapter —— 适配 HttpRequestHandler 接口
  • SimpleServletHandlerAdapter —— 适配 Servlet 接口
  • RequestMappingHandlerAdapter —— 适配 @RequestMapping 方法

DispatcherServlet 拿到处理器后,遍历所有 HandlerAdapter,找到 supports 返回 true 的那个,然后调用 handle 方法。整个过程完全符合开闭原则——新增一种处理器类型,只需要新增一个适配器实现,不需要改 DispatcherServlet

核心思想:适配器模式让 DispatcherServlet 和具体处理器解耦。DispatcherServlet 只依赖 HandlerAdapter 接口,不依赖任何具体的处理器实现。这就是面向接口编程的威力。

三种适配器的对比

类型 实现方式 耦合度 灵活性 适用场景
类适配器 继承被适配者 高(继承耦合) 低(编译时确定) 被适配者接口简单,需要继承其行为
对象适配器 组合被适配者 低(组合耦合) 高(运行时动态组合) 大多数场景,推荐使用
接口适配器 抽象类空实现 接口方法太多,客户端只需要部分方法

适配器模式的核心逻辑

下面这张图展示了适配器模式的核心结构。你可以看到,客户端只依赖目标接口,适配器在中间做转换,被适配者完全不知道适配器的存在。

适配器模式核心结构 Client(客户端) «interface» Target request() Adapter(适配器) request() → adaptee.specificRequest() Adaptee(被适配者) specificRequest() 依赖 实现 持有引用 客户端只依赖 Target 接口,不依赖 Adaptee Adapter 在中间做转换,对客户端透明 新增 Adaptee 只需新增 Adapter,符合开闭原则

什么时候用适配器模式?

我总结了几条经验,你写代码时可以对照着看:

  1. 想用现成的类,但它的接口和你的系统不匹配——加一层适配器,别改现成类的代码。
  2. 想创建一个可复用的类,它需要和多个未来可能出现的类协作——让这个类依赖目标接口,具体适配由适配器完成。
  3. 系统中存在多个功能相似但接口不同的类——用适配器把它们统一到同一个接口下,客户端代码就干净了。
一个小建议:适配器模式不要滥用。如果系统里到处都是适配器,说明你的接口设计可能有问题。我见过一个项目,三层架构每层之间都套了适配器,代码读起来像俄罗斯套娃。适配器是用来解决「遗留系统整合」和「第三方库接入」问题的,不是用来做常规架构设计的。

好了,适配器模式就讲到这里。记住一句话:适配器不改变功能,只改变接口。下次遇到接口不兼容的问题,先想想能不能加个适配器,而不是去改别人的代码。