GoF设计模式——适配器模式

本文是【GoF设计模式】系列第6篇,更多内容欢迎关注公众号:咖啡八杯

前言

为什么需要适配器模式?

写业务代码时经常碰到这种情况:项目里已经定义好了一个接口 PaymentGateway,所有支付都走它的 pay(orderId, amount) 方法;今天产品说要接入微信支付,打开 SDK 一看------微信用的是 unifiedOrder(body, outTradeNo, totalFee, ip),参数和方法名跟系统的接口对不上。

arduino 复制代码
// 系统希望的调用方式
paymentGateway.pay("ORD001", new BigDecimal("99.9"));

// 但微信 SDK 实际上长这样
wxPayApi.unifiedOrder("商品", "ORD001", 9990, "127.0.0.1"); // 单位是分

两种选择:把订单模块全部改成直接调微信 SDK,或者改微信 SDK 让它实现 PaymentGateway------前者一旦切换支付渠道就要重写业务代码,后者根本动不了第三方 SDK 的源码。

这种"现有的类"和"需要的接口"对不上的矛盾,就是适配器模式要解决的问题。

概念

适配器模式(Adapter Pattern)是一种结构型 设计模式,核心思想是将一个类的接口转换成客户端期望的另一个接口,让原本不兼容的类能够协同工作。

适配器模式包含三个角色:

  • Target(目标接口) :客户端期望使用的接口
  • Adapter(适配器) :实现目标接口,内部持有被适配者,把目标接口的调用转换为被适配者的调用
  • Adaptee(被适配者) :已有的类,其接口与目标接口不兼容,但功能正好是需要的
classDiagram direction BT class Target { <> +request() } class Adapter { -adaptee: Adaptee +request() } class Adaptee { +specificRequest() } class Client { } Adapter ..|> Target : 实现 Adapter o--> Adaptee : 持有 Client --> Target : 依赖

Adapter 实现 Target 接口并持有 Adaptee 实例,Client 只依赖 Target,完全不感知 Adaptee 的存在。Adapter 内部把 request() 翻译成 specificRequest(),参数和返回值的差异都在适配器里处理。

可以把适配器理解为电源转换头:新装的墙面插座只有两孔(系统期望的接口),家里的老电器是三脚插头(已有的、改不了的类),中间塞一个两脚转三脚的转换头------插座只看到两脚,老电器照样能用,"翻译"工作全在转换头内部完成。这个比喻贯穿后面的实现章节,方便对照理解。

实现

适配器模式有两种实现方式:对象适配器 (组合)和类适配器(继承)。前者是 GoF 推荐方式,也是实际开发中最常用的;后者由于 Java 单继承限制,使用场景有限。

对象适配器

对象适配器通过组合实现:适配器实现目标接口,内部持有被适配者的引用,将调用委托给被适配者。

java 复制代码
// 目标接口
public interface Target {
    public void request();
}

// 被适配者
public class Adaptee {
    public void specificRequest() {
        System.out.println("Adaptee 的特定请求");
    }
}

// 对象适配器:组合持有被适配者
public class Adapter implements Target {
    private Adaptee adaptee;

    public Adapter(Adaptee adaptee) {
        this.adaptee = adaptee;
    }

    @Override
    public void request() {
        adaptee.specificRequest();
    }
}

// 客户端
Target target = new Adapter(new Adaptee());
target.request();

引入一个例子:「家里的两孔插座,要给一台三脚插头的老电器供电,买一个两脚转三脚的电源转换头,一头插进插座、一头接上老电器------老电器就能正常工作了。换一台老电器(吹风机、电烙铁)?拔下来插另一台,转换头本身不用动」。

插座对应 Target(系统期望的接口),老电器对应 Adaptee(已有的、改不了的类),转换头对应 Adapter------它实现插座的两脚接口、内部握着老电器的引用,"插上"对应构造方法注入的组合关系。

java 复制代码
// 目标接口:新插座只认两脚插头
public interface TwoPinSocket {
    public void powerOn();
}

// 被适配者:老电器自带三脚插头
public class ThreePinAppliance {
    public void plugInThreePin() {
        System.out.println("老电器:三脚插头通电,开始工作");
    }
}

