适配器 (Adapter) 是一种结构型设计模式,用来解决“接口不兼容但又想复用”的问题。它通过引入一个适配器,将已有类的接口转换为客户端期望的接口,让原本不能直接协同工作的类可以一起工作。
换句话说:不改老代码,对外暴露一个新接口,通过适配器在中间做转换和转发。
典型问题场景
Section titled “典型问题场景”- 系统中已经存在一批稳定运行的旧服务或第三方库,它们的接口设计与现在的统一规范不一致。
- 新系统希望统一调用方式,但又不想大规模修改旧代码,甚至有些第三方库根本无法修改源码。
- 希望在不改或尽量少改旧实现的前提下,把这些旧服务“接”到新的接口体系里。
适配器模式就是在客户端和被适配者之间,加一层转换器,把“插头”对上。
- Target(目标接口):客户端现在期待使用的接口。
- Adaptee(被适配者):已有的、接口不兼容的类。
- Adapter(适配器):实现目标接口,内部持有被适配者实例,在目标接口方法里调用被适配者的逻辑,并做必要的数据转换。
- Client(客户端):只依赖目标接口,通过适配器间接使用被适配者。
结构示意:
classDiagram
class FeeService {
<<interface>>
+pay(studentId: String, amount: BigDecimal) boolean
}
class LegacyPaymentClient {
+doPay(accountNo: String, money: double) int
}
class LegacyPaymentAdapter {
-client LegacyPaymentClient
+pay(studentId: String, amount: BigDecimal) boolean
}
class FeeApplicationService
FeeService <|.. LegacyPaymentAdapter
LegacyPaymentAdapter o-- LegacyPaymentClient
FeeApplicationService ..> FeeService : uses
示例:学校缴费系统中的适配器
Section titled “示例:学校缴费系统中的适配器”某高校有一套老的缴费系统,只提供了一个 LegacyPaymentClient,按照“账户号 + double 金额”来扣费,返回整型状态码:
0表示成功- 非零表示失败
现在学校建设新的学生缴费中台,希望统一用一个 FeeService 接口:
- 入参使用
studentId和BigDecimal金额 - 返回布尔值表示是否扣款成功
又不能随意修改旧的 LegacyPaymentClient。这时就可以通过适配器,把新中台的 FeeService 接口,适配到旧的支付客户端上。
1. 目标接口:统一的缴费服务
Section titled “1. 目标接口:统一的缴费服务”import java.math.BigDecimal;
/** * 目标接口:学生缴费服务 */public interface FeeService {
/** * 学生缴费 * @param studentId 学号 * @param amount 金额 * @return 是否扣费成功 */ boolean pay(String studentId, BigDecimal amount);}2. 被适配者:旧的支付客户端
Section titled “2. 被适配者:旧的支付客户端”/** * 被适配者:历史缴费系统的支付客户端 * 约定: * - 传入账户号和 double 金额 * - 返回 0 表示成功,其他数字表示失败 */public class LegacyPaymentClient {
public int doPay(String accountNo, double money) { // 这里假设是调用老系统的远程接口 System.out.println("调用历史缴费系统,账户=" + accountNo + ", 金额=" + money); // 简化起见,假设都成功 return 0; }}3. 适配器:将新接口适配到旧系统
Section titled “3. 适配器:将新接口适配到旧系统”import java.math.BigDecimal;
/** * 适配器:实现统一的 FeeService 接口 * 内部委托给 LegacyPaymentClient 完成实际扣费 */public class LegacyPaymentAdapter implements FeeService {
private final LegacyPaymentClient client;
public LegacyPaymentAdapter(LegacyPaymentClient client) { this.client = client; }
@Override public boolean pay(String studentId, BigDecimal amount) { // 1. 学号转换为老系统账户号(示例中简单拼接) String accountNo = "STU-" + studentId;
// 2. BigDecimal 转 double(真实项目里要注意精度和边界处理) double money = amount.doubleValue();
// 3. 调用历史系统 int resultCode = client.doPay(accountNo, money);
// 4. 将状态码转换为布尔结果 return resultCode == 0; }}4. 客户端:新的缴费应用服务
Section titled “4. 客户端:新的缴费应用服务”import java.math.BigDecimal;
/** * 新的缴费应用服务,只依赖 FeeService 接口 */public class FeeApplicationService {
private final FeeService feeService;
public FeeApplicationService(FeeService feeService) { this.feeService = feeService; }
public void payTuition(String studentId, BigDecimal amount) { boolean success = feeService.pay(studentId, amount); if (success) { System.out.println("学号 " + studentId + " 学费缴纳成功"); } else { System.out.println("学号 " + studentId + " 学费缴纳失败"); } }
public static void main(String[] args) { // 组合:旧系统客户端 + 适配器 + 新应用服务 LegacyPaymentClient legacyClient = new LegacyPaymentClient(); FeeService feeService = new LegacyPaymentAdapter(legacyClient); FeeApplicationService appService = new FeeApplicationService(feeService);
appService.payTuition("20240001", new BigDecimal("5000.00")); }}在这个示例中:
- 新的缴费中台只面向
FeeService接口编程。 LegacyPaymentAdapter将FeeService的调用,转换为对LegacyPaymentClient的调用。- 历史缴费系统可以继续保持原有接口与实现,新系统也获得了统一、清晰的接口。
示例:对接第三方成绩服务并读取数据库
Section titled “示例:对接第三方成绩服务并读取数据库”学校想要在数据中台中统一查询学生的成绩信息:
- 中台内部希望通过
StudentScoreService接口,以studentId查询成绩列表。 - 实际成绩数据存放在 MySQL 或 MongoDB 等数据源中,访问方式依赖第三方提供的
ThirdPartyScoreClient。 - 第三方接口返回的是特定格式的 DTO 列表,字段命名、数据结构与中台希望暴露给上层的领域模型不一致。
可以通过适配器,把中台的 StudentScoreService 接口,适配到第三方客户端和底层数据库访问上。
1. 目标接口:统一的学生成绩查询
Section titled “1. 目标接口:统一的学生成绩查询”import java.util.List;
/** * 目标接口:学生成绩查询服务 */public interface StudentScoreService {
/** * 按学号查询该学生的所有课程成绩 * @param studentId 学号 * @return 领域层的成绩视图 */ List<StudentScoreView> listScores(String studentId);}/** * 领域层的成绩视图对象 */public class StudentScoreView { private String courseCode; private String courseName; private String term; private Integer score;
// getter/setter 省略}2. 被适配者:第三方成绩客户端(封装了 MySQL/MongoDB 访问)
Section titled “2. 被适配者:第三方成绩客户端(封装了 MySQL/MongoDB 访问)”这里用伪代码说明第三方接口,它可能内部用 MySQL 或 MongoDB 实现,学校方不关心细节,只能通过对方提供的客户端调用。
import java.util.List;
/** * 被适配者:第三方成绩系统的客户端 * 假设其内部可能使用 MySQL 或 MongoDB 查询成绩 */public class ThirdPartyScoreClient {
/** * 按学号查询成绩,返回的是第三方定义的数据结构 */ public List<ThirdPartyScoreDTO> queryScoresByStudentNo(String stuNo) { // 内部可能是 MySQL: // SELECT * FROM t_score WHERE student_no = ? // // 也可能是 MongoDB: // db.scores.find({ studentNo: stuNo }) // // 这里简化为返回模拟数据 return List.of( new ThirdPartyScoreDTO("CS101", "计算机基础", "2023-秋", 92), new ThirdPartyScoreDTO("MA101", "高等数学", "2023-秋", 88) ); }}/** * 第三方定义的成绩 DTO */public class ThirdPartyScoreDTO { private String code; private String name; private String semester; private Integer point;
public ThirdPartyScoreDTO(String code, String name, String semester, Integer point) { this.code = code; this.name = name; this.semester = semester; this.point = point; }
// getter/setter 省略}3. 适配器:统一中台接口,内部委托第三方客户端
Section titled “3. 适配器:统一中台接口,内部委托第三方客户端”import java.util.List;import java.util.stream.Collectors;
/** * 适配器:实现 StudentScoreService,内部委托 ThirdPartyScoreClient */public class ThirdPartyScoreAdapter implements StudentScoreService {
private final ThirdPartyScoreClient client;
public ThirdPartyScoreAdapter(ThirdPartyScoreClient client) { this.client = client; }
@Override public List<StudentScoreView> listScores(String studentId) { // 1. 学号可能需要转换为第三方的 studentNo 规则 String stuNo = studentId;
// 2. 调用第三方客户端 List<ThirdPartyScoreDTO> dtoList = client.queryScoresByStudentNo(stuNo);
// 3. 将第三方 DTO 转换为中台的领域视图对象 return dtoList.stream() .map(dto -> { StudentScoreView view = new StudentScoreView(); view.setCourseCode(dto.getCode()); view.setCourseName(dto.getName()); view.setTerm(dto.getSemester()); view.setScore(dto.getPoint()); return view; }) .collect(Collectors.toList()); }}4. 上层应用服务:只依赖统一的查询接口
Section titled “4. 上层应用服务:只依赖统一的查询接口”import java.util.List;
/** * 应用服务:给教务管理、数据中台等上层系统提供统一的查询入口 */public class StudentScoreApplicationService {
private final StudentScoreService scoreService;
public StudentScoreApplicationService(StudentScoreService scoreService) { this.scoreService = scoreService; }
public void printScores(String studentId) { List<StudentScoreView> scores = scoreService.listScores(studentId); for (StudentScoreView score : scores) { System.out.println(score.getTerm() + " " + score.getCourseCode() + " " + score.getCourseName() + " 成绩=" + score.getScore()); } }
public static void main(String[] args) { ThirdPartyScoreClient client = new ThirdPartyScoreClient(); StudentScoreService scoreService = new ThirdPartyScoreAdapter(client); StudentScoreApplicationService appService = new StudentScoreApplicationService(scoreService);
appService.printScores("20240001"); }}这个示例中:
- 第三方成绩客户端可以基于 MySQL 或 MongoDB 自由实现,只需要保证对外暴露
ThirdPartyScoreClient接口。 - 学校数据中台定义了自己的
StudentScoreService和StudentScoreView,作为统一的数据访问契约。 ThirdPartyScoreAdapter负责把第三方返回的数据结构转换成中台的领域对象,实现接口对齐。
类适配器与对象适配器
Section titled “类适配器与对象适配器”按实现方式,适配器模式常见有两种写法:
- 对象适配器:适配器内部通过组合持有被适配者引用(本篇示例使用的方式)。
- 类适配器:适配器通过继承被适配者,并实现目标接口。
在 Java 这类单继承语言里,更常用对象适配器:
- 可以适配多个不同的被适配者实现。
- 不会被单继承限制住。
- 组合关系更清晰,有利于解耦。
- 引入历史系统:例如学校已有老的学生账户、缴费、宿舍管理系统,接口风格各不相同,需要在新的数据中台统一调用。
- 第三方库封装:第三方提供的接口风格不符合当前项目的接口规范,希望包一层统一接口。
- 重构过渡阶段:新旧接口共存期间,通过适配器兼容旧接口,逐步迁移到新接口。
优点与注意事项
Section titled “优点与注意事项”优点
- 复用已有实现:在不修改旧代码的前提下接入新系统。
- 隔离变化:客户端只依赖目标接口,屏蔽了历史系统或第三方库的细节。
- 迁移友好:可以作为系统重构、系统整合中的过渡层。
注意事项
- 适配逻辑本身不要过于复杂,否则适配器会变成新的“泥球”。
- 如果可以直接修改被适配者,使其实现目标接口,有时更简单;适配器更适合不能改源码或改动成本较高的场景。
- 适配器模式关注的是接口不兼容但又想复用的问题。
- 通过在中间加一层适配,将新接口的调用转换为对旧接口的调用。
- 在学校这类信息系统较多的场景中,适配器非常适合用来对接历史系统、第三方服务和统一的数据中台接口。