【软考设计模式】适配器模式:接口兼容的“翻译官“与代码填空精讲
系列定位:软考软件设计师 / 系统架构设计师 — 结构型模式专题第 1 讲
考察分值:上午题 1-2 分,下午题常作为代码填空或类图识别出现
难度等级:⭐⭐⭐☆☆(概念直观,但类适配器 vs 对象适配器的区分是高频失分点)
一、考纲定位与模式定义
1.1 考纲要求
适配器模式在软考中属于结构型模式的基础内容。考察形式包括:
上午选择题:判断场景描述所属模式;识别类适配器(继承)与对象适配器(组合/聚合)的实现差异;判断适配器模式与桥接模式、装饰模式的区别
下午设计题:补全适配器类的适配逻辑(将目标接口方法委托给被适配者的方法);补全类适配器中的继承关系或对象适配器中的组合关系;识别类图角色(Target、Adapter、Adaptee)
1.2 模式定义
适配器模式:将一个类的接口转换成客户希望的另外一个接口。适配器模式使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。
核心意图:当现有类(Adaptee)的接口与客户端期望的接口(Target)不一致时,不修改原有代码,而是通过一个"适配器"(Adapter)来做接口转换,让原本不兼容的接口能够协同工作。就像出国旅行时带的电源转换插头 —— 不改变你的电器,也不改变当地的插座,只加一个中间转换层。
二、UML 类图与角色划分
2.1 对象适配器(组合/聚合,更常用)
┌─────────────────────────────────────────────────────────┐ │ Client │ │ + main() │ │ ├── Target target = new Adapter(adaptee); │ │ └── target.request(); // 调用的是适配后的方法 │ └───────────────────────┬─────────────────────────────────┘ │ uses ▼ ┌─────────────────────────────────────────────────────────┐ │ <<interface>> │ │ Target │ │ + request() │ └───────────────────────┬─────────────────────────────────┘ △ implements │ ▼ ┌─────────────────────────────────────────────────────────┐ │ Adapter │ │ - adaptee: Adaptee ◄── 组合/聚合关系 │ │ + request() { │ │ adaptee.specificRequest(); // 委托调用 │ │ } │ └─────────────────────────────────────────────────────────┘ │ uses ▼ ┌─────────────────────────────────────────────────────────┐ │ Adaptee │ │ + specificRequest() │ │ // 已有的接口,但与 Target 不兼容 │ └─────────────────────────────────────────────────────────┘2.2 类适配器(继承,较少用)
┌─────────────────────────────────────────────────────────┐ │ Client │ │ + main() │ │ ├── Target target = new Adapter(); │ │ └── target.request(); │ └───────────────────────┬─────────────────────────────────┘ │ uses ▼ ┌─────────────────────────────────────────────────────────┐ │ <<interface>> │ │ Target │ │ + request() │ └───────────────────────┬─────────────────────────────────┘ △ implements │ ▼ ┌─────────────────────────────────────────────────────────┐ │ Adapter │ │ ── extends ──▶ Adaptee ◄── 继承关系 │ │ + request() { │ │ specificRequest(); // 直接调用父类方法 │ │ } │ └─────────────────────────────────────────────────────────┘ △ │ extends ▼ ┌─────────────────────────────────────────────────────────┐ │ Adaptee │ │ + specificRequest() │ └─────────────────────────────────────────────────────────┘| 角色 | 职责 | 软考填空关键词 |
|---|---|---|
| Target(目标接口) | 客户端期望的接口,定义客户端需要调用的方法 | interface+request() |
| Adaptee(被适配者) | 已经存在的、功能正确但接口不兼容的类 | 现有类,方法名与 Target 不同 |
| Adapter(适配器) | 将 Adaptee 的接口转换为 Target 的接口 | implements Target+ 组合/继承 Adaptee |
| Client(客户端) | 面向 Target 接口编程,通过 Adapter 间接使用 Adaptee | Target target = new Adapter(...) |
类图识别要点:软考类图中,适配器模式一定有Target 接口、Adaptee 现有类、Adapter 转换类三个角色。关键看 Adapter 与 Adaptee 的关系:如果是实线空心菱形(组合/聚合)→ 对象适配器;如果是实线空心三角(继承)→ 类适配器。
三、场景一:对象适配器 —— 第三方库接口适配(最常用)
业务背景:系统已经定义了统一的日志接口Logger(Target),要求所有日志组件都实现log(String msg)方法。现在引入了一个第三方日志库ThirdPartyLogger(Adaptee),它提供的方法是writeLog(String content),接口不兼容。不能修改第三方库的源码,也不能修改系统已有的Logger接口。使用对象适配器解决。
说明:对象适配器通过组合/聚合持有 Adaptee 的引用,在 Adapter 内部将 Target 的方法调用转换为 Adaptee 的方法调用。这是实际开发中最常用的方式,因为 Java 不支持多继承,类适配器只能继承一个类,而对象适配器可以适配多个 Adaptee。
3.1 代码实现
// Target:系统统一的日志接口 public interface Logger { void log(String message); } // Adaptee:第三方日志库(不可修改源码) public class ThirdPartyLogger { public void writeLog(String content) { System.out.println("[第三方日志] " + content); } } // Adapter:对象适配器(组合 Adaptee) public class ThirdPartyLoggerAdapter implements Logger { private ThirdPartyLogger adaptee; public ThirdPartyLoggerAdapter(ThirdPartyLogger adaptee) { this.adaptee = adaptee; } @Override public void log(String message) { adaptee.writeLog(message); } } // Client:客户端面向 Target 接口编程 public class Client { public static void main(String[] args) { Logger logger = new ThirdPartyLoggerAdapter(new ThirdPartyLogger()); logger.log("系统启动成功"); } }关键体会:客户端完全不知道ThirdPartyLogger的存在,它只面向Logger接口编程。Adapter 像 "翻译官" 一样,把log()"翻译" 成writeLog()。第三方库的源码没有被修改,系统的Logger接口也没有被修改,双方通过 Adapter 和解。
四、场景二:类适配器 —— 遗留系统接口兼容(继承方式)
业务背景:系统有一个遗留的LegacyPrinter类(Adaptee),提供printOld(String text)方法。新系统定义了Printer接口(Target),要求实现print(String document)方法。由于LegacyPrinter是类而不是接口,且新系统需要直接复用LegacyPrinter的部分内部逻辑,使用类适配器通过继承来适配。
说明:类适配器通过继承Adaptee 类来实现适配。优点是可以直接访问 Adaptee 的 protected 方法,缺点是受限于单继承(Java 中 Adapter 只能继承一个 Adaptee 类)。软考中如果考到类适配器,通常会在题干中明确说明 "通过继承实现"。
4.1 代码实现
// Target:新系统统一的打印机接口 public interface Printer { void print(String document); } // Adaptee:遗留打印机类(不可修改源码) public class LegacyPrinter { public void printOld(String text) { System.out.println("[遗留打印机] 正在打印: " + text); } protected void warmUp() { System.out.println("[遗留打印机] 预热中..."); } } // Adapter:类适配器(继承 Adaptee) public class LegacyPrinterAdapter extends LegacyPrinter implements Printer { @Override public void print(String document) { warmUp(); printOld(document); } } // Client:客户端面向 Target 接口编程 public class Client { public static void main(String[] args) { Printer printer = new LegacyPrinterAdapter(); printer.print("2024年年度报告.pdf"); } }关键体会:类适配器通过继承获得了 Adaptee 的所有非 private 方法,可以直接调用warmUp()等 protected 方法。这是类适配器相对于对象适配器的优势。但代价是失去了灵活性 —— 如果以后需要适配另一个品牌的遗留打印机,必须再写一个 Adapter 类。
五、场景三:双向适配器 —— 两个不兼容系统的互操作(高级场景)
业务背景:系统 A 使用SystemAInterface接口,系统 B 使用SystemBInterface接口,两个系统需要互相调用对方的功能,但接口完全不兼容。使用双向适配器让两个系统可以互相适配。
说明:双向适配器是适配器模式的一种扩展,一个 Adapter 同时实现两个接口,内部持有两个系统的引用,实现双向转换。软考中此场景出现频率较低,但上午选择题可能作为 "适配器模式变体" 出现。
5.1 代码实现
// 系统 A 的接口 public interface SystemAInterface { void methodA(); } // 系统 B 的接口 public interface SystemBInterface { void methodB(); } // 系统 A 的具体实现 public class SystemA implements SystemAInterface { @Override public void methodA() { System.out.println("系统 A 执行 methodA"); } } // 系统 B 的具体实现 public class SystemB implements SystemBInterface { @Override public void methodB() { System.out.println("系统 B 执行 methodB"); } } // 双向适配器 public class TwoWayAdapter implements SystemAInterface, SystemBInterface { private SystemAInterface systemA; private SystemBInterface systemB; public TwoWayAdapter(SystemAInterface systemA, SystemBInterface systemB) { this.systemA = systemA; this.systemB = systemB; } @Override public void methodA() { if (systemA != null) { systemA.methodA(); } else { System.out.println("通过适配器,用系统 B 的能力模拟 methodA"); systemB.methodB(); } } @Override public void methodB() { if (systemB != null) { systemB.methodB(); } else { System.out.println("通过适配器,用系统 A 的能力模拟 methodB"); systemA.methodA(); } } } // Client public class Client { public static void main(String[] args) { SystemA systemA = new SystemA(); SystemB systemB = new SystemB(); TwoWayAdapter adapter = new TwoWayAdapter(systemA, systemB); SystemAInterface aClient = adapter; aClient.methodA(); SystemBInterface bClient = adapter; bClient.methodB(); } }六、三种场景对比与演进思路
| 维度 | 场景一:对象适配器 | 场景二:类适配器 | 场景三:双向适配器 |
|---|---|---|---|
| Adapter 与 Adaptee 关系 | 组合/聚合(持有引用) | 继承(extends) | 组合(持有两个系统引用) |
| Java 支持度 | ✅ 完全支持 | ⚠️ 单继承限制 | ✅ 完全支持 |
| Adaptee 数量 | 可适配多个 Adaptee | 只能适配一个 Adaptee | 可适配多个 |
| 访问 Adaptee 内部 | 只能访问 public 方法 | 可访问 protected 方法 | 只能访问 public 方法 |
| 灵活性 | 高(运行时注入 Adaptee) | 低(编译时绑定) | 中 |
| 典型应用 | 第三方库适配、多 Adaptee 场景 | 复用遗留类 protected 方法 | 两个系统互操作 |
| 软考出现频率 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐ |
软考核心结论:软考中如果没有特别说明,默认考察对象适配器(组合方式)。如果题干明确提到 "继承" 或 "无法修改 Adaptee 源码但需要访问其 protected 方法",才考虑类适配器。
七、软考高频考点与易混淆辨析
7.1 高频考点
| 考点 | 内容 |
|---|---|
| 模式分类 | 结构型模式(GoF 23 正式成员) |
| 核心三角色 | Target(目标接口)、Adaptee(被适配者)、Adapter(适配器) |
| 对象适配器 | Adapter组合/聚合Adaptee,将 Target 方法委托给 Adaptee 方法 |
| 类适配器 | Adapter继承Adaptee,直接调用父类方法 |
| 设计原则 | 开闭原则:不修改 Target 和 Adaptee 的源码,只新增 Adapter |
| 适用场景 | ① 使用现有类但接口不匹配 ② 需要复用多个子类但接口不一致 ③ 需要统一多个类的接口 |
| 与桥接模式区别 | 适配器:事后补救,接口不兼容时的转换;桥接:事前设计,将抽象与实现分离 |
| 与装饰模式区别 | 适配器:转换接口,不改变功能;装饰:增强功能,不改变接口 |
7.2 易混淆辨析:适配器 vs 桥接 vs 装饰
| 对比项 | 适配器模式 | 桥接模式 | 装饰模式 |
|---|---|---|---|
| 目的 | 转换接口,让不兼容的接口能一起工作 | 分离抽象与实现,让它们独立变化 | 增强功能,给对象添加额外职责 |
| 是否改变接口 | ✅ 改变(Target → Adaptee 转换) | ❌ 不改变(抽象接口与实现接口一致) | ❌ 不改变(保持原接口,增加功能) |
| 是否改变功能 | ❌ 不改变(只是转换调用方式) | ❌ 不改变(只是分离层次) | ✅ 改变(增加新功能) |
| 设计时机 | 事后补救(已有代码不兼容) | 事前设计(架构层面分离) | 运行时动态增强 |
| 类图特征 | 有 Adapter 类做转换 | 有 Abstraction 和 Implementor 两个独立层次 | 有 Decorator 继承/组合 Component |
| 软考考法 | 代码填空(补全 Adapter 转换逻辑) | 类图识别(两个独立继承树) | 类图识别(递归组合结构) |
记忆口诀:适配器是 "翻译官"(接口转换),桥接是 "分层设计"(抽象与实现分离),装饰是 "套娃"(层层增强功能)。
八、真题风格模拟与代码填空
模拟题 1(上午选择题)
以下关于适配器模式的叙述中,正确的是()。
A. 适配器模式属于创建型模式,主要用于创建兼容的接口
B. 在对象适配器中,Adapter 通过继承 Adaptee 来实现接口转换
C. 适配器模式可以在不修改原有类源码的前提下,使接口不兼容的类能够协同工作
D. 适配器模式与装饰模式的核心区别在于:适配器模式增强对象功能,装饰模式转换接口
答案:C
解析:
A 错误:适配器属于结构型模式,不是创建型。
B 错误:对象适配器通过组合/聚合(持有引用)Adaptee,类适配器才通过继承。
C 正确:这是适配器模式的核心价值 —— 不修改原有代码(开闭原则),只新增 Adapter 做转换。
D 错误:说反了。适配器模式转换接口(不改变功能),装饰模式增强功能(不改变接口)。
模拟题 2(下午代码填空 — 对象适配器)
某系统定义了统一的支付接口
Payment(Target),要求实现pay(double amount)方法。现有遗留类OldPaymentGateway(Adaptee),提供makePayment(String amount)方法。请使用对象适配器补全(1)~(4)。
// Target:统一支付接口 interface Payment { void pay(double amount); } // Adaptee:遗留支付网关 class OldPaymentGateway { public void makePayment(String amount) { System.out.println("旧网关支付: ¥" + amount); } } // Adapter:对象适配器 class PaymentAdapter (1)______ Payment { private (2)______ adaptee; public PaymentAdapter((3)______ adaptee) { this.adaptee = adaptee; } @Override public void pay(double amount) { adaptee.(4)______(String.valueOf(amount)); } } public class Client { public static void main(String[] args) { Payment payment = new PaymentAdapter(new OldPaymentGateway()); payment.pay(199.99); } }答案:
(1)
implements(2)
OldPaymentGateway(3)
OldPaymentGateway(4)
makePayment
阅卷要点:
(1) 必须是
implements,因为对象适配器实现 Target 接口(如果是类适配器则是extends Adaptee implements Target)。(2)(3) 必须是
OldPaymentGateway(Adaptee 类型),体现组合关系。(4) 必须是
makePayment,这是 Adaptee 的方法名,Adapter 负责将 Target 的pay()转换为 Adaptee 的makePayment()。
模拟题 3(下午代码填空 — 类适配器)
某系统新定义了
MediaPlayer接口(Target),要求实现play(String filename)方法。现有遗留类AdvancedMediaPlayer(Adaptee),提供playMp4(String file)和playVlc(String file)方法。请使用类适配器补全(1)~(3)。
// Target:统一播放器接口 interface MediaPlayer { void play(String filename); } // Adaptee:高级播放器(支持 MP4 和 VLC) class AdvancedMediaPlayer { public void playMp4(String file) { System.out.println("播放 MP4: " + file); } public void playVlc(String file) { System.out.println("播放 VLC: " + file); } } // Adapter:类适配器 class MediaAdapter (1)______ AdvancedMediaPlayer (2)______ MediaPlayer { @Override public void play(String filename) { if (filename.endsWith(".mp4")) { (3)______(filename); } else if (filename.endsWith(".vlc")) { playVlc(filename); } } } public class Client { public static void main(String[] args) { MediaPlayer player = new MediaAdapter(); player.play("movie.mp4"); } }答案:
(1)
extends(2)
implements(3)
playMp4
阅卷要点:
(1) 必须是
extends,类适配器通过继承 Adaptee 获得其方法。(2) 必须是
implements,类适配器同时实现 Target 接口。(3) 必须是
playMp4,因为类适配器继承了 Adaptee,可以直接调用父类方法(不需要adaptee.playMp4(),因为this就是 Adaptee 的子类)。注意:类适配器中调用父类方法可以直接写方法名,也可以写
super.playMp4(filename),两者都算对。但adaptee.playMp4()是对象适配器的写法,在类适配器中会扣分(因为类适配器没有adaptee字段)。
九、常见陷阱与注意事项
陷阱 1:误认为适配器是创建型模式
适配器属于结构型模式,因为它解决的是类与类之间的结构关系(接口转换),而不是对象的创建问题。软考上午题如果问"以下属于创建型模式的是",选项里出现"适配器"不能选。
陷阱 2:对象适配器 vs 类适配器混淆
这是软考最高频的混淆点:
对象适配器:
implements Target+private Adaptee adaptee(组合关系)类适配器:
extends Adaptee implements Target(继承关系)
快速判断技巧:看 Adapter 类图里与 Adaptee 的关系线。如果是带空心菱形的实线(组合/聚合)→ 对象适配器;如果是带空心三角的实线(继承)→ 类适配器。
陷阱 3:类适配器中错误使用对象适配器的调用方式
类适配器继承了 Adaptee,可以直接调用父类方法(如playMp4(filename)或super.playMp4(filename))。如果写成adaptee.playMp4(filename),说明把类适配器当成了对象适配器,判卷时会扣分(因为类适配器没有adaptee成员变量)。
陷阱 4:与桥接模式混淆
桥接模式(Bridge)也是结构型模式,但它的目的是将抽象与实现分离,使它们可以独立变化,通常有两个独立的继承树(如Shape抽象层和Drawing实现层)。适配器模式只有一个 Adaptee 和一个 Target,目的是接口转换。如果类图里只有一个转换类(Adapter)连接两个不兼容的接口 → 适配器;如果有两个独立的层次结构通过组合关联 → 桥接。
陷阱 5:与装饰模式混淆
装饰模式(Decorator)也是结构型模式,包装一个对象以添加新功能,但保持接口不变(如BufferedReader包装Reader)。适配器模式是改变接口以兼容不同接口,不添加新功能。如果类图里 Adapter 的方法名与 Target 一致,但内部调用了不同的 Adaptee 方法 → 适配器;如果 Decorator 新增了方法或增强了原有方法的功能 → 装饰。
陷阱 6:Adapter 的构造器参数
对象适配器的构造器通常需要传入 Adaptee 实例(如new Adapter(new Adaptee())),因为 Adapter 通过组合持有 Adaptee。类适配器通常不需要构造器参数(直接new Adapter()),因为 Adapter 自身就是 Adaptee 的子类。软考代码填空时,如果看到 Adapter 构造器有参数 → 对象适配器;无参数 → 类适配器。
十、总结
| 要点 | 内容 |
|---|---|
| 定义 | 将一个类的接口转换成客户希望的另外一个接口,使原本不兼容的类能协同工作 |
| 分类 | 结构型模式(GoF 23 正式成员) |
| 核心三角色 | Target(目标接口)、Adaptee(被适配者)、Adapter(适配器) |
| 对象适配器 | Adapter组合Adaptee,将 Target 方法委托给 Adaptee 方法 |
| 类适配器 | Adapter继承Adaptee,直接调用父类方法 |
| 设计原则 | 开闭原则:不修改 Target 和 Adaptee,只新增 Adapter |
| 与桥接模式区别 | 适配器:事后补救,接口转换;桥接:事前设计,抽象与实现分离 |
| 与装饰模式区别 | 适配器:转换接口,不改变功能;装饰:增强功能,不改变接口 |
| 软考重点 | 对象适配器 vs 类适配器的区分;代码填空补全 Adapter 的转换逻辑;类图角色识别 |
| 答题技巧 | 看到"接口不兼容"、"需要转换接口"、"已有类但接口不匹配" → 适配器;看到组合关系 → 对象适配器;看到继承关系 → 类适配器 |
系列预告:下一篇将讲桥接模式—— 当类的抽象和实现都需要独立扩展时,如何用 "组合代替继承" 的方式,将两个独立变化的维度解耦,避免类爆炸。咱们下回见。
