从ISP到适配器模式:接口不兼容的终极解决方案

各位好,今天我们来聊聊一个非常实用的话题——适配器模式。说实话,这个模式是我在实际项目中用得最多的设计模式之一。为什么?因为现实世界中的接口从来不会按照你的理想来设计。

先从一个真实场景说起。几年前我接手一个遗留系统,需要对接第三方支付SDK。对方的接口长这样:bool Pay(string orderId, double amount)。而我们系统内部已经有一套成熟的支付抽象:class IPayment { virtual void ProcessPayment(const PaymentRequest& req) = 0; }。你想想看,这俩接口完全不搭边。怎么办?改第三方库?不可能。改自己系统里几十个调用点?那是给自己挖坑。

这时候,适配器模式就派上用场了。

接口隔离原则(ISP)与适配器的关系

在讲适配器之前,得先提一下接口隔离原则(ISP)。ISP告诉我们:客户端不应该被迫依赖它不使用的接口。说白了,就是接口要小而专,不要搞大而全的"胖接口"。

我在项目中见过太多这样的代码:一个接口里塞了十几个方法,结果实现类里一半的方法都是空实现或者直接抛异常。这就是典型的违反ISP。

核心观点:适配器模式本质上是在弥补接口设计上的"不完美"。它让你在不修改现有代码的前提下,让两个不兼容的接口协同工作。

适配器模式的两种实现方式

适配器模式有两种经典实现:类适配器和对象适配器。嗯,这里要注意,它们各有适用场景,选错了会给自己找麻烦。

类适配器(使用继承)

类适配器通过多重继承来实现。一个类同时继承目标接口和需要适配的类。

// 目标接口(我们系统期望的)
class IPayment {
public:
    virtual void ProcessPayment(const PaymentRequest& req) = 0;
    virtual ~IPayment() = default;
};

// 需要适配的第三方支付类
class ThirdPartyPay {
public:
    bool Pay(string orderId, double amount) {
        // 第三方支付逻辑
        return true;
    }
};

// 类适配器:同时继承目标接口和第三方类
class PaymentAdapter : public IPayment, private ThirdPartyPay {
public:
    void ProcessPayment(const PaymentRequest& req) override {
        // 做参数转换
        string orderId = req.GetOrderId();
        double amount = req.GetAmount();
        
        // 调用第三方接口
        bool result = Pay(orderId, amount);
        
        // 处理返回结果
        if (!result) {
            throw PaymentException("支付失败");
        }
    }
};

类适配器的优点很明显:代码简洁,不需要额外组合对象。但缺点也很致命——它依赖继承,而继承是静态的。你没法在运行时切换适配逻辑。

我曾经踩过的坑:用类适配器去适配一个没有虚析构函数的类,结果内存泄漏查了我整整两天。记住,C++里用继承做适配器,一定要确保基类有虚析构函数。

对象适配器(使用组合)

对象适配器通过组合来实现。适配器持有被适配对象的引用,然后实现目标接口。

// 对象适配器:使用组合
class PaymentAdapter : public IPayment {
private:
    ThirdPartyPay* m_adaptee;  // 持有被适配对象
    
public:
    PaymentAdapter(ThirdPartyPay* adaptee) : m_adaptee(adaptee) {}
    
    void ProcessPayment(const PaymentRequest& req) override {
        string orderId = req.GetOrderId();
        double amount = req.GetAmount();
        
        bool result = m_adaptee->Pay(orderId, amount);
        
        if (!result) {
            throw PaymentException("支付失败");
        }
    }
};

我个人习惯优先使用对象适配器。为什么?因为组合比继承更灵活。你可以在运行时传入不同的被适配对象,甚至可以在适配器里做缓存、日志、重试等额外操作。

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

对比维度 类适配器 对象适配器
实现方式 继承 组合
灵活性 低(编译时绑定) 高(运行时绑定)
代码量 稍多
适用场景 被适配类接口固定,不需要动态切换 需要动态切换适配对象,或需要添加额外逻辑
C++特有风险 多重继承、虚析构问题 需要管理对象生命周期

我的建议:除非你非常确定被适配的类永远不会变,否则一律用对象适配器。我在项目中用对象适配器的比例大概是9:1。类适配器只在我写单元测试mock时才偶尔用一下。

适配器模式的核心逻辑

为了让你更直观地理解适配器模式的工作原理,我画了一张图:

适配器模式核心结构 客户端 (Client) 目标接口 (Target) 适配器 (Adapter) 实现 被适配者 (Adaptee) 调用 工作流程 客户端调用目标接口 → 适配器接收请求 → 适配器调用被适配者的方法 → 适配器做参数/结果转换 → 返回给客户端

实际项目中的适配器模式

我来说一个真实案例。之前做一个物联网平台,需要对接不同厂商的传感器。每个厂商的SDK接口都不一样:

  • 厂商A:int GetTemperature() 返回摄氏度
  • 厂商B:float ReadTempF() 返回华氏度
  • 厂商C:void FetchData(char* buffer) 返回JSON字符串

我们系统内部定义了一个统一的传感器接口:

class ISensor {
public:
    virtual double GetTemperatureCelsius() = 0;
    virtual double GetHumidity() = 0;
    virtual ~ISensor() = default;
};

这时候,每个厂商的SDK都需要一个适配器。我用对象适配器实现了三个适配器类,每个都持有对应厂商SDK对象的指针,在内部做单位转换和数据解析。

关键设计决策:适配器里还做了数据缓存和异常处理。比如厂商C的接口偶尔会返回空数据,适配器里就加了重试逻辑。这些业务逻辑如果散落在客户端代码里,维护起来就是噩梦。

适配器模式的适用场景

什么时候该用适配器模式?我总结了几个典型场景:

  1. 第三方库集成:对方的接口和你系统的抽象不匹配
  2. 遗留系统改造:老代码的接口太"原始",需要包装成新接口
  3. 统一多个相似接口:比如上面说的多厂商传感器
  4. 测试替身:用适配器包装真实对象,方便做mock测试

注意:不要滥用适配器。如果你发现自己写了太多适配器,那说明你的接口设计可能有问题。适配器是"胶水",不是"补丁"。它解决的是接口不兼容问题,而不是代码结构混乱问题。

总结

适配器模式,说白了就是"中间人"。它站在客户端和被适配者之间,把双方的接口翻译成对方能理解的语言。类适配器用继承,对象适配器用组合——我个人强烈推荐后者。

记住一点:适配器模式不是银弹。它解决的是接口层面的不兼容,而不是设计层面的不合理。如果你的系统里到处都是适配器,那可能是时候重新审视你的接口设计了。

好了,今天就聊到这里。适配器模式虽然简单,但用好了能省去大量重构的麻烦。下次遇到接口不兼容的问题,先别急着改代码,想想能不能加个适配器。


专注资料整理