// 适配器:两脚转三脚转换头
public class ThreeToTwoAdapter implements TwoPinSocket {
    private ThreePinAppliance appliance; // 持有老电器

    public ThreeToTwoAdapter(ThreePinAppliance appliance) {
        this.appliance = appliance;
    }

    @Override
    public void powerOn() {
        // 转换头内部把"两脚通电"翻译成"三脚通电"
        appliance.plugInThreePin();
    }
}

// 使用:插座只认 TwoPinSocket 接口
TwoPinSocket socket = new ThreeToTwoAdapter(new ThreePinAppliance());
socket.powerOn();

组合方式最大的好处是灵活:换一个老电器(被适配者)只需 new 一个新的转换头实例传进去,适配器自身的代码完全不动;单元测试时也方便传 Mock 对象进去。

类适配器

类适配器通过继承实现:适配器同时继承被适配者并实现目标接口。Java 单继承的限制下,被适配者必须是类。

java 复制代码
// 目标接口
public interface Target {
    public void request();
}

// 被适配者
public class Adaptee {
    public void specificRequest() {
        System.out.println("Adaptee 的特定请求");
    }
}

// 类适配器:继承被适配者,实现目标接口
public class Adapter extends Adaptee implements Target {
    @Override
    public void request() {
        specificRequest(); // 直接调用继承来的方法
    }
}

// 客户端
Target target = new Adapter();
target.request();

引入一个例子:「同样是要给老电器接两孔插座,这次厂家干脆出了款改造型号 :把老电器自带的三脚插头直接焊掉、换成两脚------电器和"转换头"合体成一台机器,对外就是两脚插头,不再需要外挂任何东西」。

合体机器既"是"老电器又"是"两脚设备------通过 extends 继承被适配者拿到原有功能,通过 implements 实现目标接口对外暴露新接口,"焊死"对应编译期就绑定的继承关系。

java 复制代码
// 目标接口
public interface TwoPinSocket {
    public void powerOn();
}

// 被适配者:老电器
public class ThreePinAppliance {
    public void plugInThreePin() {
        System.out.println("老电器:三脚插头通电,开始工作");
    }
}

// 类适配器:继承老电器,同时实现两脚插座接口
public class FusedAppliance extends ThreePinAppliance implements TwoPinSocket {
    @Override
    public void powerOn() {
        // 继承自父类,直接调用
        plugInThreePin();
    }
}

// 使用
TwoPinSocket socket = new FusedAppliance();
socket.powerOn();

类适配器的优势是可以重写被适配者的方法(比如修改老电器某个功能的行为),缺点是与被适配者强耦合,一旦被适配者是接口或者需要适配多个不同的实现就用不了。

如何选择

两种实现方式的对比:

维度 对象适配器(组合) 类适配器(继承)
推荐程度 首选 仅特殊场景
耦合度 低,依赖接口 高,依赖具体类
灵活性 运行时可替换被适配者 编译期固定
可测试性 容易 Mock 难以 Mock
被适配者要求 类或接口均可 必须是类
是否能重写 Adaptee 方法 不能

记忆口诀组合优于继承,对象适配器是首选。只有在确实需要重写被适配者方法、且被适配者是类的场景才考虑类适配器。

总结

适配器模式本质上是一层"翻译器"------已有的类和系统期望的接口对不上,又不能改任何一边,就在中间加一层适配器做转换。

什么时候用

  • 想复用一个已有类,但它的接口和系统期望的不一样
  • 引入第三方库或遗留代码,需要统一调用方式
  • 系统中有多个功能相似但接口不同的类,需要统一接口
  • 新旧系统升级过渡期,需要让旧接口能在新框架下继续工作

什么时候不用

  • 接口差异巨大(方法数量、参数类型完全不同),适配器会变得臃肿,不如重新设计
  • 能修改被适配者源码且代价不大,直接修改更简单
  • 系统设计阶段就能定义接口规范,从源头统一即可,不需要适配

简单记忆

适配器解决"接口不兼容"的问题,是给已有类"穿件新外套"。能改源码就改,改不了才用适配器。

适配器 vs 装饰器 vs 代理 vs 外观:四个结构型模式都"包了一层对象",结构相似但意图不同:

