大白话说Java设计模式-14-适配器模式(业务实战篇)

📌 PDF :AI人工智能 --- AI Agent智能体实战项目

大白话说Java设计模式-14-适配器模式(业务实战篇):大白商城多端 API 兼容的"翻译官"

📌 一句话本质:适配器模式就是"翻译官",让不兼容的接口能一起工作。

🏷️ 标签:适配器模式 / Java 设计模式 / 第三方对接 / Spring MVC / 大白商城 🎯 适合:初中级后端 / 经常对接第三方 API 的工程师


目录


一、业务场景引入:为什么需要"翻译官"?

大白商城 2025 年对接了 5 家物流公司(顺丰/中通/京东/韵达/邮政),踩了一个大坑。事情是这样的:

老板要求"订单发货后必须自动同步物流轨迹到商城"。我作为后端负责人,吭哧吭哧对接 5 家物流公司,写了 5 套完全不同的调用代码

打开代码一看,心里凉了:

java 复制代码
/**
 * 大白商城老的"多物流公司"代码
 */
public class LogisticsService {

    public void shipGoods(Long orderId, String company) {
        if ("shunfeng".equals(company)) {
            // 顺丰的接口
            ShunFengClient sf = new ShunFengClient("app_key_xxx");
            ShunFengRequest req = new ShunFengRequest();
            req.setOrderNo("ORDER_" + orderId);
            req.setCustomerName("大白商城");
            ShunFengResponse resp = sf.createOrder(req);
            System.out.println("顺丰单号: " + resp.getMailNo());
        } else if ("zhongtong".equals(company)) {
            // 中通的接口
            ZTOClient zto = new ZTOClient("partner_id_xxx");
            ZTORequest req = new ZTORequest();
            req.setOrderId("ORDER_" + orderId);
            req.setSendSite("杭州站点");
            ZTOResponse resp = zto.submitOrder(req);
            System.out.println("中通单号: " + resp.getOrderNo());
        } else if ("jd".equals(company)) {
            // 京东的接口
            JDLogisticsClient jd = new JDLogisticsClient("token_xxx");
            JDRequest req = new JDRequest();
            req.setOrderId(orderId);
            req.setSkuList(/* ... */);
            JDResponse resp = jd.createOrder(req);
            System.out.println("京东单号: " + resp.getWaybillNo());
        } else if ("yunda".equals(company)) {
            // 韵达的接口
            // ... 又是一套完全不同的调用
        } else if ("youzheng".equals(company)) {
            // 邮政的接口
            // ... 又是一套
        }
    }
}

这种代码的痛点

序号 问题 后果
5 套不同的接口 每个物流公司调用方式都不同
if-else 堆叠 加一家物流公司要改 if-else
业务代码被污染 看不到核心业务(发货)
第三方接口变更 一家物流公司改接口,要全改

老板要的"加物流公司不动业务代码",怎么搞?

答案就是------适配器模式

1.1 大白话讲透适配器

继续打比方:

场景:大白商城要和 5 家物流公司(顺丰/中通/京东/韵达/邮政)对接。每家公司的接口都不同(参数名、调用方式、返回格式都不同)。

  • 错误做法:业务代码里写 5 个 if-else,每个分支调不同的接口
  • 正确做法
    1. 大白商城定义统一的"物流接口"LogisticsAdapter
    2. 每家物流公司写一个**"翻译官"**(ShunFengAdapterZTOAdapterJDAdapter...)
    3. 翻译官把"大白商城的统一请求"翻译成"物流公司能识别的请求"
    4. 业务代码只调统一接口,不管有多少家物流公司

适配器模式 = 把一个类的接口转换成客户希望的另一个接口,让原本不兼容的类能一起工作

1.2 适配器模式的 3 个真实场景

大白商城里,适配器模式用在:

场景 "被适配者" "目标接口"
多物流公司对接 顺丰/中通/京东各自的 API 大白商城统一的物流接口
多支付渠道 支付宝/微信/银联 SDK 大白商城统一的支付接口
多短信服务 阿里云/腾讯云/华为云短信 大白商城统一的短信接口
老系统对接 老系统 API 新系统接口规范
Spring MVC 各种 Controller HandlerAdapter 统一处理

任何"对接不兼容的第三方接口"的场景,都用适配器


二、反面教材:不兼容接口的"灾难现场"

我们看 4 个反面教材,看它们是怎么一步步崩的。

2.1 反面教材 v1:业务代码直接调第三方

java 复制代码
/**
 * ❌ 反面教材 v1:业务代码直接调第三方
 */
public class LogisticsServiceV1 {

    public void ship(Long orderId, String company) {
        if ("shunfeng".equals(company)) {
            // 直接调顺丰
            new ShunFengClient("app_key").createOrder(/* 顺丰参数 */);
        } else if ("zhongtong".equals(company)) {
            // 直接调中通
            new ZTOClient("partner_id").submitOrder(/* 中通参数 */);
        }
        // ... 每家一套
    }
}

翻车现场

序号 问题 后果
业务被污染 业务代码看不到核心逻辑
加新公司难 改 if-else
第三方变更易受影响 顺丰改 API,全改

2.2 反面教材 v2:包装一层但仍硬编码

java 复制代码
/**
 * ❌ 反面教材 v2:包装一层但仍硬编码
 */
public class LogisticsServiceV2 {

    public void ship(Long orderId, String company) {
        if ("shunfeng".equals(company)) {
            // 即使包装了,仍然 if-else
            ShunFengAdapter adapter = new ShunFengAdapter();
            adapter.ship(orderId);
        } else if ("zhongtong".equals(company)) {
            ZTOAdapter adapter = new ZTOAdapter();
            adapter.ship(orderId);
        }
    }
}

翻车现场

