适配器模式:类适配器、对象适配器、实战:日志框架适配
适配器模式,说白了就是「让不兼容的东西能一起工作」。
我刚开始做企业级开发时,遇到过这么个场景:系统里用了 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。但理解适配器模式能帮你读懂这些桥接包的原理,遇到类似问题时也能自己动手实现。
适配器模式的结构图
下面这张图展示了适配器模式的核心结构。我画的是对象适配器的版本,因为实际项目中更常用。
什么时候用适配器模式?
我总结了几种典型场景:
- 系统升级或迁移:旧接口不能改,新系统需要兼容旧代码
- 引入第三方库:第三方库的接口和你的系统不匹配
- 统一多个相似接口:比如多个日志框架、多个缓存框架需要统一调用方式
避坑指南:我曾经在一个项目里过度使用适配器,把十几个不相关的接口都适配了一遍。结果适配器层越来越厚,代码反而更难维护了。适配器模式适合「接口不兼容」的场景,不要为了用模式而用模式。
类适配器 vs 对象适配器:怎么选?
| 对比维度 | 类适配器 | 对象适配器 |
|---|---|---|
| 实现方式 | 继承 | 组合 |
| 灵活性 | 低(继承是静态的) | 高(组合是动态的) |
| 封装性 | 差(父类方法暴露) | 好(只暴露需要的方法) |
| 适用场景 | 被适配类方法少且稳定 | 大多数场景 |
我个人建议:优先选对象适配器。除非你确定被适配类不会变,而且你确实需要继承它的某些 protected 方法。
小结
适配器模式不是什么高深的技术,它就是「转接头」的代码化表达。类适配器用继承,对象适配器用组合。实战中,对象适配器更灵活,也更符合设计原则。
日志框架适配是适配器模式的经典应用。你想想看,一个运行了几年的老系统,几百个类都用了 Log4j,突然要换成 SLF4J。如果没有适配器模式,那得改到什么时候?
嗯,这就是适配器模式的价值——让新旧代码和平共处,让系统平滑演进。