适配器模式完全指南:从插头类比到支付网关接入的工程实战

适配器模式是 23 个 GoF 模式里使用频率最高的之一,但它常被低估为"换个接口的小技巧"。实际上"系统集成、第三方库接入、遗留代码兼容"这些工程里最棘手的问题,大多数都靠适配器解决。这篇文章把适配器从原理讲到 Stripe / Slack / 老 API 升级的真实场景,讲清楚它与代理、装饰器、桥接的边界。

适配器要解决的根本问题

电源插头是最直观的类比:中国插头插不进美标插座,你不是把墙拆了改插座,也不是把电器拆了改插头 —— 而是中间放一个"适配头"。软件世界里的对应物:

  • 你的代码已经调用某个接口(client 侧);
  • 有一个东西提供你需要的能力但接口长得不一样(adaptee 侧);
  • 你不能或不想改任何一边,于是写一个"中间层"把后者包装成前者的样子。

就这么简单。但应用场景广得惊人:Java 集合 API、SLF4J 桥接、Stripe / 支付宝接入、跨数据库适配、新旧版本协议兼容,本质都是适配器。

类适配器 vs 对象适配器

GoF 列了两种实现:

类适配器(用继承)

// 已有的不兼容接口
class LegacyPrinter {
    public void printOldStyle(String text) {
        System.out.println("OLD: " + text);
    }
}

// 期望的新接口
interface NewPrinter {
    void print(String text);
}

// 类适配器:继承 + 实现
class LegacyAdapter extends LegacyPrinter implements NewPrinter {
    @Override
    public void print(String text) {
        printOldStyle(text);
    }
}

// 使用
NewPrinter p = new LegacyAdapter();
p.print("hello");

问题:Java 单继承,如果 adaptee 已经有父类就用不了类适配器。所以工程上几乎都用对象适配器。

对象适配器(用组合,推荐)

class LegacyAdapter implements NewPrinter {
    private final LegacyPrinter legacy;
    public LegacyAdapter(LegacyPrinter legacy) { this.legacy = legacy; }

    @Override
    public void print(String text) {
        legacy.printOldStyle(text);
    }
}

NewPrinter p = new LegacyAdapter(new LegacyPrinter());
p.print("hello");

用组合不受单继承限制,且能在运行时换 adaptee。对象适配器是默认选择,类适配器只在极少数场景考虑

实战 1:支付网关统一接入

典型场景:产品要支持支付宝、微信、Stripe、PayPal 四种支付。它们的 SDK 接口风格差异极大:

// 支付宝(假想 SDK)
AlipayClient client = new DefaultAlipayClient(...);
AlipayTradePrecreateResponse resp = client.execute(request);
String code = resp.getCode();
if ("10000".equals(code)) { /* 成功 */ }

// 微信支付
WechatPayClient wxClient = WechatPayClient.builder()...build();
TradeResult result = wxClient.createOrder(orderInfo);
if (result.isSuccess()) { /* 成功 */ }

// Stripe
Stripe.apiKey = "sk_test_...";
PaymentIntent intent = PaymentIntent.create(Map.of("amount", 1000, "currency", "usd"));
String status = intent.getStatus();    // "succeeded" / "requires_action" ...

// PayPal
PayPalHttpClient ppClient = new PayPalHttpClient(env);
HttpResponse<Order> orderResp = ppClient.execute(new OrdersCreateRequest()...);

业务代码不应该直面四套不同的 API,否则下单逻辑会被分支撕碎。设计一个统一接口,然后给每个支付商写适配器:

// 1. 统一抽象
public interface PaymentGateway {
    PaymentResult charge(PaymentRequest request);
    PaymentResult refund(String paymentId, BigDecimal amount);
    PaymentStatus query(String paymentId);
}

public class PaymentRequest {
    String orderId; BigDecimal amount; String currency; String userId;
    // ...
}

public class PaymentResult {
    boolean success; String paymentId; String errorCode; String errorMessage;
}

// 2. 适配器:支付宝
public class AlipayAdapter implements PaymentGateway {
    private final AlipayClient client;