序号 问题 后果
仍有 if-else 换汤不换药
业务不知道用哪个适配器 业务方要知道每家公司的存在

2.3 反面教材 v3:用 Map 但 Key 是字符串

java 复制代码
/**
 * ❌ 反面教材 v3:用 Map 但 Key 是字符串
 */
public class LogisticsServiceV3 {

    private static final Map<String, LogisticsAdapter> ADAPTER_MAP = new HashMap<>();
    static {
        ADAPTER_MAP.put("shunfeng", new ShunFengAdapter());
        ADAPTER_MAP.put("zhongtong", new ZTOAdapter());
    }

    public void ship(Long orderId, String company) {
        LogisticsAdapter adapter = ADAPTER_MAP.get(company);
        if (adapter == null) {
            throw new IllegalArgumentException("不支持的物流公司: " + company);
        }
        adapter.ship(orderId);
    }
}

翻车现场

序号 问题 后果
字符串 Key 易错 写错 Key 编译能过运行崩
加新公司要改 Map 不算"开闭原则"
业务方要知道公司编码 不够抽象

2.4 反面教材 v4:完全硬编码

java 复制代码
/**
 * ❌ 反面教材 v4:完全硬编码
 */
public class LogisticsServiceV4 {

    public void ship(Long orderId) {
        // 硬编码:永远只发顺丰
        new ShunFengClient("app_key").createOrder(/* ... */);
    }
}

翻车现场

序号 问题 后果
绑死一家 客户想换物流公司就崩
无法支持多公司 业务受限

2.5 4 个反面教材的共同病根

痛点 反模式方案能不能解决?
业务不依赖具体公司 ❌ 全部绑死
加新公司不动业务 ❌ 全部要改
第三方变更不影响业务 ❌ 全部要改业务
统一调用入口 ❌ 全部散乱

必须上适配器模式


三、模式原理:适配器的"两种实现 + 一张图"

3.1 适配器的 4 个核心角色

角色 职责 例子
目标接口(Target) 客户期望的接口 LogisticsAdapter
被适配者(Adaptee) 已有的、但接口不兼容的类 ShunFengClient
适配器(Adapter) 把被适配者转换成目标接口 ShunFengAdapter
客户端(Client) 使用目标接口 LogisticsService

关键点客户端只调目标接口,适配器负责"翻译"

3.2 一张图看懂适配器

复制代码
业务方(LogisticsService)
  ↓ 调用
目标接口(LogisticsAdapter)
  ↓
  ├─ ShunFengAdapter → ShunFengClient(顺丰)
  ├─ ZTOAdapter      → ZTOClient(中通)
  ├─ JDAdapter       → JDLogisticsClient(京东)
  ├─ YunDaAdapter    → YunDaClient(韵达)
  └─ YouZhengAdapter → YouZhengClient(邮政)

每个 Adapter 把"大白商城的统一请求"
翻译成"对应物流公司的请求格式"

3.3 适配器的"灵魂三问"

Q1:适配器 vs 装饰器,区别是什么?

答:

  • 适配器改变接口(让不兼容的兼容),包装第三方
  • 装饰器不改变接口(增强原有功能),包装自己
  • 适配器侧重"翻译"装饰器侧重"增强"

Q2:类适配器 vs 对象适配器,怎么选?

答:

  • 类适配器 :用继承(Adapter extends Adaptee),Java 单继承限制
  • 对象适配器 :用组合(Adapter 持有 Adaptee),更灵活
  • 优先用对象适配器(组合优于继承)

Q3:什么时候用适配器,什么时候用桥接?

答:

  • 适配器已有类 (第三方)接口不兼容,事后补救
  • 桥接设计时 就考虑抽象和实现分离,事前规划
  • 适配器是"打补丁"桥接是"从零设计"

3.4 适配器的 3 种写法

写法 适用场景 缺点
类适配器(继承) 简单场景 Java 单继承限制
对象适配器(组合) 通用场景 多一个对象
接口适配器(抽象类) 多个方法只想实现部分 不适用于第三方

大白商城主推对象适配器(组合 + 灵活)。


四、实战代码:大白商城第三方对接完整实现

下面是大白商城生产环境在用的适配器实现,全套代码可直接复制到 IDEA 跑

4.1 项目环境与依赖

pom.xml

xml 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
                             https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.2.0</version>
        <relativePath/>
    </parent>

    <groupId>com.dabai.mall</groupId>
    <artifactId>mall-design-pattern-14</artifactId>
    <version>1.0.0-SNAPSHOT</version>
    <name>mall-design-pattern-14</name>
    <description>大白商城 - 设计模式 14 适配器模式</description>

    <properties>
        <java.version>17</java.version>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

4.2 目标接口:LogisticsAdapter

java 复制代码
package com.dabai.mall.logistics;

import java.math.BigDecimal;

/**
 * ✅ 目标接口:大白商城统一物流接口
 * <p>
 * 所有物流公司的适配器都实现这个接口
 *
 * @author 大白商城技术团队
 */
public interface LogisticsAdapter {

    /**
     * ✅ 发货
     */
    ShipResult ship(ShipRequest request);

    /**
     * ✅ 查询物流轨迹
     */
    TrackResult track(String waybillNo);

    /**
     * ✅ 取消发货
     */
    CancelResult cancel(String waybillNo);

    /**
     * ✅ 获取物流公司编码
     */
    String getCompanyCode();
}

配套 DTO

java 复制代码
package com.dabai.mall.logistics;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.math.BigDecimal;

/**
 * 大白商城统一发货请求
 */
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ShipRequest {

    private String orderNo;
    private String receiverName;
    private String receiverPhone;
    private String receiverAddress;
    private String senderName;
    private String senderPhone;
    private String senderAddress;
    private BigDecimal weight;
    private BigDecimal volume;
    private String remark;
}

