设计模式 · 适配器模式 (Adapter Pattern)
设计模式 · 适配器模式 (Adapter Pattern)
前言
在软件工程的实际演进中,我们经常会面临一种进退两难的局面:
系统需要引入一个非常核心的现存组件或第三方库,但它的接口标准与我们当前系统的主流架构完全不兼容。
如果我们为了迎合这个外部组件而大规模修改核心代码,不仅会打破现有的稳定性,还会造成严重的逻辑污染。
适配器模式就是为了这种“亡羊补牢”或“新老交替”的场景而生的。
它就像是一个软件层面的“扩展坞”,优雅地在不兼容的接口之间建立起一座桥梁,让系统能够无缝地复用既有资产。
本文参考博客:
本文代码链接: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 完全无感知。
classDiagram
direction LR
class Client {
+request()
}
class Target {
<<interface>>
+request()
}
class Adapter {
-Adaptee adaptee
+request()
}
class Adaptee {
+specificRequest()
}
Client --> Target : 面向目标接口调用
Target <|.. Adapter : 实现统一接口
Adapter --> Adaptee : 转调原有能力
三、 场景推演
假设我们正在构建一个用于特定垂直领域的问答系统,系统底层基于检索增强生成(RAG)架构。
在系统初期,我们定义了一个统一的现代化大模型接口标准。
但现在,我们需要接入一个早年开发、专门针对某些特定维护手册微调过的老旧本地化模型服务。
1. 定义 Target(目标接口)
这是我们系统内部统一的调用标准,客户端都基于此接口开发。
1 | // Target: 现代 RAG 系统的统一大模型接口 |
2. 引入 Adaptee(被适配者)
这是一个老旧的模型服务,它的方法签名完全不一样,甚至要求将所有输入打包成一个特定的 JSON 字符串。
1 | // Adaptee: 外部老旧的模型服务,接口不兼容 |
3. 构建 Adapter(适配器)
我们创建一个适配器,实现目标接口,并在内部包装老旧服务,完成输入输出的“翻译”。
1 | // Adapter: 适配器类,通过组合引入 Adaptee |
4. 客户端调用
系统的主干逻辑依然保持纯净,完美接入了旧模型。
1 | public class Client { |
四、实战案例一:MQ消息适配
4.1 需求分析
随着公司业务发展,营销系统需要对接越来越多的 MQ 消息(注册开户、商品下单、第三方订单等),以及不同来源的服务接口,来发放奖励(裂变、首单返利等)。
假设系统原本只接入一种订单消息,后来陆续接入开户 MQ、内部订单 MQ、POP 订单 MQ。它们的字段名并不统一:
- 开户消息:
number - 内部订单消息:
uid - POP 订单消息:
uId
但发券业务真正需要的是统一的:
userIdbizIdbizTimedesc
如果业务代码里到处写 if-else 判断消息类型,就会越来越乱。每新增一种 MQ,主流程就要继续修改。
适配器模式的思路是:把不同消息先转换成统一模型 RebateInfo,主业务只处理 RebateInfo。
flowchart LR
A[CreateAccout MQ] --> D[MQAdapter]
B[OrderMq MQ] --> D
C[POPOrderDelivered MQ] --> D
D --> E[RebateInfo]
E --> F[发券业务]
适配器模式的价值:
- 定义统一的目标结构
- 通过适配器屏蔽各 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 面条代码时序图
sequenceDiagram
participant Client as 业务入口
participant MQ as 原始MQ对象
participant OS as OrderService
participant POS as POPOrderService
Client->>MQ: 读取不同字段 number / uid / uId
Client->>Client: 手动转换 userId
alt 内部订单或开户
Client->>OS: queryUserOrderCount(userId)
OS-->>Client: 订单数量
else POP订单
Client->>POS: isFirstOrder(userId)
POS-->>Client: 是否首单
end
Client->>Client: 判断是否发券
4.3.2 适配器模式类图
sequenceDiagram
participant Client as 发券业务
participant MQA as MQAdapter
participant RI as RebateInfo
participant OA as OrderAdapterService
participant S as 原始订单服务
Client->>MQA: filter(message, link)
MQA->>RI: 设置 userId / bizId / bizTime / desc
MQA-->>Client: RebateInfo
Client->>OA: isFirst(rebateInfo.userId)
OA->>S: 调用原始服务方法
S-->>OA: 原始结果
OA-->>Client: boolean
Client->>Client: 发券
五、实战案例二:redis缓存集群适配
5.1 需求分析
在缓存集群适配案例中,项目最开始只有一套本地封装好的缓存工具 RedisUtils,业务通过统一的 ICacheService 使用缓存能力:
1 | cacheService.set(key, value); |
这种设计在只有一套 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 判断:
1 | if (1 == redisType) { |
这种写法短期能跑,但会把底层缓存差异暴露给业务层。
redisType 进入了业务接口。调用方不仅要知道
key、value,还要知道 1 代表EGM、2 代表IIR,业务代码被迫理解缓存集群选择规则。if-else 会在多个方法中重复出现。get、set、带过期时间的
set、del都要判断一次,后续缓存操作越多,重复分支越多。扩展成本高。新增一套缓存
SDK时,必须修改CacheClusterServiceImpl,继续增加分支逻辑,容易让实现类变成臃肿的分发器。
所以本案例真正要解决的问题是:
让业务继续面向统一缓存接口编程,把
EGM、IIR、RedisUtils的方法差异隔离到适配层。
适配器模式改造后:
EGMCacheAdapter负责适配EGM。IIRCacheAdapter负责适配IIR。JDKProxyFactory负责创建业务可用的ICacheService代理对象。
改造后的业务调用变成:
1 | ICacheService proxyEGM = JDKProxyFactory.getProxy(ICacheService.class, EGMCacheAdapter.class); |
此时业务不再传 redisType,也不需要知道 EGM.gain、IIR.setExpire 这些底层方法名。新增缓存集群时,只需要新增对应适配器即可。
5.2 架构图
5.2.1 面条代码架构图

5.2.2 适配器模式架构图

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