- 示例工程
- 教程
【免费下载链接】java-design-patterns
Design patterns implemented in Java
本文以 java-design-patterns 仓库中 adapter 模块(
localization/it/adapter/README.md对应的模式讲解)为骨架,结合模块源码、测试与构建配置,深入剖析适配器模式(Adapter Pattern,又称 Wrapper 包装器)的意图、角色划分、对象适配器的实现方式、适用场景与类/对象两种变体的取舍。读完本文,你将能够理解适配器如何让"接口不兼容"的类协同工作,并能直接在 Java 项目中复刻这一结构。
模式概览:一个解决接口不兼容问题的结构型模式
适配器模式属于结构型模式(Structural Pattern),出自经典的《Design Patterns: Elements of Reusable Object-Oriented Software》(GoF 四卷书)一书,在仓库元信息中标记了Gang of Four标签。
意图(Intent):将某个类的接口转换成客户端期望的另一个接口。适配器模式让那些因接口不兼容而原本无法协作的类能够一起工作。
用一句话概括:适配器模式允许你把一个原本不兼容的对象"包装"进一个适配器中,使其与另一个类兼容。用维基百科的定义来说:在软件工程中,适配器模式是一种软件设计模式,它允许已有类的接口被当作另一种接口来使用。它常被用来让已有类在不修改自身源代码的前提下与其他类协同工作。
这一模式之所以重要,是因为现实中的类往往各自定义接口,而当我们要复用它们时,接口签名却不一定对得上——适配器正是那个"翻译层"。
现实世界的类比:读卡器、电源适配器与翻译官
原文档给出了三个贴近生活的类比,帮助理解适配器的本质:
- 读卡器:存储卡上的照片要转移到电脑上,需要一种与电脑端口兼容的适配设备,读卡器就是一个 adapter——它把存储卡的接口转换成电脑能识别的 USB 接口。
- 电源适配器:三脚插头无法插入两孔插座,需要一个电源适配器让它变得兼容。
- 翻译官:同声传译把一个人说的话翻译给另一个人,让两个语言不通的人能够交流。
这三个例子共同揭示了适配器的核心价值:在不改变两端(被适配对象与客户端)的前提下,通过一个中间层完成接口的转换。
模式角色与 UML 类图
适配器模式通常由四个角色构成,在仓库源码中一一对应:
| 角色 | 模式职责 | 本示例中的实现 |
|---|---|---|
| Target(目标接口) | 客户端所期望的接口 | RowingBoat |
| Client(客户端) | 只面向 Target 接口编程 | Captain |
| Adaptee(被适配者) | 已存在但接口不兼容的类 | FishingBoat |
| Adapter(适配器) | 将 Adaptee 的接口转换为 Target | FishingBoatAdapter |
适配器模式 UML 类图
类图直观地展示了依赖方向:Captain只依赖RowingBoat接口,FishingBoatAdapter实现RowingBoat并持有(组合)FishingBoat,从而把sail()包装成row()。
源码级示例:海盗来袭,船长必须逃离
仓库中的示例讲述了一个完整的故事:船长只会驾驶划艇(rowing boat),完全不会开船;一天海盗来了,船长必须逃走,可手边只有一艘渔船(fishing boat)。我们需要一个适配器,让船长用他"会划桨"的技能驾驶渔船逃生。
整个示例位于 adapter/src/main/java/com/iluwatar/adapter 目录下,共四个核心类。
1. Target:RowingBoat接口
RowingBoat是客户端期望的接口,也是整个模式的"目标":
public interface RowingBoat { void row(); }2. Adaptee:FishingBoat类
FishingBoat是需要复用的既有类,但它暴露的方法是sail()(航行),与客户端需要的row()(划桨)接口不匹配:
@Slf4j final class FishingBoat { void sail() { LOGGER.info("The fishing boat is sailing"); } }从源码可以看到:FishingBoat被声明为final包级私有类,sail()也是包级方法,并通过 Lombok 的@Slf4j注解输出日志——这正是"我们不打算修改既有类"的现实写照。
3. Client:Captain类
Captain是模式的客户端,它只面向RowingBoat接口编程:
@Setter @NoArgsConstructor @AllArgsConstructor public final class Captain { private RowingBoat rowingBoat; void row() { rowingBoat.row(); } }注意Captain持有的是接口类型RowingBoat而非具体类,因此只要给它注入任何实现了RowingBoat的对象,它就能"划船"。Lombok 注解@Setter、@NoArgsConstructor、@AllArgsConstructor分别为它生成了 setter、无参构造和全参构造,方便在运行时替换所依赖的船只。
4. Adapter:FishingBoatAdapter类
FishingBoatAdapter是模式的核心——适配器。它实现RowingBoat接口,内部通过组合持有FishingBoat,把row()调用转发给渔船的sail():
public class FishingBoatAdapter implements RowingBoat { private final FishingBoat boat = new FishingBoat(); public final void row() { boat.sail(); } }适配器的 Javadoc 明确写道:"Adapter class. Adapts the interface of the device (FishingBoat) into RowingBoat interface expected by the client (Captain)."——即把被适配设备的接口转换为客户端期望的接口。
组装与运行
在 App.java 的main方法中完成组装并演示:
public static void main(final String[] args) { // The captain can only operate rowing boats but with adapter he is able to // use fishing boats as well var captain = new Captain(new FishingBoatAdapter()); captain.row(); }运行后,由于FishingBoat.sail()使用 SLF4J 记录日志,控制台输出如下(时间戳会随运行时刻变化):
10:25:08.074 [main] INFO com.iluwatar.adapter.FishingBoat -- The fishing boat is sailing至此,船长借助适配器成功"驾驶"渔船逃离海盗。整个过程没有修改FishingBoat的源代码,也没有修改Captain的代码,仅通过新增一个FishingBoatAdapter就完成了接口桥接。
调用链与时序验证:从 row() 到 sail()
为了确认上述调用链的真实性,仓库提供了单元测试 AdapterPatternTest.java,它用 Mockito 的spy包装FishingBoatAdapter,再注入Captain,最后用verify断言调用链:
@Test void testAdapter() { var captain = (Captain) beans.get(ROWING_BEAN); // when captain moves captain.row(); // the captain internally calls the battleship object to move var adapter = (RowingBoat) beans.get(FISHING_BEAN); verify(adapter).row(); }测试的 Javadoc 也说明了意图:"when we use the row() method on a captain bean(client), it is internally calling sail method on the fishing boat object."测试文件把Captain与适配器作为两个 bean 存入 Map,模拟了依赖注入的场景,验证了"客户端调用row()→ 适配器转发 →FishingBoat.sail()"的完整链路。
适配器模式时序图
时序图展示了消息的流转顺序:Captain调用FishingBoatAdapter.row(),适配器内部再调用FishingBoat.sail()。
从构建配置看,adapter/pom.xml 依赖了slf4j-api、logback-classic(日志输出)、junit-jupiter-engine与mockito-core(测试),并通过maven-assembly-plugin将com.iluwatar.adapter.App指定为主类,说明该模块可以独立打包运行;仓库根目录提供了mvnw/mvnw.cmd包装脚本,可在本地通过 Maven Wrapper 执行构建与测试。
适用场景:什么时候使用适配器模式
原文档列出了四种典型适用情形,在实际项目中可按此对照:
- 想使用一个已有的类,但它的接口与你的需求不匹配——这是最直接的场景,适配器负责把接口"翻译"成你需要的样子。
- 想创建一个可复用的类,它能与无关的、事先未预料的类协作,即那些接口不一定兼容的类。适配器让客户端与具体实现彻底解耦。
- 需要同时使用多个已有的子类,但逐一为它们创建子类来适配接口不切实际——此时对象适配器可以适配其父类的接口,一次适配即可覆盖所有子类。
- 在使用第三方库的大多数应用中,适配器常被用作应用与第三方库之间的中间层,把应用与库解耦。当需要更换库时,只需为新库写一个新的适配器,而无需改动应用代码。
最后一个场景正是适配器模式在企业级开发中的最大价值:它是"隔离外部依赖变更"的廉价方案。
类适配器 vs 对象适配器:两种变体的取舍
适配器模式存在两种变体,它们在灵活性上有着明显差异。本仓库示例使用的是对象适配器(通过组合持有FishingBoat),源码 App.java 的 Javadoc 中也明确说明:"There are two variations of the Adapter pattern: The class adapter implements the adaptee's interface whereas the object adapter uses composition to contain the adaptee in the adapter object. This example uses the object adapter approach."
**类适配器(Class Adapter)**的特点:
- 通过继承被适配类来完成适配,因此绑定于一个特定的被适配类,无法适配一个类及其全部子类;
- 因为适配器是被适配类的子类,所以可以覆盖被适配类的一部分行为;
- 只引入一个对象,不需要额外的指针间接引用被适配类。
**对象适配器(Object Adapter)**的特点:
- 单个适配器可以同时与多个被适配类协作,包括被适配类及其所有子类,还能一次性为所有被适配类增加功能;
- 但覆盖被适配类行为变得更困难——需要先创建被适配类的子类,再让适配器引用这个子类而非被适配类本身。
选择建议:当需要覆盖被适配者的行为且被适配类不会扩展时,类适配器更直接;当需要适配一个类层级、追求组合的灵活性与低耦合时,对象适配器(本仓库的默认选择)更符合"优先组合而非继承"的现代工程实践。
JDK 中的真实案例
适配器模式在 JDK 中随处可见,原文档列出了如下经典实例,可作为学习参照:
java.util.Arrays#asList():把数组适配为List接口;java.util.Collections#list():把Enumeration适配为List;java.util.Collections#enumeration():把Collection适配为Enumeration;javax.xml.bind.annotation.adapters.XmlAdapter:JAXB 中用于在 XML 表示与 Java 类型之间做适配的机制。
此外,java.io.InputStreamReader与java.io.OutputStreamWriter也是经典的流适配器(把字节流适配为字符流),GUI 组件库中也常通过插件式适配器在不同组件接口之间转换。
总结
适配器模式通过引入一个中间适配层,让接口不兼容但功能匹配的类得以协作。在 java-design-patterns 的 adapter 模块中:
RowingBoat(Target)、Captain(Client)、FishingBoat(Adaptee)、FishingBoatAdapter(Adapter)四个角色分工清晰;- 采用对象适配器(组合)实现,不修改任何既有类的源码;
- 测试与构建配置完整,可独立运行验证调用链。
掌握适配器模式后,你在面对"新系统要复用老代码""更换第三方库""整合异构组件"等场景时,就有了一个低侵入、高内聚的解决方案——这正是它在 GoF 模式中长期保持高频实用性的原因。
- 示例工程
- 教程
【免费下载链接】java-design-patterns
Design patterns implemented in Java
相关推荐
Druid SQL 解析器重构实录:统一 DialectFeature 门控模式与 tableAlias() 方法分解
Druid SQL 解析器重构实录:统一 DialectFeature 门控模式与 tableAlias 方法分解 导读 本文以 Druid(阿里云 DataW
示例工程教程Java 适配器模式(Adapter Pattern)实战解析:以 java-design-patterns 项目为例实现不兼容接口的无缝集成
Java 适配器模式(Adapter Pattern)实战解析:以 java design patterns 项目为例实现不兼容接口的无缝集成 适配器模式(Ad
示例工程教程Java 适配器模式实战解析:以 java-design-patterns 仓库的 FishingBoatAdapter 为例
Java 适配器模式实战解析:以 java design patterns 仓库的 FishingBoatAdapter 为例 导读 本文以当前仓库 java
示例工程教程
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考