/**
 * 大白商城统一发货结果
 */
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
class ShipResult {

    private boolean success;
    private String waybillNo;        // 运单号
    private String errorCode;
    private String errorMessage;
}

/**
 * 物流轨迹
 */
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
class TrackResult {

    private boolean success;
    private String waybillNo;
    private java.util.List<TrackPoint> tracks;
    private String errorCode;
}

/**
 * 轨迹点
 */
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
class TrackPoint {

    private String time;
    private String location;
    private String description;
}

/**
 * 取消结果
 */
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
class CancelResult {

    private boolean success;
    private String errorCode;
    private String errorMessage;
}

4.3 被适配者:3 个第三方物流客户端

ShunFengClient(顺丰)

java 复制代码
package com.dabai.mall.logistics.thirdparty;

import lombok.extern.slf4j.Slf4j;

/**
 * ✅ 被适配者:顺丰 SDK(模拟)
 * <p>
 * 这是第三方 SDK,我们不能改它的接口
 */
@Slf4j
public class ShunFengClient {

    private final String appKey;

    public ShunFengClient(String appKey) {
        this.appKey = appKey;
    }

    /**
     * 顺丰的接口:参数和返回都和大白商城不一样
     */
    public ShunFengResponse createOrder(ShunFengRequest req) {
        log.info("【顺丰】创建订单, appKey={}, orderNo={}", appKey, req.getOrderNo());
        // 实际调用顺丰 API
        return new ShunFengResponse("SF" + System.currentTimeMillis(), "success");
    }

    /**
     * 顺丰的轨迹查询
     */
    public ShunFengTrackResponse queryRoute(String mailNo) {
        log.info("【顺丰】查询轨迹, mailNo={}", mailNo);
        return new ShunFengTrackResponse(mailNo, "已揽收", "2026-07-25 10:00:00");
    }
}

/**
 * 顺丰请求(参数名都不一样)
 */
class ShunFengRequest {
    private String orderNo;       // 大白商城叫 orderNo
    private String customerName;  // 顺丰叫 customerName
    private String customerPhone; // 顺丰叫 customerPhone
    private String address;       // 顺丰叫 address(不区分收发)
    private Double weight;        // 顺丰是 Double

    public String getOrderNo() { return orderNo; }
    public void setOrderNo(String orderNo) { this.orderNo = orderNo; }
    public String getCustomerName() { return customerName; }
    public void setCustomerName(String customerName) { this.customerName = customerName; }
    public String getCustomerPhone() { return customerPhone; }
    public void setCustomerPhone(String customerPhone) { this.customerPhone = customerPhone; }
    public String getAddress() { return address; }
    public void setAddress(String address) { this.address = address; }
    public Double getWeight() { return weight; }
    public void setWeight(Double weight) { this.weight = weight; }
}

/**
 * 顺丰响应
 */
class ShunFengResponse {
    private String mailNo;        // 顺丰叫 mailNo(运单号)
    private String resultCode;    // 顺丰叫 resultCode

    public ShunFengResponse(String mailNo, String resultCode) {
        this.mailNo = mailNo;
        this.resultCode = resultCode;
    }
    public String getMailNo() { return mailNo; }
    public String getResultCode() { return resultCode; }
}

class ShunFengTrackResponse {
    private String mailNo;
    private String routeInfo;
    private String scanTime;

    public ShunFengTrackResponse(String mailNo, String routeInfo, String scanTime) {
        this.mailNo = mailNo;
        this.routeInfo = routeInfo;
        this.scanTime = scanTime;
    }
    public String getMailNo() { return mailNo; }
    public String getRouteInfo() { return routeInfo; }
    public String getScanTime() { return scanTime; }
}

ZTOClient(中通)

java 复制代码
package com.dabai.mall.logistics.thirdparty;

import lombok.extern.slf4j.Slf4j;

/**
 * ✅ 被适配者:中通 SDK(模拟)
 */
@Slf4j
public class ZTOClient {

    private final String partnerId;

    public ZTOClient(String partnerId) {
        this.partnerId = partnerId;
    }

    /**
     * 中通的接口:参数和返回又不一样
     */
    public ZTOResponse submitOrder(ZTORequest req) {
        log.info("【中通】提交订单, partnerId={}, orderId={}", partnerId, req.getOrderId());
        return new ZTOResponse("ZTO" + System.currentTimeMillis(), true);
    }

    public ZTOTrackResponse getRoute(String orderNo) {
        log.info("【中通】查询轨迹, orderNo={}", orderNo);
        return new ZTOTrackResponse(orderNo, "已揽收", "2026-07-25 11:00:00");
    }
}

class ZTORequest {
    private String orderId;       // 中通叫 orderId
    private String sendSite;      // 中通叫 sendSite
    private String receiveName;
    private String receivePhone;
    private String receiveAddress;
    private Double weight;

    public String getOrderId() { return orderId; }
    public void setOrderId(String orderId) { this.orderId = orderId; }
    public String getSendSite() { return sendSite; }
    public void setSendSite(String sendSite) { this.sendSite = sendSite; }
    public String getReceiveName() { return receiveName; }
    public void setReceiveName(String receiveName) { this.receiveName = receiveName; }
    public String getReceivePhone() { return receivePhone; }
    public void setReceivePhone(String receivePhone) { this.receivePhone = receivePhone; }
    public String getReceiveAddress() { return receiveAddress; }
    public void setReceiveAddress(String receiveAddress) { this.receiveAddress = receiveAddress; }
    public Double getWeight() { return weight; }
    public void setWeight(Double weight) { this.weight = weight; }
}

class ZTOResponse {
    private String orderNo;
    private boolean success;

