跳转到内容

适配器 (Adapter)

Ken 码农
  • 设计模式
  • 结构型模式

适配器 (Adapter) 是一种结构型设计模式,用来解决“接口不兼容但又想复用”的问题。它通过引入一个适配器,将已有类的接口转换为客户端期望的接口,让原本不能直接协同工作的类可以一起工作。

换句话说:不改老代码,对外暴露一个新接口,通过适配器在中间做转换和转发。


  • 系统中已经存在一批稳定运行的旧服务或第三方库,它们的接口设计与现在的统一规范不一致。
  • 新系统希望统一调用方式,但又不想大规模修改旧代码,甚至有些第三方库根本无法修改源码。
  • 希望在不改或尽量少改旧实现的前提下,把这些旧服务“接”到新的接口体系里。

适配器模式就是在客户端被适配者之间,加一层转换器,把“插头”对上。


  • 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 接口:

  • 入参使用 studentIdBigDecimal 金额
  • 返回布尔值表示是否扣款成功

又不能随意修改旧的 LegacyPaymentClient。这时就可以通过适配器,把新中台的 FeeService 接口,适配到旧的支付客户端上。


import java.math.BigDecimal;
/**
* 目标接口:学生缴费服务
*/
public interface FeeService {
/**
* 学生缴费
* @param studentId 学号
* @param amount 金额
* @return 是否扣费成功
*/
boolean pay(String studentId, BigDecimal amount);
}

/**
* 被适配者:历史缴费系统的支付客户端
* 约定:
* - 传入账户号和 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;
}
}

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 接口编程。
  • LegacyPaymentAdapterFeeService 的调用,转换为对 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 接口。
  • 学校数据中台定义了自己的 StudentScoreServiceStudentScoreView,作为统一的数据访问契约。
  • ThirdPartyScoreAdapter 负责把第三方返回的数据结构转换成中台的领域对象,实现接口对齐。

按实现方式,适配器模式常见有两种写法:

  • 对象适配器:适配器内部通过组合持有被适配者引用(本篇示例使用的方式)。
  • 类适配器:适配器通过继承被适配者,并实现目标接口。

在 Java 这类单继承语言里,更常用对象适配器:

  • 可以适配多个不同的被适配者实现。
  • 不会被单继承限制住。
  • 组合关系更清晰,有利于解耦。

  • 引入历史系统:例如学校已有老的学生账户、缴费、宿舍管理系统,接口风格各不相同,需要在新的数据中台统一调用。
  • 第三方库封装:第三方提供的接口风格不符合当前项目的接口规范,希望包一层统一接口。
  • 重构过渡阶段:新旧接口共存期间,通过适配器兼容旧接口,逐步迁移到新接口。

优点

  • 复用已有实现:在不修改旧代码的前提下接入新系统。
  • 隔离变化:客户端只依赖目标接口,屏蔽了历史系统或第三方库的细节。
  • 迁移友好:可以作为系统重构、系统整合中的过渡层。

注意事项

  • 适配逻辑本身不要过于复杂,否则适配器会变成新的“泥球”。
  • 如果可以直接修改被适配者,使其实现目标接口,有时更简单;适配器更适合不能改源码或改动成本较高的场景。

  • 适配器模式关注的是接口不兼容但又想复用的问题。
  • 通过在中间加一层适配,将新接口的调用转换为对旧接口的调用。
  • 在学校这类信息系统较多的场景中,适配器非常适合用来对接历史系统、第三方服务和统一的数据中台接口。