模式 接口关系 核心意图
适配器 目标接口 ≠ 被包装对象接口 转换接口,让不兼容的类协同
装饰器 目标接口 = 被包装对象接口 增强功能,接口不变
代理 目标接口 = 被包装对象接口 控制访问,附加访问前后逻辑
外观 目标接口是新设计的 简化复杂子系统的调用

口诀对比:适配改接口,装饰增功能,代理控访问,外观简调用。

练习题目

万能遥控器

题目描述 :小明的万能遥控器通过统一的 Switchable 接口控制家电,该接口定义了:

  • turnOn():开启设备
  • turnOff():关闭设备

家中的智能灯 SmartLight 直接实现了 Switchable 接口。但还有一台旧风扇 OldFan,它只提供了 setSpeed(int speed) 方法(0=关闭,1=低速,2=中速,3=高速),无法直接被遥控器调用。

请使用适配器模式实现 OldFanAdapter,让旧风扇也能通过 Switchable 接口控制。适配规则:

  • turnOn() → 调用 setSpeed(1)(默认低速档开启)
  • turnOff() → 调用 setSpeed(0)(关闭)

输入描述:第一行输入整数 N(1 ≤ N ≤ 20),表示操作次数。接下来 N 行,每行包含设备名称和操作,用空格分隔:

  • 设备名称:Light(智能灯)或 Fan(旧风扇)
  • 操作:onoff

输出描述:对每条操作,输出对应设备响应信息:

  • 智能灯开启:Light is on
  • 智能灯关闭:Light is off
  • 旧风扇开启(经适配器):Old Fan speed set to 1
  • 旧风扇关闭(经适配器):Old Fan speed set to 0

输入示例

vbnet 复制代码
4
Light on
Fan on
Fan off
Light off

输出示例

vbnet 复制代码
Light is on
Old Fan speed set to 1
Old Fan speed set to 0
Light is off

解题思路 :题目中遥控器代码必须按统一的 Switchable 接口调用,但旧风扇只有 setSpeed 方法,接口完全不兼容。如果不用适配器,遥控器内部就要写 if (设备 instanceof OldFan) 这样的分支判断,每加一类老设备就要改遥控器代码,违反开闭原则。

使用对象适配器:让 OldFanAdapter 实现 Switchable 接口,内部持有 OldFan 实例,把 turnOn/turnOff 翻译成对应的 setSpeed 调用。遥控器只面向 Switchable 接口编程,新增设备只需新增适配器,遥控器代码零改动。

java 复制代码
import java.util.*;

public class Main {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        int n = sc.nextInt();
        while (n-- > 0) {
            String name = sc.next();
            String ope = sc.next();
            // 根据设备名称构造对应的 Switchable 实现
            Switchable sw;
            if ("Light".equals(name)) {
                sw = new SmartLight();
            } else {
                // 旧风扇通过适配器包装后变成 Switchable
                sw = new OldFanAdapter(new OldFan());
            }
            if ("on".equals(ope)) {
                sw.turnOn();
            } else {
                sw.turnOff();
            }
        }
    }
}

// 目标接口
interface Switchable {
    public void turnOn();
    public void turnOff();
}

// 已经实现目标接口的设备,直接用
class SmartLight implements Switchable {
    public void turnOn() {
        System.out.println("Light is on");
    }
    public void turnOff() {
        System.out.println("Light is off");
    }
}

// 被适配者:接口与 Switchable 不兼容
class OldFan {
    public void setSpeed(int speed) {
        System.out.println("Old Fan speed set to " + speed);
    }
}

// 适配器:实现目标接口,持有被适配者
class OldFanAdapter implements Switchable {
    private OldFan oldFan;

    public OldFanAdapter(OldFan oldFan) {
        this.oldFan = oldFan;
    }

    public void turnOn() {
        oldFan.setSpeed(1); // 翻译为低速档启动
    }

    public void turnOff() {
        oldFan.setSpeed(0); // 翻译为速度 0 关闭
    }
}

扩展:实际项目中的适配器模式

Java I/O 字节流转字符流

InputStreamReader 是 JDK 内置的典型适配器------目标接口是字符流 Reader,被适配者是字节流 InputStream。每次用 BufferedReader 包装文件读取时背后就在用适配器:

java 复制代码
FileInputStream fis = new FileInputStream("data.txt"); // 字节流
InputStreamReader reader = new InputStreamReader(fis, "UTF-8"); // 适配为字符流

适配器内部通过 StreamDecoder 按指定字符集把字节解码成字符,业务代码只面向 Reader 接口,不关心底层是文件、网络还是内存。

SLF4J 桥接各类日志框架

老项目里同时存在用 Log4j、JUL、JCL 的库,每家日志框架 API 互不兼容。SLF4J 通过 log4j-over-slf4jjul-to-slf4jjcl-over-slf4j 等桥接包,把各种日志框架的 API 适配到 SLF4J 接口上,所有日志最终走统一出口。这正是适配器模式"统一多个相似实现"的典型应用------业务代码只写 LoggerFactory.getLogger(),底层无论是 Logback 还是 Log4j2 都透明。

Spring MVC 的 HandlerAdapter

Spring MVC 中,Controller 可以是 @Controller 注解类、HttpRequestHandlerServlet、甚至 WebFlux 的函数式端点------它们处理请求的方法签名完全不同。DispatcherServlet 不可能用 if-else 判断各种类型,而是通过 HandlerAdapter 接口的不同实现(RequestMappingHandlerAdapterHttpRequestHandlerAdapter 等)把各种 Handler 适配为统一的 handle(request, response, handler) 调用。新增一种 Controller 类型只需新增一个 Adapter,分发逻辑完全不动。

第三方支付/短信 SDK 接入

接入微信、支付宝、银联三家支付时,三方 SDK 的方法签名、参数单位、返回码各不相同------微信金额单位是分、支付宝是字符串、银联还要 RSA 加签。在系统内定义统一的 PaymentGateway 接口,每家 SDK 写一个 WxPayAdapter / AliPayAdapter / UnionPayAdapter,订单模块只调统一接口,切换支付渠道时业务代码零改动。短信服务商(阿里云/腾讯云)的接入也是同样的套路。

ORM 框架的方言适配

MyBatis-Plus 的 IDialect、JPA Hibernate 的 Dialect 都是数据库方言适配器。MySQL 分页用 LIMIT,Oracle 用 ROWNUM,PostgreSQL 用 LIMIT OFFSET------业务层只调用 pagedQuery(),底层根据当前数据源选用对应的方言适配器拼 SQL。换数据库时只需换方言实现,DAO 代码不动。

现在可能还在写简单的 CRUD,但等遇到要接入新支付渠道、新短信服务商,或者升级旧框架时------与其每次都改业务代码,不如在中间塞一个适配器,让业务代码永远只面向自己的接口编程。这就是适配器模式真正的价值。

技术交流 & 更多原创内容,关注公众号:咖啡八杯

相关推荐
武子康23 分钟前
Java-07 深入浅出 MyBatis数据库一对多关系模型实战:表结构设计与查询实现
java·后端
花椒技术1 小时前
企业内部 Agent 落地复盘:Gateway、Skill 和二次确认如何串起受控业务执行
后端·agent·ai编程
Agent手记2 小时前
制造业生产流程自动化,Agent需要具备哪些能力?深度拆解2026工业级智能体落地范式与核心架构
大数据·人工智能·ai·架构·自动化
Yunzenn3 小时前
深度分析字节最新研究cola-DLM 第 07 章:推理流水线逐行拆解 —— 从 prompt 到生成文本
人工智能·驱动开发·深度学习·chatgpt·架构·prompt·github
我是一颗柠檬3 小时前
【MySQL全面教学】MySQL事务与ACID Day9(2026年)
数据库·后端·mysql
枕星而眠3 小时前
数据结构八大排序详解(一):四大简单排序
c语言·数据结构·c++·后端
IT_陈寒3 小时前
React useEffect闭包陷阱差点把我整失业了
前端·人工智能·后端
苍何4 小时前
爆肝两周,我把 Codex 最全实战指南开源了
后端
颖火虫盟主4 小时前
Linux 系统分层架构:从硬件通电到 systemd 进程管理
linux·运维·架构
bug菌4 小时前
【SpringBoot 3.x 第254节】夯爆了,数据库访问性能优化实战详解!
数据库·spring boot·后端