从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时才偶尔用一下。
适配器模式的核心逻辑
为了让你更直观地理解适配器模式的工作原理,我画了一张图:
实际项目中的适配器模式
我来说一个真实案例。之前做一个物联网平台,需要对接不同厂商的传感器。每个厂商的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的接口偶尔会返回空数据,适配器里就加了重试逻辑。这些业务逻辑如果散落在客户端代码里,维护起来就是噩梦。
适配器模式的适用场景
什么时候该用适配器模式?我总结了几个典型场景:
- 第三方库集成:对方的接口和你系统的抽象不匹配
- 遗留系统改造:老代码的接口太"原始",需要包装成新接口
- 统一多个相似接口:比如上面说的多厂商传感器
- 测试替身:用适配器包装真实对象,方便做mock测试
注意:不要滥用适配器。如果你发现自己写了太多适配器,那说明你的接口设计可能有问题。适配器是"胶水",不是"补丁"。它解决的是接口不兼容问题,而不是代码结构混乱问题。
总结
适配器模式,说白了就是"中间人"。它站在客户端和被适配者之间,把双方的接口翻译成对方能理解的语言。类适配器用继承,对象适配器用组合——我个人强烈推荐后者。
记住一点:适配器模式不是银弹。它解决的是接口层面的不兼容,而不是设计层面的不合理。如果你的系统里到处都是适配器,那可能是时候重新审视你的接口设计了。
好了,今天就聊到这里。适配器模式虽然简单,但用好了能省去大量重构的麻烦。下次遇到接口不兼容的问题,先别急着改代码,想想能不能加个适配器。