    public PaymentResult charge(PaymentRequest req) {
        AlipayTradePrecreateRequest aliReq = new AlipayTradePrecreateRequest();
        aliReq.setBizContent(toJson(Map.of(
            "out_trade_no", req.orderId,
            "total_amount", req.amount.toString(),
            "subject", "订单 " + req.orderId
        )));
        try {
            AlipayTradePrecreateResponse resp = client.execute(aliReq);
            return "10000".equals(resp.getCode())
                ? PaymentResult.ok(resp.getTradeNo())
                : PaymentResult.fail(resp.getCode(), resp.getMsg());
        } catch (Exception e) {
            return PaymentResult.fail("network", e.getMessage());
        }
    }
    // refund、query 类似
}

// 3. 适配器:Stripe
public class StripeAdapter implements PaymentGateway {
    public PaymentResult charge(PaymentRequest req) {
        try {
            PaymentIntent intent = PaymentIntent.create(Map.of(
                "amount", req.amount.movePointRight(2).intValue(),  // 转分
                "currency", req.currency.toLowerCase(),
                "metadata", Map.of("order_id", req.orderId)
            ));
            return PaymentResult.ok(intent.getId());
        } catch (StripeException e) {
            return PaymentResult.fail(e.getCode(), e.getMessage());
        }
    }
}

// 4. 业务代码只面对统一接口
@Service
public class OrderService {
    private final Map<String, PaymentGateway> gateways;   // alipay / wechat / stripe / paypal

    public void placeOrder(Order order) {
        PaymentGateway gw = gateways.get(order.getPaymentChannel());
        PaymentResult r = gw.charge(toRequest(order));
        if (r.isSuccess()) saveSuccess(order, r);
        else handleFail(order, r);
    }
}

这套设计的威力:新加一个支付商 = 写一个新适配器,业务代码一行不动。某个支付商接口升级 = 改一个适配器内部的实现,对外契约不变,业务零感知。

实战 2:日志门面 SLF4J 的桥接器

Java 生态有 log4j、log4j2、java.util.logging、Logback、commons-logging 五六个日志实现。如果你的库直接依赖某一个,用你库的人就被绑定到那个实现。SLF4J 提供"日志门面"+"桥接器",经典适配器:

// 你的库代码只依赖 SLF4J API
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class MyLib {
    private static final Logger log = LoggerFactory.getLogger(MyLib.class);
    public void doWork() { log.info("working"); }
}

// 用户根据自己的实际环境引入"桥接"依赖,把 SLF4J 适配到具体实现
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-log4j12</artifactId>     <!-- 桥接到 log4j -->
</dependency>

// 或换成 logback:
<dependency>
    <groupId>ch.qos.logback</groupId>
    <artifactId>logback-classic</artifactId>
</dependency>

反向桥接(老代码用了 log4j API,系统用 Logback)也有:log4j-over-slf4j 把 log4j 调用转到 SLF4J,SLF4J 再桥接到 Logback。整个 Java 日志生态就是一个巨大的适配器网。

实战 3:数据格式转换

// 老 API 返回的对象
class LegacyUserDTO {
    public String first_name;
    public String last_name;
    public Long birth_timestamp;
    public Integer status_code;        // 1=active 2=inactive 3=banned
}

// 新业务需要的领域对象
class User {
    private String fullName;
    private LocalDate birthDate;
    private UserStatus status;        // 枚举:ACTIVE / INACTIVE / BANNED
}

// 适配器
class LegacyUserAdapter {
    public static User toUser(LegacyUserDTO dto) {
        User u = new User();
        u.setFullName(dto.first_name + " " + dto.last_name);
        u.setBirthDate(LocalDate.ofInstant(
            Instant.ofEpochMilli(dto.birth_timestamp), ZoneId.systemDefault()));
        u.setStatus(switch (dto.status_code) {
            case 1 -> UserStatus.ACTIVE;
            case 2 -> UserStatus.INACTIVE;
            case 3 -> UserStatus.BANNED;
            default -> throw new IllegalArgumentException();
        });
        return u;
    }
}

这种"DTO 到领域对象"的转换是 DDD 实践里的常见适配器。MapStruct、ModelMapper 这些库就是为了减少这类样板代码而生 —— 它们用注解生成适配器,你只写转换规则不写 boilerplate。

适配器 vs 代理 vs 装饰器 vs 外观

