适配器模式:类适配器、对象适配器、实战:日志框架适配

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

我刚开始做企业级开发时,遇到过这么个场景:系统里用了 Log4j,但新引入的第三方框架只认 SLF4J 接口。两套日志体系,总不能各打各的吧?那时候我还没系统学过设计模式,硬着头皮写了个转换类,后来才发现——这不就是适配器模式嘛。

今天我们就来聊聊这个模式。我会把类适配器、对象适配器讲清楚,再带你看一个日志框架适配的实战案例。

适配器模式的核心思想

适配器模式解决的是接口不兼容的问题。它像是一个转接头,把客户端的调用转换成目标对象能理解的形式。

举个例子:你有个 USB-C 接口的笔记本,但会议室投影仪只支持 HDMI。怎么办?买个转接头。适配器模式就是这个转接头。

在 Java 里,适配器模式有两种实现方式:

  • 类适配器:通过继承来实现
  • 对象适配器:通过组合来实现

我个人更推荐对象适配器,原因后面会讲。

类适配器

类适配器使用继承,适配器类同时继承目标类和被适配类。说白了就是「多继承」的变通方案。

看代码:

// 目标接口:客户端期望的 5V 电压
public interface IVoltage5V {
    int output5V();
}

// 被适配类:220V 电压
public class Voltage220V {
    public int output220V() {
        return 220;
    }
}

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

// 客户端使用
public class Phone {
    public void charge(IVoltage5V v) {
        if (v.output5V() == 5) {
            System.out.println("电压正常,开始充电");
        }
    }
}

类适配器的优点是代码简单,不需要额外对象。但缺点也很明显:

  • Java 是单继承,适配器一旦继承了被适配类,就不能再继承其他类
  • 被适配类的方法全部暴露给客户端,破坏了封装性

我在项目中很少用类适配器,除非被适配类的方法确实都需要暴露。

对象适配器

对象适配器使用组合,适配器类持有被适配对象的引用。这是更灵活的方式。

// 对象适配器:持有被适配对象,实现目标接口
public class VoltageAdapter2 implements IVoltage5V {
    private Voltage220V voltage220V;

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

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

// 客户端使用
public class Client {
    public static void main(String[] args) {
        Voltage220V v220 = new Voltage220V();
        IVoltage5V adapter = new VoltageAdapter2(v220);
        Phone phone = new Phone();
        phone.charge(adapter);
    }
}

对象适配器的好处:

  • 不破坏继承体系,适配器可以继承其他类
  • 被适配对象可以动态替换,运行时灵活切换
  • 符合「组合优于继承」的设计原则

核心区别一句话总结:类适配器是「我是谁我就继承谁」,对象适配器是「我需要谁我就持有谁」。

实战:日志框架适配

好了,理论讲完,我们来看一个真实场景。

我记得有个老项目,日志用的是 Log4j 1.x。后来公司统一要求迁移到 SLF4J + Logback。但项目里几百个类都直接调用了 Log4j 的 API,全改一遍不现实。

这时候适配器模式就派上用场了。我们写一个适配器,把 Log4j 的 Logger 包装成 SLF4J 的 Logger 接口。

// SLF4J 的 Logger 接口(简化版)
public interface Slf4jLogger {
    void info(String msg);
    void error(String msg);
    void warn(String msg);
}

// Log4j 的 Logger 类(假设已有)
public class Log4jLogger {
    public void logInfo(String msg) {
        System.out.println("[Log4j] INFO: " + msg);
    }
    public void logError(String msg) {
        System.out.println("[Log4j] ERROR: " + msg);
    }
    public void logWarn(String msg) {
        System.out.println("[Log4j] WARN: " + msg);
    }
}

// 适配器:把 Log4jLogger 适配成 Slf4jLogger
public class Log4jToSlf4jAdapter implements Slf4jLogger {
    private Log4jLogger log4jLogger;

    public Log4jToSlf4jAdapter(Log4jLogger log4jLogger) {
        this.log4jLogger = log4jLogger;
    }

    @Override
    public void info(String msg) {
        log4jLogger.logInfo(msg);
    }

    @Override
    public void error(String msg) {
        log4jLogger.logError(msg);
    }

    @Override
    public void warn(String msg) {
        log4jLogger.logWarn(msg);
    }
}

// 客户端使用
public class Application {
    public static void main(String[] args) {
        Log4jLogger oldLogger = new Log4jLogger();
        Slf4jLogger logger = new Log4jToSlf4jAdapter(oldLogger);
        logger.info("系统启动成功");
        logger.error("数据库连接失败");
    }
}

你看,客户端代码完全不用改,只需要在初始化时把适配器注入进去就行。这就是适配器模式的威力——不改动现有代码,就能让新旧系统协同工作

我的经验:实际项目中,日志框架适配往往不是自己手写,而是用现成的桥接包,比如 log4j-over-slf4j。但理解适配器模式能帮你读懂这些桥接包的原理,遇到类似问题时也能自己动手实现。

适配器模式的结构图

下面这张图展示了适配器模式的核心结构。我画的是对象适配器的版本,因为实际项目中更常用。

适配器模式结构图(对象适配器) Client Target(接口) Adapter Adaptee 依赖 实现 持有引用 Client 依赖 Target 接口 Adapter 实现 Target 接口,同时持有 Adaptee 引用 调用时,Adapter 将请求转发给 Adaptee 处理

什么时候用适配器模式?

我总结了几种典型场景:

  • 系统升级或迁移:旧接口不能改,新系统需要兼容旧代码
  • 引入第三方库:第三方库的接口和你的系统不匹配
  • 统一多个相似接口:比如多个日志框架、多个缓存框架需要统一调用方式

避坑指南:我曾经在一个项目里过度使用适配器,把十几个不相关的接口都适配了一遍。结果适配器层越来越厚,代码反而更难维护了。适配器模式适合「接口不兼容」的场景,不要为了用模式而用模式。

类适配器 vs 对象适配器:怎么选?

对比维度 类适配器 对象适配器
实现方式 继承 组合
灵活性 低(继承是静态的) 高(组合是动态的)
封装性 差(父类方法暴露) 好(只暴露需要的方法)
适用场景 被适配类方法少且稳定 大多数场景

我个人建议:优先选对象适配器。除非你确定被适配类不会变,而且你确实需要继承它的某些 protected 方法。

小结

适配器模式不是什么高深的技术,它就是「转接头」的代码化表达。类适配器用继承,对象适配器用组合。实战中,对象适配器更灵活,也更符合设计原则。

日志框架适配是适配器模式的经典应用。你想想看,一个运行了几年的老系统,几百个类都用了 Log4j,突然要换成 SLF4J。如果没有适配器模式,那得改到什么时候?

嗯,这就是适配器模式的价值——让新旧代码和平共处,让系统平滑演进


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