java设计模式 · 适配器模式 (Adapter Pattern)

文章目录

    • 前言
    • [一、 核心定义](#一、 核心定义)
    • [二、 标准体系结构图](#二、 标准体系结构图)
    • [三、 场景推演](#三、 场景推演)
    • 四、实战案例一: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 生态中,由于遵循"组合优于继承"的原则,我们绝大多数情况下采用的是对象适配器,因为它更加灵活,且能突破单继承的限制。

二、 标准体系结构图

在适配器模式的标准体系中,通常包含以下四个关键角色:

  1. Target(目标接口): 当前系统业务所期待的统一标准接口。客户端只认这个接口。
  2. Adaptee(被适配者): 已经存在的、包含核心逻辑但接口与 Target 不兼容的类或第三方组件。
  3. Adapter(适配器): 模式的核心枢纽。它实现了 Target 接口,并在内部持有一个 Adaptee 的实例。它负责接收客户端的请求,并将其"翻译"成 Adaptee 能够理解的特定方法调用。
  4. 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

但发券业务真正需要的是统一的:

  • userId
  • bizId
  • bizTime
  • desc

如果业务代码里到处写 if-else 判断消息类型,就会越来越乱。每新增一种 MQ,主流程就要继续修改。

适配器模式的思路是:把不同消息先转换成统一模型 RebateInfo,主业务只处理 RebateInfo
CreateAccout MQ
MQAdapter
OrderMq MQ
POPOrderDelivered MQ
RebateInfo
发券业务

适配器模式的价值:

  • 定义统一的目标结构
  • 通过适配器屏蔽各 MQ/接口的差异,上层业务代码只与统一接口打交道

三种 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,而是需要接入新的缓存集群组件,例如新加入的EGMIIR组件,

这两个缓存集群组件本质上都能完成缓存读写,但它们对外暴露的方法名并不完全一致。

它们表达的是类似的缓存能力,但接口形式并不统一:

操作 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);

这种写法短期能跑,但会把底层缓存差异暴露给业务层。

  1. redisType 进入了业务接口。调用方不仅要知道 keyvalue,还要知道 1 代表 EGM、2 代表 IIR,业务代码被迫理解缓存集群选择规则。

  2. if-else 会在多个方法中重复出现。get、set、带过期时间的 setdel 都要判断一次,后续缓存操作越多,重复分支越多。

  3. 扩展成本高。新增一套缓存 SDK 时,必须修改 CacheClusterServiceImpl,继续增加分支逻辑,容易让实现类变成臃肿的分发器。

所以本案例真正要解决的问题是:

让业务继续面向统一缓存接口编程,把 EGMIIRRedisUtils 的方法差异隔离到适配层。

适配器模式改造后:

  • 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.gainIIR.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 处理接口兼容问题。

最终效果是:
不兼容对象
适配器
统一接口
稳定业务代码

适配器模式不是为了让代码"看起来高级",而是为了让业务主流程少知道一点外部混乱,多保持稳定。

相关推荐
snakeshe10101 小时前
SpringBoot 多人协作平台实战(6):SpringBoot Controller 入门与登录模块开发
java
用户298698530141 小时前
用 Java 操作 Word 文档?试试添加内容控件
java·后端
带刺的坐椅1 小时前
Java AI 框架三国杀:Solon AI vs Spring AI vs LangChain4j 深度对比
java·ai·langchain4j·spring-ai·solon-ai
苍煜1 小时前
K8s 集群快速搭建(系列第八篇:单机/多节点集群实战)
java·容器·kubernetes
Chase_______1 小时前
Java 基础语言 ① —— Java 运行机制与开发环境:从 javac 到 JVM 全流程解析
java·jvm·python
北风toto1 小时前
在 Axios 中发送 POST 请求并携带参数通常有以下两种方式
java
cui_ruicheng1 小时前
Linux线程(二):pthread 线程库与线程控制
java·开发语言·jvm
山北雨夜漫步2 小时前
LangGraph
java·前端·算法
jakeswang2 小时前
【AI面经】大模型半夜发短信骂客户?Agent 工具调用失控,你如何设计防护机制?
java·后端