代理模式和适配器模式区别
代理模式 vs 适配器模式 完整详解+核心区别
一、先分别吃透两种模式本质
1. 适配器模式(Adapter,结构型)
核心目的:做「转换兼容」
两个已有接口不匹配,通过适配器做一层接口转换,让原本不能协作的两类代码一起工作。
- 关键词:兼容、翻译、转换、统一接口
- 角色:
- 目标接口 Target:我方程序期望调用的统一接口
- 被适配者 Adaptee:已有旧接口/第三方类,方法名、参数和我方不匹配
- 适配器 Adapter:包装 Adaptee,实现 Target,内部做参数、方法转发转换
- 类比:转换插头
插座是标准Target(国标三脚),国外电器是Adaptee(美标两脚),适配器插头做转换,让电器插上插座。
两种实现
- 类适配器:继承被适配类 + 实现目标接口(Java单继承很少用)
- 对象适配器:组合持有被适配对象(主流)
2. 代理模式(Proxy,结构型)
核心目的:做「控制增强」
用一个代理类包装原始目标对象,对外暴露完全一模一样的接口,不修改原有接口定义,在方法前后增加额外逻辑(权限、缓存、日志、延时创建、远程调用)。
- 关键词:控制、增强、拦截、中介、透明访问
- 角色:
- 抽象主题 Subject:原始对象与代理共同实现的统一接口
- 真实对象 RealSubject:业务核心对象
- 代理 Proxy:实现同一接口,持有真实对象,拦截所有方法
- 类比:中介/经纪人
明星(RealSubject)和经纪人(Proxy)对外联系方式完全一致(都能接演出),但经纪人会做前置筛选报价、后置签合同,明星只负责表演。
常见分类
- 静态代理:手写代理类
- 动态代理:JDK动态代理、CGLIB(Spring AOP底层就是动态代理)
- 远程代理、虚拟代理、保护代理、缓存代理
二、最核心本质差异(一句话区分)
- 适配器:接口不一样,做转换,解决「不兼容」
对外提供新接口,抹平新旧/第三方接口差异;使用者看不到原始类接口。 - 代理:接口完全一样,做增强,控制访问
对外和原始对象完全相同接口,使用者无感知,透明调用,只附加额外逻辑。
关键分水岭:接口是否改变
- 调用方期望的接口 ≠ 原始类接口 → 适配器
- 调用方期望的接口 = 原始类接口 → 代理
三、分层详细对比
1. 设计目标不同
适配器
打通两套不兼容接口,复用已有第三方/旧代码,不用修改原有代码。
场景:老系统接口、第三方SDK、不同组件整合。
代理
在不改动原对象代码前提下,对原有功能进行拦截、增强、管控。
场景:权限校验、日志、缓存、懒加载、远程RPC、事务、限流。
2. 对外暴露接口
适配器
适配器实现全新的目标接口Target,内部调用被适配者Adaptee的异构方法。
外部调用Adapter的是一套新方法名,和Adaptee方法名完全不同。
示例:
- Target:
pay(amount) - Adaptee第三方支付:
transferMoney(money, channel) - Adapter内部:
pay()里转换参数调用transferMoney()
代理
代理和真实对象实现同一个接口Subject,方法名、参数、返回值完全一致。
外部调用Proxy的方法,和直接调用RealSubject无任何区别。
示例:
- 统一接口:
queryOrder(Long id) - Proxy.queryOrder() 先查缓存,无缓存再调用 RealSubject.queryOrder()
3. 与被包装对象的关系
适配器
被包装的 Adaptee 是外部/老旧异构组件,本来就不适合当前系统调用规范,两者设计初衷无关。
适配器是翻译官。
代理
被包装的 RealSubject 是系统核心业务对象,本身完全满足业务功能,只是需要附加控制逻辑。
代理是保镖/中介。
4. 是否改变调用行为
适配器
核心逻辑委托给被适配者,适配器只做参数、格式转换,不新增额外横切逻辑。
没有前置校验、缓存、日志这类增强,仅做格式翻译。
代理
核心逻辑依然委托真实对象,但会增加前后附加逻辑:
前置:权限校验、参数校验、缓存查询
后置:日志记录、耗时统计、事务提交
5. 代码示例直观区分
示例1:适配器(接口不匹配,转换)
// 目标接口:系统统一支付标准publicinterfacePayment{voidpay(BigDecimalmoney);}// 第三方老支付SDK(接口不兼容)publicclassOldThirdPay{publicvoidtransfer(BigDecimalcash,Integertype){System.out.println("第三方转账:"+cash);}}// 适配器:统一转换成Payment接口publicclassPayAdapterimplementsPayment{privateOldThirdPayadaptee=newOldThirdPay();@Overridepublicvoidpay(BigDecimalmoney){// 转换参数,适配第三方接口adaptee.transfer(money,1);}}// 使用:业务只认Payment,完全不知道OldThirdPay存在Paymentpay=newPayAdapter();pay.pay(newBigDecimal(100));示例2:代理(接口完全一致,增加缓存增强)
// 统一抽象接口publicinterfaceOrderService{OrdergetOrder(Longid);}// 真实业务类publicclassOrderServiceImplimplementsOrderService{@OverridepublicOrdergetOrder(Longid){// 查询数据库returndb.selectById(id);}}// 缓存代理,接口完全一致publicclassOrderProxyimplementsOrderService{privateOrderServicereal=newOrderServiceImpl();privateMap<Long,Order>cache=newHashMap<>();@OverridepublicOrdergetOrder(Longid){// 前置增强:缓存拦截if(cache.containsKey(id)){returncache.get(id);}Orderorder=real.getOrder(id);// 后置增强:放入缓存cache.put(id,order);returnorder;}}// 使用方式和原对象完全一致,上层无感知OrderServiceservice=newOrderProxy();service.getOrder(1001L);6. 适用业务场景对比
适配器典型场景
- 整合第三方SDK(微信/支付宝老版本接口、OSS存储、第三方短信)
- 新旧系统兼容:老数据库DAO、遗留接口适配新业务框架
- 统一多类异构工具:多种日志组件、多种文件存储适配统一工具类
- 不同协议转换:HTTP接口适配内部RPC调用格式
代理典型场景
- 动态代理做AOP:日志、切面、事务、限流、权限校验(Spring核心)
- 缓存代理:数据库查询加本地缓存、Redis缓存
- 虚拟代理:大图片/大文件延迟加载(用到才创建真实对象)
- 保护代理:接口访问权限校验,未登录拦截调用
- 远程代理:RPC客户端代理,屏蔽网络通信细节(Dubbo/Feign)
- 静态代理:封装工具类增加统一异常处理
7. 优缺点与取舍
适配器优点
快速兼容异构代码,无需修改第三方/遗留类;统一对外调用标准。
缺点:仅做转换,无法统一增加横切增强逻辑。
代理优点
无侵入增强原有业务,上层调用零改动;动态代理可统一批量切面处理。
缺点:只能用于接口匹配的场景,不能解决接口不兼容问题。
四、表格汇总对比
| 对比维度 | 适配器模式(Adapter) | 代理模式(Proxy) |
|---|---|---|
| 核心职责 | 接口转换、兼容异构组件 | 透明拦截、功能增强、访问控制 |
| 对外接口 | 提供全新目标接口,与被包装对象接口不同 | 和原始对象完全相同接口,方法签名一致 |
| 设计关系 | 解决「接口不匹配」问题 | 不改变接口,附加额外逻辑 |
| 类比 | 转换插头、翻译官 | 经纪人、中介、保镖 |
| 包装对象 | 第三方/遗留异构类 | 系统原生标准业务对象 |
| 附加逻辑 | 仅参数/格式转换,无横切增强 | 缓存、日志、权限、事务、延时加载 |
| 上层感知 | 上层只依赖Target,看不到原始类 | 上层完全无感知,像直接调用原对象 |
| 典型框架应用 | SpringMVC HandlerAdapter、集合工具适配 | Spring AOP动态代理、Dubbo远程代理 |
五、极易混淆的边界场景区分技巧
场景1:如果我包装后,方法名变了 → 适配器
需要把sendMsg(String phone,String content)改成系统统一sendSms(SmsDTO dto),参数结构重组,一定是适配器。
场景2:包装后方法名、参数完全不变,只是加日志/缓存 → 代理
getUser(Long id)前后加打印耗时、缓存查询,接口一丝不动,一定是代理。
场景3:同时需要转换+增强怎么处理?
分层组合:外层代理做增强,内层适配器做转换
调用链路:Controller → Proxy(日志/权限)→ Adapter(接口转换)→ 第三方SDK
六、一句话记忆口诀
接口不一样,要兼容,用适配器;
接口一模一样,要增强,用代理。