适配器模式(Adapter):类适配器、对象适配器、接口适配器

适配器模式,说白了就是「让不兼容的东西能一起工作」。

我刚开始接触设计模式时,觉得这玩意儿不就是个转接头吗?后来在项目中踩过坑才明白——它远不止这么简单。你想想看,现实中的电源适配器能把220V转成手机需要的5V,代码里的适配器也是同样的道理:把一个类的接口,转换成客户端期望的另一个接口。

为什么需要适配器?

我在一个老项目中遇到过这样的场景:系统里原本有一套日志接口,所有模块都依赖它。后来要接入第三方日志框架,接口完全不一样。改老代码?风险太大。不改?新框架用不了。

这时候适配器模式就派上用场了。它像一层「胶水」,把两套不兼容的接口粘在一起。不改动原有代码,就能让新组件正常工作。

核心定义: 将一个类的接口转换成客户希望的另一个接口。适配器模式让那些接口不兼容的类可以一起工作。

三种实现方式

适配器模式有三种常见形态:类适配器、对象适配器、接口适配器。每种都有自己的适用场景,我一个个说。

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

类适配器使用多重继承来实现。Java不支持多继承,所以通常是一个类继承被适配者,同时实现目标接口。

// 目标接口
public interface Target {
    void request();
}

// 被适配的类
public class Adaptee {
    public void specificRequest() {
        System.out.println("被适配者的方法");
    }
}

// 类适配器
public class ClassAdapter extends Adaptee implements Target {
    @Override
    public void request() {
        // 调用父类的方法
        specificRequest();
    }
}

这种方式的优点是代码简洁,缺点是继承关系是静态的,不够灵活。我个人习惯在适配逻辑比较简单、不需要适配多个类时用这种方式。

注意: 类适配器因为使用了继承,所以无法适配Adaptee的子类。如果你需要适配一个类层次结构,对象适配器更合适。

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

对象适配器使用组合方式,持有被适配者的引用。这是最常用的方式,也是我推荐的首选。

// 对象适配器
public class ObjectAdapter implements Target {
    private Adaptee adaptee;
    
    public ObjectAdapter(Adaptee adaptee) {
        this.adaptee = adaptee;
    }
    
    @Override
    public void request() {
        adaptee.specificRequest();
    }
}

// 使用
Adaptee adaptee = new Adaptee();
Target target = new ObjectAdapter(adaptee);
target.request();

对象适配器的好处是灵活。你可以传入Adaptee的任何子类,甚至可以在运行时动态替换。我曾经在一个支付系统里用过这种方式——对接了七八种不同的支付渠道,每个渠道的接口都不一样,用对象适配器轻松搞定。

对比维度 类适配器 对象适配器
实现方式 继承 组合
灵活性 低(静态绑定) 高(动态绑定)
代码量 较少 稍多
适用场景 适配逻辑简单固定 需要适配多个类或子类

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

接口适配器也叫缺省适配模式。当一个接口定义了太多方法,而你只需要实现其中几个时,可以用一个抽象类提供空实现,子类按需覆盖。

public interface Worker {
    void work();
    void eat();
    void sleep();
    void rest();
}

// 接口适配器:提供默认空实现
public abstract class WorkerAdapter implements Worker {
    @Override
    public void work() {}
    
    @Override
    public void eat() {}
    
    @Override
    public void sleep() {}
    
    @Override
    public void rest() {}
}

// 子类只需要实现关心的方法
public class Programmer extends WorkerAdapter {
    @Override
    public void work() {
        System.out.println("写代码");
    }
    
    @Override
    public void eat() {
        System.out.println("吃泡面");
    }
}

嗯,这里要注意:接口适配器不是为了解决接口不兼容的问题,而是为了简化接口的实现。我在写事件监听器时经常用这种方式——监听器接口可能有十几个方法,但我只关心其中一两个。

适配器模式在Spring MVC中的HandlerAdapter

说到实际应用,Spring MVC的HandlerAdapter是个经典案例。你想想看,Spring MVC要处理各种类型的处理器:普通的Controller、HttpRequestHandler、Servlet等等。它们的接口各不相同,怎么统一处理?

答案就是适配器模式。

Spring定义了一个HandlerAdapter接口:

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

每种处理器都有对应的适配器实现:

  • SimpleControllerHandlerAdapter — 适配实现了Controller接口的处理器
  • HttpRequestHandlerAdapter — 适配实现了HttpRequestHandler接口的处理器
  • SimpleServletHandlerAdapter — 适配Servlet类型的处理器

DispatcherServlet在收到请求后,会遍历所有HandlerAdapter,调用supports方法检查是否能处理当前处理器。找到合适的适配器后,调用handle方法执行请求。

我曾经看过Spring的源码,这个设计真的很巧妙。它让Spring MVC可以轻松扩展新的处理器类型——只要写一个新的适配器实现就行,完全不用改核心流程。

我的建议: 如果你在设计一个需要支持多种接入方式的系统,不妨参考Spring MVC的做法。定义一个适配器接口,每种接入方式写一个适配器实现。这样系统扩展性会非常好。

适配器模式的核心逻辑

下面这张图展示了适配器模式的核心结构,我画得比较简洁,方便你理解整体脉络:

适配器模式核心结构 客户端 Client 目标接口 Target 适配器 Adapter 被适配者 Adaptee 依赖 实现 持有/继承 适配器将Adaptee的接口转换成Target接口,客户端通过Target接口调用 类适配器使用继承,对象适配器使用组合

什么时候用适配器模式?

我总结了几种典型场景:

  • 系统需要复用已有的类,但接口不匹配 — 这是最直接的使用场景
  • 想要创建一个可复用的类,与多个不相关的类协同工作 — 这些类可能没有统一的接口
  • 需要适配多个版本的接口 — 比如老版本接口和新版本接口共存时
  • 不想修改现有代码,但需要接入新功能 — 适配器作为中间层,保护现有代码不受影响
避坑指南: 我曾经在一个项目里过度使用适配器,导致系统里到处都是适配器类,反而增加了复杂度。适配器模式不是万能的,如果两套接口本身设计就有问题,应该优先考虑重构接口,而不是用适配器打补丁。

总结

适配器模式的核心思想就一句话:让不兼容的接口协同工作

类适配器用继承,对象适配器用组合,接口适配器用缺省实现。三种方式各有优劣,我个人最常用的是对象适配器,因为它灵活、符合组合优于继承的原则。

Spring MVC的HandlerAdapter是个很好的学习范例,它展示了适配器模式在大型框架中如何优雅地解决多类型处理器的统一调用问题。

记住,适配器模式不是为了炫技,而是为了解决实际问题。下次遇到接口不兼容的情况,先想想能不能用适配器,而不是急着改别人的代码。

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