文章目录
-
- 前言
- [一、 核心定义](#一、 核心定义)
- [二、 标准体系结构图](#二、 标准体系结构图)
- [三、 场景推演](#三、 场景推演)
- 四、实战案例一:MQ消息适配
-
- [4.1 需求分析](#4.1 需求分析)
- [4.2 架构图](#4.2 架构图)
-
- [4.2.1 面条代码架构图](#4.2.1 面条代码架构图)
- [4.2.2 适配器模式架构图](#4.2.2 适配器模式架构图)
- [4.3 时序图](#4.3 时序图)
-
- [4.3.1 面条代码时序图](#4.3.1 面条代码时序图)
- [4.3.2 适配器模式类图](#4.3.2 适配器模式类图)
- 五、实战案例二:redis缓存集群适配
-
- [5.1 需求分析](#5.1 需求分析)
- [5.2 架构图](#5.2 架构图)
-
- [5.2.1 面条代码架构图](#5.2.1 面条代码架构图)
- [5.2.2 适配器模式架构图](#5.2.2 适配器模式架构图)
- [5.3 时序图](#5.3 时序图)
-
- [5.3.1 面条代码架构图](#5.3.1 面条代码架构图)
- [5.3.2 适配器模式架构图](#5.3.2 适配器模式架构图)
- 总结
前言
在软件工程的实际演进中,我们经常会面临一种进退两难的局面:
系统需要引入一个非常核心的现存组件或第三方库,但它的接口标准与我们当前系统的主流架构完全不兼容。
如果我们为了迎合这个外部组件而大规模修改核心代码,不仅会打破现有的稳定性,还会造成严重的逻辑污染。
适配器模式就是为了这种"亡羊补牢"或"新老交替"的场景而生的。
它就像是一个软件层面的"扩展坞",优雅地在不兼容的接口之间建立起一座桥梁,让系统能够无缝地复用既有资产。
本文参考博客:
本文代码链接:https://github.com/likerhood/CodeDesignWork/tree/main/codedesign5.0-0 到codedesign5.1-2都是
一、 核心定义
适配器模式(Adapter Pattern) 旨在将一个类的接口转换成客户希望的另外一个接口。它使得原本由于接口不兼容而不能一起工作的那些类可以协同工作。
- 本质: 接口转换与兼容性适配。
- 分类: 主要分为"对象适配器"(基于组合机制)和"类适配器"(基于继承机制)。在 Java 生态中,由于遵循"组合优于继承"的原则,我们绝大多数情况下采用的是对象适配器,因为它更加灵活,且能突破单继承的限制。

二、 标准体系结构图
在适配器模式的标准体系中,通常包含以下四个关键角色:
- Target(目标接口): 当前系统业务所期待的统一标准接口。客户端只认这个接口。
- Adaptee(被适配者): 已经存在的、包含核心逻辑但接口与 Target 不兼容的类或第三方组件。
- Adapter(适配器): 模式的核心枢纽。它实现了 Target 接口,并在内部持有一个 Adaptee 的实例。它负责接收客户端的请求,并将其"翻译"成 Adaptee 能够理解的特定方法调用。
- Client(客户端): 针对 Target 接口进行编程的调用方,对底层的 Adaptee 完全无感知。
面向目标接口调用
实现统一接口
转调原有能力
Client
+request()
<<interface>>
Target
+request()
Adapter
-Adaptee adaptee
+request()
Adaptee
+specificRequest()
三、 场景推演
假设我们正在构建一个用于特定垂直领域的问答系统,系统底层基于检索增强生成(RAG)架构。
在系统初期,我们定义了一个统一的现代化大模型接口标准。
但现在,我们需要接入一个早年开发、专门针对某些特定维护手册微调过的老旧本地化模型服务。
1. 定义 Target(目标接口)
这是我们系统内部统一的调用标准,客户端都基于此接口开发。
Java
// Target: 现代 RAG 系统的统一大模型接口
public interface StandardRAGClient {
/**
* @param prompt 用户的提问
* @param context 检索到的上下文
*/
String generateAnswer(String prompt, String context);
}
2. 引入 Adaptee(被适配者)
这是一个老旧的模型服务,它的方法签名完全不一样,甚至要求将所有输入打包成一个特定的 JSON 字符串。
Java
// Adaptee: 外部老旧的模型服务,接口不兼容
public class LegacyLocalModelService {
public String executeInference(String combinedPayload) {
System.out.println("LegacyLocalModelService: 正在解析合并后的复杂 Payload 并执行推理...");
return "基于特定维护手册生成的回答...";
}
}
3. 构建 Adapter(适配器)
我们创建一个适配器,实现目标接口,并在内部包装老旧服务,完成输入输出的"翻译"。
Java
// Adapter: 适配器类,通过组合引入 Adaptee
public class LegacyModelAdapter implements StandardRAGClient {
private LegacyLocalModelService legacyService;
public LegacyModelAdapter(LegacyLocalModelService legacyService) {
this.legacyService = legacyService;
}
@Override
public String generateAnswer(String prompt, String context) {
// 1. 将现代接口的参数"翻译"成老旧服务需要的格式
String payload = String.format("{\"context\": \"%s\", \"question\": \"%s\"}", context, prompt);
// 2. 委托给 Adaptee 执行真正的推理逻辑
String rawResult = legacyService.executeInference(payload);
// 3. (可选) 将 Adaptee 的返回结果解析适配回系统期待的格式
return "[已适配处理] " + rawResult;
}
}
4. 客户端调用
系统的主干逻辑依然保持纯净,完美接入了旧模型。
Java
public class Client {
public static void main(String[] args) {
// 1. 存在一个老旧的本地模型服务
LegacyLocalModelService legacyModel = new LegacyLocalModelService();
// 2. 将其套上适配器,转换成系统标准的 RAG 客户端
StandardRAGClient llmClient = new LegacyModelAdapter(legacyModel);
// 3. 客户端按标准接口规范发起调用,完全不知道底层是老旧服务
String response = llmClient.generateAnswer("如何排查主机排气温度过高?", "相关维护手册段落...");
System.out.println(response);
}
}
四、实战案例一:MQ消息适配
4.1 需求分析
随着公司业务发展,营销系统需要对接越来越多的 MQ 消息(注册开户、商品下单、第三方订单等),以及不同来源的服务接口,来发放奖励(裂变、首单返利等)。
假设系统原本只接入一种订单消息,后来陆续接入开户 MQ、内部订单 MQ、POP 订单 MQ。它们的字段名并不统一:
- 开户消息:
number - 内部订单消息:
uid - POP 订单消息:
uId
但发券业务真正需要的是统一的:
userIdbizIdbizTimedesc
如果业务代码里到处写 if-else 判断消息类型,就会越来越乱。每新增一种 MQ,主流程就要继续修改。
适配器模式的思路是:把不同消息先转换成统一模型 RebateInfo,主业务只处理 RebateInfo。
CreateAccout MQ
MQAdapter
OrderMq MQ
POPOrderDelivered MQ
RebateInfo
发券业务
适配器模式的价值:
- 定义统一的目标结构
- 通过适配器屏蔽各 MQ/接口的差异,上层业务代码只与统一接口打交道

- 图片来源:[重学 Java 设计模式:实战适配器模式「从多个MQ消息体中,抽取指定字段值场景」 | 小傅哥 bugstack 虫洞栈](https://bugstack.cn/md/develop/design-pattern/2020-06-02-重学 Java 设计模式《适配器模式》.html)
三种 MQ 消息的字段各不相同:
| MQ 来源 | 用户ID字段 | 业务ID字段 | 时间字段 |
|---|---|---|---|
create_account |
number |
number |
accountDate |
OrderMq |
uid |
orderId |
createOrderTime |
POPOrderDelivered |
uId |
orderId |
orderTime |
RebateInfo 定义了一套标准字段 ,所有 MQ 消息经过 MQAdapter 适配后,都转成这个统一格式:
| 字段 | 含义 | 对应原始字段 |
|---|---|---|
userId |
用户ID | 各 MQ 里叫法不同的用户字段 |
bizId |
业务单号 | 订单号 / 开户编号 |
bizTime |
业务时间 | 下单时间 / 开户时间 |
desc |
描述 | 业务描述 |
4.2 架构图
4.2.1 面条代码架构图

4.2.2 适配器模式架构图

4.3 时序图
4.3.1 面条代码时序图
POPOrderService OrderService 原始MQ对象 业务入口 POPOrderService OrderService 原始MQ对象 业务入口 alt [内部订单或开户] [POP订单] 读取不同字段 number / uid / uId 手动转换 userId queryUserOrderCount(userId) 订单数量 isFirstOrder(userId) 是否首单 判断是否发券
4.3.2 适配器模式类图
原始订单服务 OrderAdapterService RebateInfo MQAdapter 发券业务 原始订单服务 OrderAdapterService RebateInfo MQAdapter 发券业务 filter(message, link) 设置 userId / bizId / bizTime / desc RebateInfo isFirst(rebateInfo.userId) 调用原始服务方法 原始结果 boolean 发券
五、实战案例二:redis缓存集群适配
5.1 需求分析
在缓存集群适配案例中,项目最开始只有一套本地封装好的缓存工具 RedisUtils,业务通过统一的 ICacheService 使用缓存能力:
java
cacheService.set(key, value);
cacheService.get(key);
cacheService.del(key);
这种设计在只有一套 Redis 工具时没有问题。业务代码只关心"我要读缓存、写缓存、删除缓存",并不需要关心底层缓存工具是如何实现的。
但是随着系统发展,缓存能力不再只依赖原来的 RedisUtils,而是需要接入新的缓存集群组件,例如新加入的EGM和IIR组件,
这两个缓存集群组件本质上都能完成缓存读写,但它们对外暴露的方法名并不完全一致。
它们表达的是类似的缓存能力,但接口形式并不统一:
| 操作 | RedisUtils(原来) | EGM(新集群1) | IIR(新集群2) |
|---|---|---|---|
| 取值 | get() | gain() | get() |
| 写入 | set() | set() | set() |
| 带超时写入 | set(key,val,timeout,unit) | setEx() | setExpire() |
| 删除 | del() | delete() | del() |
这就产生了一个典型的接口不兼容问题:业务想要的是统一的缓存服务接口,但底层不同缓存组件提供的方法名称和调用方式并不一致。
如果不使用设计模式,最直接的做法就是在缓存服务实现类中增加 redisType 参数,然后通过 if 判断:
java
if (1 == redisType) {
return egm.gain(key);
}
if (2 == redisType) {
return iir.get(key);
}
return redisUtils.get(key);
这种写法短期能跑,但会把底层缓存差异暴露给业务层。
-
redisType 进入了业务接口。调用方不仅要知道
key、value,还要知道 1 代表EGM、2 代表IIR,业务代码被迫理解缓存集群选择规则。 -
if-else 会在多个方法中重复出现。get、set、带过期时间的
set、del都要判断一次,后续缓存操作越多,重复分支越多。 -
扩展成本高。新增一套缓存
SDK时,必须修改CacheClusterServiceImpl,继续增加分支逻辑,容易让实现类变成臃肿的分发器。
所以本案例真正要解决的问题是:
让业务继续面向统一缓存接口编程,把
EGM、IIR、RedisUtils的方法差异隔离到适配层。
适配器模式改造后:
EGMCacheAdapter负责适配EGM。IIRCacheAdapter负责适配IIR。JDKProxyFactory负责创建业务可用的ICacheService代理对象。
改造后的业务调用变成:
java
ICacheService proxyEGM = JDKProxyFactory.getProxy(ICacheService.class, EGMCacheAdapter.class);
proxyEGM.set("user_name_01", "like");
String value = proxyEGM.get("user_name_01");
此时业务不再传 redisType,也不需要知道 EGM.gain、IIR.setExpire 这些底层方法名。新增缓存集群时,只需要新增对应适配器即可。
5.2 架构图
5.2.1 面条代码架构图

5.2.2 适配器模式架构图

5.3 时序图
5.3.1 面条代码架构图
RedisUtils IIR EGM CacheClusterServiceImpl 业务代码 RedisUtils IIR EGM CacheClusterServiceImpl 业务代码 alt [redisType = 1] [redisType = 2] [default] get(key, redisType) gain(key) get(key) get(key)
5.3.2 适配器模式架构图
EGM / IIR SDK ICacheAdapter JDKInvocationHandler ICacheService代理对象 JDKProxyFactory 业务代码 EGM / IIR SDK ICacheAdapter JDKInvocationHandler ICacheService代理对象 JDKProxyFactory 业务代码 getProxy(ICacheService, EGMCacheAdapter) proxy set(key, value) invoke() set(key, value) set / setEx / delete / gain result result result result
总结
适配器模式的核心价值是隔离变化。
在 codedesign5.0-* 中,它隔离了不同 MQ 消息体和不同订单服务接口的差异;在 codedesign5.1-* 中,它隔离了不同缓存 SDK 的方法差异。
适配器模式适合用在这些场景:
- 外部系统字段和内部模型不一致。
- 第三方 SDK 方法名、参数、返回值和业务接口不一致。
- 老接口不能改,但新业务希望用统一接口。
- 系统中出现大量
if-else处理接口兼容问题。
最终效果是:
不兼容对象
适配器
统一接口
稳定业务代码
适配器模式不是为了让代码"看起来高级",而是为了让业务主流程少知道一点外部混乱,多保持稳定。