第41章:设计模式面试题6:实现一个适配器模式,适配第三方库接口

适配器模式,说白了就是「让不兼容的东西能一起工作」。

我面试的时候特别喜欢问这个模式。为什么?因为它太实用了。你想想看,哪个项目里没集成过第三方库?哪个第三方库的接口能完全符合你的预期?

适配器模式就是干这个活的——它把第三方库的接口,转换成你系统里期望的接口。

一个真实的场景

我记得几年前做一个支付系统。公司决定接入一个新的支付渠道,对方提供了SDK。但问题来了——我们系统内部定义了一套统一的支付接口,而第三方SDK的方法签名、参数顺序、返回值格式,全都不一样。

改我们自己的接口?不现实,因为已经对接了好几个渠道。改第三方SDK?更不可能。那怎么办?

适配器模式就是标准答案。

适配器模式的核心结构

适配器模式其实就三个角色:

  • 目标接口(Target):你系统里期望的接口
  • 适配者(Adaptee):第三方库的接口
  • 适配器(Adapter):中间的转换层

我习惯用类适配器和对象适配器两种方式。类适配器用多重继承,对象适配器用组合。C++里我推荐对象适配器,更灵活,耦合更低。

核心原则:适配器不改变原有接口,只做转换。别想着去改第三方库的代码,那是给自己挖坑。

代码示例:对象适配器

假设我们系统里定义了一个支付接口:

// 目标接口:我们系统期望的支付方式
class IPayment {
public:
    virtual ~IPayment() = default;
    virtual bool pay(const std::string& orderId, double amount) = 0;
    virtual std::string getStatus(const std::string& orderId) = 0;
};

现在来了一个第三方支付SDK,它的接口长这样:

// 第三方支付SDK(适配者)
class ThirdPartyPayment {
public:
    // 注意:参数顺序不同,返回类型也不同
    int submitPayment(double amount, const std::string& transactionId) {
        // 实际调用第三方API...
        return 200; // 返回HTTP状态码
    }
    
    std::string queryResult(const std::string& transactionId) {
        // 返回"success"或"failed"
        return "success";
    }
};

你看,参数顺序反了,返回类型也不一样。直接替换?不行。写适配器:

// 适配器:把第三方接口转换成我们系统的接口
class PaymentAdapter : public IPayment {
private:
    ThirdPartyPayment* m_thirdParty; // 组合方式持有第三方对象
    
public:
    PaymentAdapter(ThirdPartyPayment* thirdParty) 
        : m_thirdParty(thirdParty) {}
    
    bool pay(const std::string& orderId, double amount) override {
        // 转换:调整参数顺序,转换返回值
        int resultCode = m_thirdParty->submitPayment(amount, orderId);
        return resultCode == 200;
    }
    
    std::string getStatus(const std::string& orderId) override {
        std::string rawStatus = m_thirdParty->queryResult(orderId);
        // 转换:把第三方返回的字符串映射成我们系统定义的枚举或状态码
        if (rawStatus == "success") {
            return "PAID";
        }
        return "FAILED";
    }
};

使用起来就很简单了:

void processOrder(IPayment* payment, const std::string& orderId, double amount) {
    if (payment->pay(orderId, amount)) {
        std::cout << "支付成功" << std::endl;
    }
}

int main() {
    ThirdPartyPayment thirdParty;
    PaymentAdapter adapter(&thirdParty);
    
    processOrder(&adapter, "ORDER_001", 99.99);
    return 0;
}

适配器模式的核心逻辑图

适配器模式结构图 客户端 目标接口 IPayment 适配器 PaymentAdapter 第三方库 ThirdPartyPayment 调用 实现 组合/调用 客户端只依赖目标接口,不直接依赖第三方库 适配器负责接口转换,隔离第三方变化

避坑指南

我曾经在一个项目里踩过坑。当时接手一个老系统,里面直接调用了第三方库的API,散落在各个业务代码里。后来第三方库升级,接口变了,整个系统改得鸡飞狗跳。

如果一开始就用适配器模式,只需要改适配器这一个地方就行。这就是适配器模式最大的价值——隔离变化

注意:适配器模式不是万能的。如果第三方库的接口变化太频繁,或者语义差异太大,适配器会变得很臃肿。这时候可以考虑用门面模式(Facade)做更高层次的封装。

类适配器 vs 对象适配器

对比维度 类适配器(多重继承) 对象适配器(组合)
实现方式 同时继承Target和Adaptee 继承Target,持有Adaptee对象
灵活性 较低,编译期绑定 较高,运行时可以切换Adaptee
耦合度 高,直接依赖具体类 低,依赖接口或抽象类
适用场景 Adaptee接口固定,且不需要额外适配 大多数实际项目

我个人几乎只用对象适配器。为什么?因为C++的多重继承容易带来歧义,而且组合比继承更灵活。你想想看,如果第三方库换了,我只需要换一个Adaptee对象,适配器代码基本不用动。

面试官会怎么问

面试时我一般会这样追问:

  • 「如果第三方库有多个版本,接口不同,你怎么处理?」——可以用策略模式+适配器,每个版本写一个适配器,运行时选择。
  • 「适配器模式和代理模式有什么区别?」——适配器侧重接口转换,代理侧重控制访问。适配器改变接口,代理不改变接口。
  • 「适配器模式会影响性能吗?」——理论上多了一层调用,但现代编译器优化后几乎无感。除非在极高频调用的场景,否则不用担心。

我的建议:在项目初期就定义好接口抽象层。哪怕一开始只对接一个第三方库,也先写适配器。这个习惯帮我省了无数重构的时间。

适配器模式其实不难,难的是什么时候用、怎么用得优雅。记住一句话:不要让你的业务代码直接依赖第三方库。中间加一层适配器,未来你会感谢自己的。


公众号:蓝海资料掘金营,微信deep3321