    public ZTOResponse(String orderNo, boolean success) {
        this.orderNo = orderNo;
        this.success = success;
    }
    public String getOrderNo() { return orderNo; }
    public boolean isSuccess() { return success; }
}

class ZTOTrackResponse {
    private String orderNo;
    private String status;
    private String scanTime;

    public ZTOTrackResponse(String orderNo, String status, String scanTime) {
        this.orderNo = orderNo;
        this.status = status;
        this.scanTime = scanTime;
    }
    public String getOrderNo() { return orderNo; }
    public String getStatus() { return status; }
    public String getScanTime() { return scanTime; }
}

JDLogisticsClient(京东)

java 复制代码
package com.dabai.mall.logistics.thirdparty;

import lombok.extern.slf4j.Slf4j;

@Slf4j
public class JDLogisticsClient {

    private final String token;

    public JDLogisticsClient(String token) {
        this.token = token;
    }

    public JDResponse createOrder(JDRequest req) {
        log.info("【京东】创建订单, token={}, orderId={}", token, req.getOrderId());
        return new JDResponse("JD" + System.currentTimeMillis(), 0);
    }

    public String queryTrack(String waybillNo) {
        log.info("【京东】查询轨迹, waybillNo={}", waybillNo);
        return "京东轨迹: 已揽收";
    }
}

class JDRequest {
    private Long orderId;
    private String receiverName;
    private String receiverPhone;
    private String receiverAddress;
    private Double weight;

    public Long getOrderId() { return orderId; }
    public void setOrderId(Long orderId) { this.orderId = orderId; }
    public String getReceiverName() { return receiverName; }
    public void setReceiverName(String receiverName) { this.receiverName = receiverName; }
    public String getReceiverPhone() { return receiverPhone; }
    public void setReceiverPhone(String receiverPhone) { this.receiverPhone = receiverPhone; }
    public String getReceiverAddress() { return receiverAddress; }
    public void setReceiverAddress(String receiverAddress) { this.receiverAddress = receiverAddress; }
    public Double getWeight() { return weight; }
    public void setWeight(Double weight) { this.weight = weight; }
}

class JDResponse {
    private String waybillNo;
    private Integer code;

    public JDResponse(String waybillNo, Integer code) {
        this.waybillNo = waybillNo;
        this.code = code;
    }
    public String getWaybillNo() { return waybillNo; }
    public Integer getCode() { return code; }
}

4.4 适配器实现:3 个 Adapter

ShunFengAdapter(顺丰适配器)

java 复制代码
package com.dabai.mall.logistics.adapter;

import com.dabai.mall.logistics.*;
import com.dabai.mall.logistics.thirdparty.*;

/**
 * ✅ 适配器:顺丰适配器(对象适配器)
 * <p>
 * 核心:把大白商城的统一请求"翻译"成顺丰的请求
 *
 * @author 大白商城技术团队
 */
public class ShunFengAdapter implements LogisticsAdapter {

    private final ShunFengClient shunFengClient;

    public ShunFengAdapter(String appKey) {
        this.shunFengClient = new ShunFengClient(appKey);
    }

    @Override
    public ShipResult ship(ShipRequest request) {
        // ✅ 翻译:统一请求 → 顺丰请求
        ShunFengRequest sfReq = new ShunFengRequest();
        sfReq.setOrderNo(request.getOrderNo());
        sfReq.setCustomerName(request.getReceiverName());
        sfReq.setCustomerPhone(request.getReceiverPhone());
        sfReq.setAddress(request.getReceiverAddress());
        sfReq.setWeight(request.getWeight() == null ? null : request.getWeight().doubleValue());

        // 调用顺丰 SDK
        ShunFengResponse sfResp = shunFengClient.createOrder(sfReq);

        // ✅ 翻译:顺丰响应 → 统一响应
        return ShipResult.builder()
                .success("success".equals(sfResp.getResultCode()))
                .waybillNo(sfResp.getMailNo())
                .build();
    }

    @Override
    public TrackResult track(String waybillNo) {
        ShunFengTrackResponse sfResp = shunFengClient.queryRoute(waybillNo);

        // 翻译轨迹
        return TrackResult.builder()
                .success(true)
                .waybillNo(sfResp.getMailNo())
                .tracks(java.util.List.of(
                        TrackPoint.builder()
                                .time(sfResp.getScanTime())
                                .location("未知")
                                .description(sfResp.getRouteInfo())
                                .build()
                ))
                .build();
    }

    @Override
    public CancelResult cancel(String waybillNo) {
        // 顺丰的取消接口(模拟)
        return CancelResult.builder().success(true).build();
    }

    @Override
    public String getCompanyCode() {
        return "shunfeng";
    }
}

ZTOAdapter(中通适配器)

java 复制代码
package com.dabai.mall.logistics.adapter;

import com.dabai.mall.logistics.*;
import com.dabai.mall.logistics.thirdparty.*;

/**
 * ✅ 适配器:中通适配器
 */
public class ZTOAdapter implements LogisticsAdapter {

    private final ZTOClient ztoClient;

    public ZTOAdapter(String partnerId) {
        this.ztoClient = new ZTOClient(partnerId);
    }

    @Override
    public ShipResult ship(ShipRequest request) {
        // ✅ 翻译:统一 → 中通
        ZTORequest ztoReq = new ZTORequest();
        ztoReq.setOrderId(request.getOrderNo());
        ztoReq.setSendSite("杭州站点");
        ztoReq.setReceiveName(request.getReceiverName());
        ztoReq.setReceivePhone(request.getReceiverPhone());
        ztoReq.setReceiveAddress(request.getReceiverAddress());
        ztoReq.setWeight(request.getWeight() == null ? null : request.getWeight().doubleValue());

        ZTOResponse ztoResp = ztoClient.submitOrder(ztoReq);

        // ✅ 翻译:中通 → 统一
        return ShipResult.builder()
                .success(ztoResp.isSuccess())
                .waybillNo(ztoResp.getOrderNo())
                .build();
    }