这四个模式都是"包装一层",但目的不同:

  • 适配器(Adapter):目标是改接口。被适配者的接口 A,客户端期望接口 B,适配器实现 B 调用 A。
  • 代理(Proxy):目标是控制访问(权限、缓存、延迟加载、远程调用)。代理和被代理对象接口相同
  • 装饰器(Decorator):目标是叠加新行为(加日志、加缓存、加压缩)。装饰器和被装饰对象接口相同,可以嵌套多层。
  • 外观(Facade):目标是简化使用。外观提供一个更简单的接口,内部协调多个复杂对象。

记忆:接口变了 → 适配器;接口没变,行为差不多但加了限制 → 代理;接口没变,行为增强了 → 装饰器;面对一群对象想给个简单入口 → 外观

双向适配器

有时候你需要"A 看 B 是 X 接口,B 看 A 是 Y 接口"的双向适配。例:

class CelsiusFahrenheitAdapter implements Celsius, Fahrenheit {
    private double temp;
    public double inCelsius()    { return temp; }
    public double inFahrenheit() { return temp * 9 / 5 + 32; }
    public void   setCelsius(double c)    { this.temp = c; }
    public void   setFahrenheit(double f) { this.temp = (f - 32) * 5 / 9; }
}

双向适配在 ORM 字段双向映射、UI 控件双向绑定里常见。但它增加了复杂度,只在两边都需要互相调用时才用。

各语言里的适配器

Python:用鸭子类型让适配器变得轻量

class LegacyPrinter:
    def print_old_style(self, text):
        print(f"OLD: {text}")

# Python 不需要显式实现接口,函数签名对得上就是"实现"
class PrinterAdapter:
    def __init__(self, legacy):
        self.legacy = legacy
    def print(self, text):
        self.legacy.print_old_style(text)

# 更简单:函数适配
def adapt(legacy):
    return lambda text: legacy.print_old_style(text)

printer = adapt(LegacyPrinter())
printer("hello")

TypeScript:类型适配

// 第三方库的回调签名
type LegacyCallback = (err: Error | null, data: any) => void;

// 你想用 Promise
function promisify<T>(fn: (cb: LegacyCallback) => void): Promise<T> {
    return new Promise((resolve, reject) => {
        fn((err, data) => err ? reject(err) : resolve(data));
    });
}

const data = await promisify(cb => legacyAPI.fetch(cb));
// Node.js 内置的 util.promisify 就是这个

常见坑

坑 1:适配器里塞业务逻辑。 适配器的职责是"接口转换",不是"业务变换"。把"查询订单后顺便更新缓存"塞进适配器是把它变成上帝类。守住边界:适配器只做格式 / 协议 / 调用方式的桥接。

坑 2:适配器吞异常。 Legacy API 抛出的异常被适配器 catch 后转成 null 返回,调用方完全不知道出了什么事。规范:适配器要么把异常按统一接口要求重新抛出,要么转成 Result 对象 —— 但绝不能静默吞掉。

坑 3:接口"演化"导致适配器爆炸。 第三方 API v1 / v2 / v3 都还在用,你写了三个适配器。新版本出了 v4,要写第四个。这时考虑用"版本路由 + 单适配器内部分支",或者升级到 anti-corruption layer(ACL)模式。

坑 4:用适配器掩盖坏 API 设计。 内部 API 设计得很难用,然后用适配器把它包装成好用的。这是治标 —— 真正的修复是改 API 本身,适配器只是中间过渡。

写在最后

适配器模式的"低调"恰恰是它的力量。它不解决任何"业务问题",但它让你的系统能对内保持干净的抽象,对外兼容现实世界各种乱七八糟的接口。在做系统集成、技术栈迁移、第三方服务接入时,适配器几乎是必不可少的工具。

给一个识别信号:当你的业务代码里 if/switch 不同的 SDK 调用方式,或者反复写 try/catch 转换异常,或者频繁手写 DTO → Entity 的转换时 —— 适配器在向你招手。把这些胶水代码抽出来放到适配器层,业务代码会立刻变干净,新接入第三方时改动范围会缩小到可控。这是设计模式里 ROI 极高的一个。

—— 别看了 · 2026
声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理 邮箱1846861578@qq.com。
技术教程

原型模式完全指南:从浅克隆到深拷贝再到不可变共享

2026-5-15 11:50:57

技术教程

桥接模式完全指南:从笛卡尔积爆炸到 N+M 的优雅设计

2026-5-15 11:50:57

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索