在日常的系统开发或重构中,我们经常会遇到这样的尴尬场景:系统需要引入一个新的第三方库,或者需要兼容一个老旧的遗留系统。但是,这些外部或老旧系统的代码往往与我们当前系统内部的标准接口不一致。甚至在很多历史遗留项目中,压根就没有定义接口规范,业务逻辑被死死地硬编码在具体的类中。
如果我们直接修改现有的核心业务代码去迎合这些“异类”,不仅工作量巨大,还会破坏系统的稳定性,严重违背了“开闭原则”(对扩展开放,对修改关闭)。这时候,适配器模式(Adapter Pattern) 就是破局的最佳利器。
今天,我将通过两个最常见的真实业务场景,带大家从零手写并深度理解 Java 中的对象适配器模式。
一、 什么是适配器模式?
适配器模式的原理其实极其生活化,它就像我们桌面上的“拓展坞”或“电源适配器”。比如你的 MacBook 只有 Type-C 接口,但你需要连接一个传统的 USB 鼠标,这时候就需要一个拓展坞在中间做一层转换。
在代码世界中,适配器模式的核心定义是:将一个类的接口转换成客户希望的另外一个接口,使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。
二、 核心目标接口 (Target)
在开始适配之前,我们首先需要明确当前系统内部正在使用的标准接口。客户端只认识、并且只应该调用这个接口。
package com.okcl.javapattern02;
/**
* 订单系统接口 (目标接口 Target)
* 代表当前系统内部统一的标准规范
*/
public interface OrderSystem {
void submitOrder(String data);
}
三、 场景一:接入不兼容的第三方 API
假设我们需要接入一个第三方提供的支付或订单路由系统,它的方法名和我们系统的标准完全不同。
1. 第三方系统代码 (Adaptee)
package com.okcl.javapattern02;
/**
* 第三方接口 (被适配者)
* 具有我们需要的功能,但接口定义与系统不兼容
*/
public class LegacyThirdPartyAP {
public void sendRequest(String data) {
System.out.printf("发送第三方请求, 数据:%s...\n", data);
}
}
2. 适配器实现 (Adapter)
我们在适配器内部直接 new 出第三方类的实例,并在重写的标准方法中去调用它。
package com.okcl.javapattern02;
/**
* 第三方接口适配器
*/
public class OrderAdapter implements OrderSystem {
// 引入第三方接口实例 (对象适配器的核心:组合)
private final LegacyThirdPartyAP legacyThirdPartyAP = new LegacyThirdPartyAP();
@Override
public void submitOrder(String data) {
// 在这里可以进行参数转换、签名加密等操作
// 最终调用第三方不兼容的接口
legacyThirdPartyAP.sendRequest(data);
}
}
四、 场景二:兼容无规范接口的遗留系统 (高频)
在真实的重构场景中,我们接手的旧项目往往没有定义规范的接口,历史遗留的业务逻辑直接写死在一个具体的类里面。这也是大家最常遇到、最让人头疼的“屎山”场景之一。
1. 历史遗留代码 (Adaptee)
package com.okcl.javapattern02;
/**
* 历史遗留老系统类
* 没有实现任何接口,逻辑直接写死
*/
public class OrderSystem2 {
public void sendMessage(String message) {
// 复杂的旧业务逻辑...
System.out.println("发送老系统消息:" + message);
}
}
2. 适配器实现 (Adapter)
为了极大地提升灵活性,这一次我们通过构造器注入的方式,将旧系统的对象动态传入适配器中。
package com.okcl.javapattern02;
/**
* 老旧系统适配器
*/
public class OrderAdapter2 implements OrderSystem {
private final OrderSystem2 orderSystem2; // 引入旧对象
// 通过构造方法注入,灵活性更高,方便后续进行单元测试或替换Mock对象
public OrderAdapter2(OrderSystem2 orderSystem2) {
this.orderSystem2 = orderSystem2;
}
@Override
public void submitOrder(String data) {
// 进行数据清洗或格式转换,最终调用旧系统逻辑
orderSystem2.sendMessage(data);
}
}
五、 客户端调用与运行结果
在客户端(如 Controller 层或 Main 方法)代码中,我们自始至终只面向统一的 OrderSystem 接口编程,完全不需要知道底层到底是第三方 API 还是历史遗留系统。
1. 客户端代码
package com.okcl.javapattern02;
public class Main {
public static void main(String[] args) {
System.out.println("--- 场景一:调用第三方接口 ---");
// 客户端只知道统一的接口,通过适配器隐式调用了第三方 API
OrderSystem orderSystem = new OrderAdapter();
orderSystem.submitOrder("订单数据-001");
System.out.println("\n--- 场景二:调用无规范的历史老系统 ---");
// 针对无接口规范的老系统,使用对象适配器并通过构造器注入实例
OrderSystem orderSystem2 = new OrderAdapter2(new OrderSystem2());
orderSystem2.submitOrder("订单数据-002");
}
}
2. 控制台输出结果
--- 场景一:调用第三方接口 ---
发送第三方请求, 数据:订单数据-001...
--- 场景二:调用无规范的历史老系统 ---
发送老系统消息:订单数据-002
从输出结果可以清晰地看到:传入的标准参数被成功透传到了底层的实际执行类中。我们的主程序完全没有出现 LegacyThirdPartyAP 或 OrderSystem2 的具体调用代码,完美实现了业务逻辑层与底层具体实现的解耦。
六、 深度思考:打破沙锅问到底
很多初学者在写完这段代码后,通常会产生两个直击灵魂的疑问。搞懂这两个问题,你的架构思维将提升一个档次。
疑问 1:既然只是写个实现类在里面调用另一个类,为什么非要叫“适配器模式”?
这其实是日常开发中最常见的直觉编码方式。之所以将其抽象为一种“设计模式”,主要基于以下考量:
建立团队沟通的“黑话”:你说“建个类去包一下”,同事可能不知道该怎么分层;但你说“写个适配器”,同事瞬间就能秒懂代码的意图和结构。
构建“防腐层”(核心):第三方接口是极其不可控的。如果明天第三方把
sendRequest方法名改了,或者加了必填参数。如果你没有适配器,你需要全局搜索替换业务代码,风险极高;但有了适配器,你只需要修改那一个适配器类即可,核心业务代码一行都不用动。这就形成了一道绝佳的防腐层。
疑问 2:为什么不用“类适配器”(继承),而清一色使用“对象适配器”(组合)?
在部分教科书里,适配器还有一种写法:让 OrderAdapter 直接 extends 旧类 OrderSystem2。但在实际工程中,99% 的场景都会像本文一样使用组合引入实例的“对象适配器”。原因极其致命:
Java 的单继承紧箍咒:一旦适配器继承了旧类,它就永远失去了继承系统核心业务父类的机会。
拒绝暴露垃圾方法:遗留类中往往有上百个废弃的
public方法。如果使用继承,这些破铜烂铁会全部暴露给适配器,带来极大的代码污染。通过组合,我们只挑需要的方法进行调用,完美隐藏了历史包袱。
总结一句话:多用组合,少用继承。 当你觉得通过组合构建一个中间类来解决接口冲突是很自然的操作时,恭喜你,你已经彻底掌握了适配器模式的精髓!