    @Override
    public TrackResult track(String waybillNo) {
        ZTOTrackResponse ztoResp = ztoClient.getRoute(waybillNo);
        return TrackResult.builder()
                .success(true)
                .waybillNo(ztoResp.getOrderNo())
                .tracks(java.util.List.of(
                        TrackPoint.builder()
                                .time(ztoResp.getScanTime())
                                .location("未知")
                                .description(ztoResp.getStatus())
                                .build()
                ))
                .build();
    }

    @Override
    public CancelResult cancel(String waybillNo) {
        return CancelResult.builder().success(true).build();
    }

    @Override
    public String getCompanyCode() {
        return "zhongtong";
    }
}

JDAdapter(京东适配器)

java 复制代码
package com.dabai.mall.logistics.adapter;

import com.dabai.mall.logistics.*;
import com.dabai.mall.logistics.thirdparty.*;

/**
 * ✅ 适配器:京东适配器
 */
public class JDAdapter implements LogisticsAdapter {

    private final JDLogisticsClient jdClient;

    public JDAdapter(String token) {
        this.jdClient = new JDLogisticsClient(token);
    }

    @Override
    public ShipResult ship(ShipRequest request) {
        // ✅ 翻译:统一 → 京东
        JDRequest jdReq = new JDRequest();
        jdReq.setOrderId(Long.parseLong(request.getOrderNo().replace("ORDER_", "")));
        jdReq.setReceiverName(request.getReceiverName());
        jdReq.setReceiverPhone(request.getReceiverPhone());
        jdReq.setReceiverAddress(request.getReceiverAddress());
        jdReq.setWeight(request.getWeight() == null ? null : request.getWeight().doubleValue());

        JDResponse jdResp = jdClient.createOrder(jdReq);

        // ✅ 翻译:京东 → 统一
        return ShipResult.builder()
                .success(jdResp.getCode() == 0)
                .waybillNo(jdResp.getWaybillNo())
                .build();
    }

    @Override
    public TrackResult track(String waybillNo) {
        String trackInfo = jdClient.queryTrack(waybillNo);
        return TrackResult.builder()
                .success(true)
                .waybillNo(waybillNo)
                .tracks(java.util.List.of(
                        TrackPoint.builder()
                                .time("2026-07-25 12:00:00")
                                .location("未知")
                                .description(trackInfo)
                                .build()
                ))
                .build();
    }

    @Override
    public CancelResult cancel(String waybillNo) {
        return CancelResult.builder().success(true).build();
    }

    @Override
    public String getCompanyCode() {
        return "jd";
    }
}

4.5 业务调用方:LogisticsService

java 复制代码
package com.dabai.mall.logistics;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

/**
 * ✅ 物流服务:适配器的调用方
 * <p>
 * 业务代码完全干净,只调统一接口
 *
 * @author 大白商城技术团队
 */
@Service
public class LogisticsService {

    /**
     * 公司编码 → 适配器
     */
    private final Map<String, LogisticsAdapter> adapterMap;

    @Autowired
    public LogisticsService(List<LogisticsAdapter> adapters) {
        this.adapterMap = adapters.stream()
                .collect(Collectors.toMap(LogisticsAdapter::getCompanyCode, a -> a));
        log.info("【LogisticsService】注册 {} 个物流适配器: {}",
                adapterMap.size(), adapterMap.keySet());
    }

    /**
     * 发货
     */
    public ShipResult ship(ShipRequest request) {
        // 关键:业务方只调统一接口
        // 找到对应的适配器(业务方不需要知道是哪家)
        LogisticsAdapter adapter = adapterMap.get(request.getLogisticsCompany());
        if (adapter == null) {
            throw new BusinessException("不支持的物流公司: " + request.getLogisticsCompany());
        }
        return adapter.ship(request);
    }

    /**
     * 查询轨迹
     */
    public TrackResult track(String waybillNo, String company) {
        LogisticsAdapter adapter = adapterMap.get(company);
        if (adapter == null) {
            throw new BusinessException("不支持的物流公司: " + company);
        }
        return adapter.track(waybillNo);
    }
}

关键点

关键点 解释
List<LogisticsAdapter> Spring 自动注入所有实现
Map<String, LogisticsAdapter> 按公司编码索引
业务代码只调 adapter.ship() 完全不知道是哪家公司

加新物流公司 3 步走

复制代码
第 1 步:新建 XAdapter implements LogisticsAdapter
第 2 步:把 XAdapter 标 @Component
第 3 步:完事。LogisticsService 一行不改。

4.6 单元测试

java 复制代码
package com.dabai.mall.logistics;

import com.dabai.mall.logistics.adapter.JDAdapter;
import com.dabai.mall.logistics.adapter.ShunFengAdapter;
import com.dabai.mall.logistics.adapter.ZTOAdapter;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import java.math.BigDecimal;
import java.util.List;

import static org.junit.jupiter.api.Assertions.*;

/**
 * 适配器完整单元测试
 */
class LogisticsAdapterTest {

    private LogisticsService logisticsService;

    @BeforeEach
    void setUp() {
        // 注册 3 个适配器
        logisticsService = new LogisticsService(List.of(
                new ShunFengAdapter("sf_app_key"),
                new ZTOAdapter("zto_partner_id"),
                new JDAdapter("jd_token")
        ));
    }

    @Test
    void testShunFengShip() {
        ShipRequest request = ShipRequest.builder()
                .orderNo("ORDER_001")
                .receiverName("张三")
                .receiverPhone("13800138000")
                .receiverAddress("北京市朝阳区")
                .weight(new BigDecimal("2.5"))
                .logisticsCompany("shunfeng")
                .build();

        ShipResult result = logisticsService.ship(request);

        assertTrue(result.isSuccess());
        assertNotNull(result.getWaybillNo());
        assertTrue(result.getWaybillNo().startsWith("SF"));
    }

    @Test
    void testZTOShip() {
        ShipRequest request = ShipRequest.builder()
                .orderNo("ORDER_002")
                .receiverName("李四")
                .receiverPhone("13800138001")
                .receiverAddress("上海市浦东")
                .weight(new BigDecimal("1.0"))
                .logisticsCompany("zhongtong")
                .build();

        ShipResult result = logisticsService.ship(request);

        assertTrue(result.isSuccess());
        assertTrue(result.getWaybillNo().startsWith("ZTO"));
    }

    @Test
    void testJDShip() {
        ShipRequest request = ShipRequest.builder()
                .orderNo("ORDER_003")
                .receiverName("王五")
                .receiverPhone("13800138002")
                .receiverAddress("广州市天河")
                .weight(new BigDecimal("3.0"))
                .logisticsCompany("jd")
                .build();

        ShipResult result = logisticsService.ship(request);

        assertTrue(result.isSuccess());
        assertTrue(result.getWaybillNo().startsWith("JD"));
    }

    @Test
    void testUnsupportedCompany() {
        ShipRequest request = ShipRequest.builder()
                .orderNo("ORDER_004")
                .receiverName("测试")
                .receiverPhone("13800138000")
                .receiverAddress("测试地址")
                .logisticsCompany("unsupported")
                .build();

        assertThrows(BusinessException.class, () -> logisticsService.ship(request));
    }

    @Test
    void testTrack() {
        ShipRequest request = ShipRequest.builder()
                .orderNo("ORDER_005")
                .receiverName("测试")
                .logisticsCompany("shunfeng")
                .build();

        ShipResult shipResult = logisticsService.ship(request);
        TrackResult trackResult = logisticsService.track(shipResult.getWaybillNo(), "shunfeng");

        assertNotNull(trackResult);
        assertNotNull(trackResult.getTracks());
        assertFalse(trackResult.getTracks().isEmpty());
    }
}

五、3 种适配器写法对比

5.1 类适配器(继承)

java 复制代码
/**
 * ❌ 类适配器:Adapter extends Adaptee
 * <p>
 * 问题:Java 单继承,Adapter 不能再继承其他
 */
public class ShunFengClassAdapter extends ShunFengClient implements LogisticsAdapter {

    public ShunFengClassAdapter() {
        super("app_key");
    }

    @Override
    public ShipResult ship(ShipRequest request) {
        // 直接调父类(继承)
        ShunFengRequest sfReq = new ShunFengRequest();
        // ...
        ShunFengResponse sfResp = super.createOrder(sfReq);
        // ...
    }

    @Override
    public TrackResult track(String waybillNo) {
        return null;
    }

    @Override
    public CancelResult cancel(String waybillNo) {
        return null;
    }

    @Override
    public String getCompanyCode() {
        return "shunfeng";
    }
}

问题

问题 解释
单继承 Adapter 不能再继承其他
紧耦合 Adapter 强依赖 Adaptee
难测试 Adaptee 的逻辑也跟着走

5.2 对象适配器(组合)✅ 推荐

java 复制代码
/**
 * ✅ 对象适配器:Adapter 持有 Adaptee
 * <p>
 * 灵活、推荐
 */
public class ShunFengAdapter implements LogisticsAdapter {

    private final ShunFengClient shunFengClient;  // 组合

    public ShunFengAdapter(String appKey) {
        this.shunFengClient = new ShunFengClient(appKey);
    }

    @Override
    public ShipResult ship(ShipRequest request) {
        // 调持有的 Adaptee
        ShunFengResponse sfResp = shunFengClient.createOrder(sfReq);
        // ...
    }
}

优势

优势 解释
灵活 可以组合任意 Adaptee
弱耦合 Adapter 和 Adaptee 通过接口组合
易测试 Adaptee 可以 Mock

5.3 接口适配器(抽象类)

java 复制代码
/**
 * ✅ 接口适配器:当目标接口有很多方法时,只关心部分方法
 */
public abstract class AbstractLogisticsAdapter implements LogisticsAdapter {

    // 把 LogisticsAdapter 的方法都空实现
    @Override
    public ShipResult ship(ShipRequest request) {
        throw new UnsupportedOperationException("ship not supported");
    }

    @Override
    public TrackResult track(String waybillNo) {
        throw new UnsupportedOperationException("track not supported");
    }

    @Override
    public CancelResult cancel(String waybillNo) {
        throw new UnsupportedOperationException("cancel not supported");
    }

    @Override
    public String getCompanyCode() {
        return "abstract";
    }
}

// 只想实现 ship 方法
public class OnlyShipAdapter extends AbstractLogisticsAdapter {

    @Override
    public ShipResult ship(ShipRequest request) {
        // 只实现这个方法,其他继承空实现
        return new ShipResult();
    }
}

适用场景

场景 适用 原因
目标接口有 10+ 方法 避免空实现所有方法
只想实现部分方法 抽象类提供空实现
多 Adapter 共享部分逻辑 抽象类抽取公共代码

5.4 3 种写法对比表

维度 类适配器 对象适配器 接口适配器
实现方式 继承 组合 抽象类 + 继承
灵活性
紧耦合
适用场景 简单 通用 多个方法部分实现
推荐度 ⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐⭐

大白商城主推对象适配器


六、适配器 vs 装饰器 / 代理 / 桥接

6.1 4 个模式对比

模式 关注点 是否改变接口 是否包装自己 适用场景
适配器 翻译接口 ✅ 改变 ❌ 包装第三方 对接不兼容接口
装饰器 增强功能 ❌ 不变 ✅ 包装自己 动态添加职责
代理 控制访问 ❌ 不变 ✅ 包装自己 权限/远程/延迟
桥接 抽象分离 ✅ 改变 ❌ 拆分实现 抽象和实现分离

6.2 一个例子区分 4 个模式

大白商城订单

模式 例子
适配器 把顺丰/中通/京东 API 包装成统一物流接口
装饰器 给订单 VO 包装价格计算(满减/折扣/运费)
代理 Spring AOP 给 Service 包装日志/权限/事务
桥接 订单状态机把"状态"和"行为"分离

6.3 决策树

复制代码
你包装的是"第三方类"还是"自己的类"?
├── 第三方类
│   └── ✅ 适配器
└── 自己的类
    ├── 改变接口?
    │   ├── 是 → ✅ 桥接
    │   └── 否
    │       ├── 增强功能?
    │       │   ├── 是 → ✅ 装饰器
    │       │   └── 否
    │       │       └── 控制访问?
    │       │           ├── 是 → ✅ 代理
    │       │           └── 否 → ❌ 不用模式

七、Spring MVC 的 HandlerAdapter 怎么借鉴?

7.1 HandlerAdapter 是什么?

Spring MVC 处理请求的"翻译官"

复制代码
HTTP 请求
  ↓
DispatcherServlet
  ↓
HandlerAdapter(适配器)
  ↓
- RequestMappingHandlerAdapter:处理 @RequestMapping
- HttpRequestHandlerAdapter:处理 HttpRequestHandler
- SimpleControllerHandlerAdapter:处理 Controller 接口
- SimpleServletHandlerAdapter:处理 Servlet

每个 HandlerAdapter 都是"翻译官"------把 HTTP 请求翻译成不同类型的 Controller 能处理的形式。

7.2 完整流程

复制代码
HTTP 请求 → DispatcherServlet
        ↓
遍历所有 HandlerAdapter
        ↓
找到 supports() 返回 true 的 Adapter
        ↓
调用 handler()
        ↓
执行真正的 Controller 方法
        ↓
返回 ModelAndView

7.3 大白商城的"抄作业"

java 复制代码
/**
 * 借鉴 Spring HandlerAdapter:统一消息处理
 */
public interface MessageHandlerAdapter<T> {

    /**
     * 是否支持该消息类型
     */
    boolean supports(Object message);

    /**
     * 处理消息
     */
    Object handle(T message);
}

/**
 * 文本消息处理
 */
@Component
public class TextMessageHandler implements MessageHandlerAdapter<String> {

    @Override
    public boolean supports(Object message) {
        return message instanceof String;
    }

    @Override
    public Object handle(String message) {
        return "处理文本: " + message;
    }
}

/**
 * 订单消息处理
 */
@Component
public class OrderMessageHandler implements MessageHandlerAdapter<Order> {

    @Override
    public boolean supports(Object message) {
        return message instanceof Order;
    }

    @Override
    public Object handle(Order message) {
        return "处理订单: " + message.getId();
    }
}

调用方

java 复制代码
@Service
public class MessageDispatcher {

    private final List<MessageHandlerAdapter<?>> adapters;

    public MessageDispatcher(List<MessageHandlerAdapter<?>> adapters) {
        this.adapters = adapters;
    }

    public Object dispatch(Object message) {
        return adapters.stream()
                .filter(adapter -> adapter.supports(message))
                .findFirst()
                .map(adapter -> adapter.handle(message))
                .orElseThrow(() -> new RuntimeException("无处理器: " + message.getClass()));
    }
}

收益

优化点 效果
统一调用 业务方只调 dispatch(message)
自动选择 根据消息类型自动选 Adapter
加新类型 新建 Adapter,dispatcher 不动

八、工程决策 Checklist

8.1 ✅ 这 5 种情况,强烈建议用适配器

序号 场景 原因
对接第三方 API 接口不兼容
老系统集成 老接口无法改
多渠道统一 物流/支付/短信
统一消息处理 不同类型消息不同处理
Spring MVC HandlerAdapter 标准做法

8.2 ❌ 这 5 种情况,绝对不要用适配器

序号 场景 原因
可以改第三方代码 直接改更简单
只有 1 个实现 不需要适配
追求极致性能 适配器有调用开销
接口差异小 直接重写更简单
业务逻辑复杂 适配器只翻译不加工

8.3 ⚠️ 适配器的 6 大常见坑

序号 表现 解决方案
适配器过多 100 个 Adapter 类 抽象公共逻辑
翻译逻辑错误 字段映射错 单元测试覆盖
异常被吃掉 第三方异常丢失 统一异常处理
线程不安全 Adapter 持有可变状态 Adapter 不可变
性能损耗 适配器有调用开销 缓存适配器
不支持新公司 if-else 没写 Spring 自动注入

8.4 面试官视角:适配器高频追问

Q1:适配器 vs 装饰器,区别是什么?

答:适配器改变接口 (翻译),包装第三方 ;装饰器不改变接口 (增强),包装自己。适配器侧重"兼容",装饰器侧重"增强"。

Q2:类适配器 vs 对象适配器,怎么选?

答:优先对象适配器 (组合)。类适配器受 Java 单继承限制,紧耦合。组合优于继承

Q3:Spring MVC 的 HandlerAdapter 怎么工作的?

答:每个 HandlerAdapter 是"翻译官",把 HTTP 请求翻译成不同 Controller 能处理的形式。DispatcherServlet 遍历所有 Adapter,找到 supports() 为 true 的那个,调用 handle()。


九、与其他模式协作

9.1 适配器 + 工厂方法 = 工厂创建适配器

java 复制代码
/**
 * 适配器 + 工厂:按公司编码创建适配器
 */
public class LogisticsAdapterFactory {

    public static LogisticsAdapter create(String companyCode) {
        return switch (companyCode) {
            case "shunfeng" -> new ShunFengAdapter("app_key");
            case "zhongtong" -> new ZTOAdapter("partner_id");
            case "jd" -> new JDAdapter("token");
            default -> throw new IllegalArgumentException("不支持: " + companyCode);
        };
    }
}

9.2 适配器 + 策略 = 多渠道动态切换

java 复制代码
/**
 * 适配器 + 策略:物流策略
 */
@Service
public class LogisticsStrategy {

    private final Map<String, LogisticsAdapter> adapterMap;

    public LogisticsStrategy(List<LogisticsAdapter> adapters) {
        this.adapterMap = adapters.stream()
                .collect(Collectors.toMap(LogisticsAdapter::getCompanyCode, a -> a));
    }

    public LogisticsAdapter getStrategy(String company) {
        return adapterMap.get(company);
    }
}

9.3 适配器 + 外观 = 统一门面

java 复制代码
/**
 * 适配器 + 外观:物流门面
 */
@Service
public class LogisticsFacade {

    @Autowired
    private LogisticsService logisticsService;

    /**
     * 统一发货
     */
    public ShipResult ship(Order order) {
        // 把订单转换为发货请求
        ShipRequest request = ShipRequest.builder()
                .orderNo(order.getOrderNo())
                .receiverName(order.getReceiverName())
                // ...
                .build();
        return logisticsService.ship(request);
    }
}

9.4 大白商城模式协作全景图

复制代码
                    ┌──────────────┐
                    │   适配器     │ ← 本篇
                    └──────┬───────┘
                           │
       ┌───────────┬───────┼───────┬───────────┐
       │           │       │       │           │
   ┌───▼───┐  ┌────▼───┐ ┌▼────┐ ┌▼─────┐  ┌───▼────┐
   │工厂方法│  │ 策略   │ │外观 │ │装饰器│  │ 桥接   │
   │(创建) │  │(切换) │ │(门面)│ │(增强)│  │(分离) │
   └───────┘  └────────┘ └──────┘ └──────┘  └───────┘
   04 篇       25 篇     17 篇     15 篇     21 篇

十、本篇小结 + 下篇预告

10.1 本篇小结(5 个核心要点)

  1. 本质 :适配器 = 翻译官,让不兼容接口能一起工作,对老代码最友好
  2. 场景 :第三方对接 / 老系统集成 / 多渠道统一,都该用适配器
  3. 对象适配器 :组合优于继承,首选
  4. Spring MVC:HandlerAdapter 是适配器模式的工业级实现。
  5. 避坑:适配器过多 / 翻译逻辑错误 / 异常被吃掉。

10.2 一句话总结

适配器不是"换种方式调方法",是"在不动第三方代码的前提下让它们兼容"。大白商城 5 家物流公司对接,从 5 套 if-else 变成 5 个 Adapter + 1 个统一接口------业务代码完全不知道有几家物流公司。

10.3 知识脑图

复制代码
适配器模式
├── 4 大角色
│   ├── 目标接口(LogisticsAdapter)
│   ├── 被适配者(顺丰/中通/京东 SDK)
│   ├── 适配器(ShunFengAdapter / ZTOAdapter / JDAdapter)
│   └── 客户端(LogisticsService)
├── 3 种写法
│   ├── 类适配器(继承)❌
│   ├── 对象适配器(组合)✅
│   └── 接口适配器(抽象类)⚠️
├── 模式对比
│   ├── vs 装饰器(翻译 vs 增强)
│   ├── vs 代理(翻译 vs 控制)
│   └── vs 桥接(翻译 vs 分离)
├── Spring HandlerAdapter
│   ├── RequestMappingHandlerAdapter
│   ├── HttpRequestHandlerAdapter
│   └── SimpleControllerHandlerAdapter
└── 模式协作
    ├── + 工厂方法(创建)
    ├── + 策略(动态切换)
    └── + 外观(统一门面)

10.4 下篇预告

第 15 篇【适配器模式 - 源码剖析篇】:JDK / Spring 中的适配器实现

下一篇我们会深入源码,回答三个问题:

  1. JDK 的 InputStreamReader / OutputStreamWriter 怎么用适配器包装字节流和字符流?
  2. Spring MVC 的 HandlerAdapter 怎么实现"多 Controller 统一处理"?
  3. MyBatis 的 Log 接口怎么用适配器兼容多种日志框架?

并附完整的源码解读 + 流程图 + 大白商城的"抄作业"实践。


觉得对您有帮助,麻烦 点点关注啦 ,您的关注是我创作的最大动力~ 🎯

相关推荐
xbgRS5 小时前
springboot的自动装配
java·spring boot
略略略咯咯11 小时前
(总结)设计模式
设计模式
嘻哈∠※12 小时前
0061基于 SpringBoot 的投稿与稿件处理系统设计与实现
java·spring boot·后端
白狐_79813 小时前
408数据结构第8章:排序②——性质对比秒杀、场景选择与外部排序
java·数据结构·算法
zander25813 小时前
LeetCode 84:柱状图中的最大矩形——单调栈如何确定左右边界
java·数据结构·算法
码匠许师傅14 小时前
【C++ 面试真题】聊聊 C++ 的拷贝构造与拷贝赋值
java·c++·面试
极创信息15 小时前
系统安全隐患全方位排查方案:标准化渗透测试全流程
java·opencv·struts·数据挖掘·eclipse·语音识别·hibernate
汉字萌萌哒16 小时前
2019CCF-CSP入门级C++试题解析
java·开发语言·c++
喜欢的名字被抢了16 小时前
程序出问题怎么查,以及如何让它不掉线
java·运维·数据库