1. 适配器模式的核心概念
适配器模式是结构型设计模式中最常用的模式之一,它主要解决接口不兼容的问题。想象一下你从国外带回来的电器插头无法直接插入国内的插座,这时候你需要一个转换插头——这个转换插头就是适配器模式在现实生活中的完美体现。
在C++中,适配器模式通常用于以下场景:
- 需要使用现有的类,但其接口与其他代码不兼容
- 需要复用一些现有的子类,但这些子类缺少一些公共功能
- 需要创建一个能够与多个不相关类或接口协同工作的类
关键点:适配器不是新增功能,而是接口转换。它让原本因接口不匹配而无法一起工作的类能够协同工作。
2. 适配器模式的两种实现方式
2.1 对象适配器(组合方式)
这是最常用的实现方式,通过组合关系将Adaptee对象作为Adapter的成员变量。让我们看一个完整的代码示例:
cpp复制#include <iostream>
#include <algorithm>
#include <string>
// 目标接口(客户端期望的接口)
class Target {
public:
virtual ~Target() = default;
virtual std::string Request() const {
return "Target: 标准目标行为";
}
};
// 需要适配的类(已有但接口不兼容的类)
class Adaptee {
public:
std::string SpecificRequest() const {
return ".eetpadA eht fo roivaheb laicepS";
}
};
// 适配器类
class Adapter : public Target {
private:
Adaptee* adaptee_; // 持有需要适配的对象
public:
Adapter(Adaptee* adaptee) : adaptee_(adaptee) {}
std::string Request() const override {
// 调用Adaptee的方法并进行转换
std::string to_reverse = this->adaptee_->SpecificRequest();
std::reverse(to_reverse.begin(), to_reverse.end());
return "适配器: (转换后) " + to_reverse;
}
};
// 客户端代码
void ClientCode(const Target* target) {
std::cout << target->Request() << std::endl;
}
int main() {
std::cout << "客户端: 我可以直接使用目标对象:\n";
Target* target = new Target;
ClientCode(target);
std::cout << "\n";
Adaptee* adaptee = new Adaptee;
std::cout << "客户端: Adaptee类有一个奇怪的接口,我看不懂:\n";
std::cout << "Adaptee: " << adaptee->SpecificRequest();
std::cout << "\n\n";
std::cout << "客户端: 但我可以通过适配器使用它:\n";
Adapter* adapter = new Adapter(adaptee);
ClientCode(adapter);
delete target;
delete adaptee;
delete adapter;
return 0;
}
输出结果:
code复制客户端: 我可以直接使用目标对象:
Target: 标准目标行为
客户端: Adaptee类有一个奇怪的接口,我看不懂:
Adaptee: .eetpadA eht fo roivaheb laicepS
客户端: 但我可以通过适配器使用它:
适配器: (转换后) Special behavior of the Adaptee.
2.2 类适配器(多重继承方式)
C++支持多重继承,因此我们可以使用继承方式实现适配器。这种方式下,Adapter同时继承Target和Adaptee:
cpp复制#include <iostream>
#include <algorithm>
#include <string>
// 目标接口
class Target {
public:
virtual ~Target() = default;
virtual std::string Request() const {
return "Target: 标准目标行为";
}
};
// 需要适配的类
class Adaptee {
public:
std::string SpecificRequest() const {
return ".eetpadA eht fo roivaheb laicepS";
}
};
// 类适配器(多重继承)
class ClassAdapter : public Target, public Adaptee {
public:
std::string Request() const override {
std::string to_reverse = SpecificRequest();
std::reverse(to_reverse.begin(), to_reverse.end());
return "类适配器: (转换后) " + to_reverse;
}
};
int main() {
std::cout << "客户端: 使用类适配器:\n";
ClassAdapter* adapter = new ClassAdapter;
ClientCode(adapter);
delete adapter;
return 0;
}
注意事项:类适配器虽然简洁,但在C++中多重继承可能带来"菱形继承"等问题。除非必要,建议优先使用对象适配器。
3. 适配器模式的实战应用
3.1 旧代码与新系统的集成
假设我们有一个旧的日志系统,接口如下:
cpp复制class LegacyLogger {
public:
void WriteToLog(const char* message, int priority) {
// 旧式日志写入实现
std::cout << "Legacy Log [" << priority << "]: " << message << std::endl;
}
};
而新系统期望的日志接口是:
cpp复制class NewLogger {
public:
virtual ~NewLogger() = default;
virtual void Log(const std::string& message, LogLevel level) = 0;
};
我们可以创建适配器来桥接这两个系统:
cpp复制enum class LogLevel { DEBUG, INFO, WARNING, ERROR };
class LoggerAdapter : public NewLogger {
private:
LegacyLogger* legacyLogger;
int ConvertLogLevel(LogLevel level) {
switch(level) {
case LogLevel::DEBUG: return 0;
case LogLevel::INFO: return 1;
case LogLevel::WARNING: return 2;
case LogLevel::ERROR: return 3;
default: return 1;
}
}
public:
LoggerAdapter(LegacyLogger* logger) : legacyLogger(logger) {}
void Log(const std::string& message, LogLevel level) override {
legacyLogger->WriteToLog(message.c_str(), ConvertLogLevel(level));
}
};
3.2 STL中的适配器应用
C++标准模板库(STL)中广泛使用了适配器模式,例如:
-
容器适配器:
std::stack适配了std::dequestd::queue适配了std::dequestd::priority_queue适配了std::vector
-
迭代器适配器:
std::reverse_iteratorstd::move_iterator
-
函数对象适配器:
std::bind(在C++11之前有std::bind1st,std::bind2nd)
以stack为例,它实际上是对底层容器(默认deque)的适配:
cpp复制template<typename T, typename Container = std::deque<T>>
class stack {
public:
void push(const T& value) { c.push_back(value); }
void pop() { c.pop_back(); }
T& top() { return c.back(); }
// ... 其他成员函数
private:
Container c; // 底层容器
};
4. 适配器模式的进阶技巧
4.1 双向适配器
有时我们需要两个类能够互相适配,这时可以创建双向适配器:
cpp复制class TwoWayAdapter : public NewLogger, private LegacyLogger {
public:
TwoWayAdapter() = default;
// 实现NewLogger接口
void Log(const std::string& message, LogLevel level) override {
WriteToLog(message.c_str(), ConvertLogLevel(level));
}
// 暴露LegacyLogger接口
using LegacyLogger::WriteToLog;
private:
int ConvertLogLevel(LogLevel level) {
// 转换逻辑同上
}
};
4.2 适配器与智能指针结合
在现代C++中,我们可以使用智能指针来管理适配器资源:
cpp复制std::unique_ptr<NewLogger> CreateLoggerAdapter() {
auto legacyLogger = std::make_unique<LegacyLogger>();
return std::make_unique<LoggerAdapter>(legacyLogger.release());
}
// 使用
auto logger = CreateLoggerAdapter();
logger->Log("Error occurred", LogLevel::ERROR);
4.3 模板适配器
使用模板可以创建更通用的适配器:
cpp复制template<typename AdapteeType>
class GenericAdapter : public Target {
private:
AdapteeType adaptee;
public:
std::string Request() const override {
// 假设所有AdapteeType都有specificRequest方法
std::string result = adaptee.specificRequest();
// 进行必要的转换
return "适配: " + result;
}
};
5. 适配器模式的优缺点与适用场景
5.1 优点
- 单一职责原则:将接口转换代码从业务逻辑中分离
- 开闭原则:无需修改现有代码就能引入新类
- 复用性:可以让多个不兼容的类一起工作
- 灵活性:可以动态切换适配的类
5.2 缺点
- 复杂度增加:需要额外引入一系列新类和接口
- 性能开销:某些情况下适配过程可能带来额外开销
- 过度使用:如果系统设计良好,可能不需要适配器
5.3 适用场景
- 需要使用现有类,但其接口不符合需求
- 想创建一个可复用的类,与多个不相关类协同工作
- 需要为多个现有子类提供统一接口
- 需要集成多个第三方库,且它们的接口各不相同
6. 适配器模式与其他模式的关系
- 桥接模式:都会涉及到接口工作,但桥接模式是预先设计的,适配器模式是事后补救
- 装饰器模式:都使用组合,但装饰器增强功能,适配器转换接口
- 外观模式:都简化接口,但外观模式定义新接口,适配器复用现有接口
- 代理模式:都作为中间层,但代理模式保持相同接口,适配器改变接口
7. 实际项目中的经验分享
7.1 性能考量
在性能敏感的场景中,适配器可能成为瓶颈。我曾经在一个高频交易系统中遇到性能问题,最终发现是日志适配器引入了不必要的字符串转换。解决方案是:
- 避免在适配器中做复杂计算
- 使用移动语义减少拷贝
- 考虑缓存常用转换结果
优化后的适配器实现:
cpp复制class OptimizedLoggerAdapter : public NewLogger {
private:
LegacyLogger& logger; // 使用引用避免指针间接访问
mutable std::unordered_map<LogLevel, int> levelCache;
int GetCachedLogLevel(LogLevel level) const {
auto it = levelCache.find(level);
if(it != levelCache.end()) return it->second;
int value = ConvertLogLevel(level);
levelCache[level] = value;
return value;
}
// ... 其余部分相同
};
7.2 测试策略
适配器需要特别注意测试:
- 测试适配器是否正确地转换了所有输入
- 测试边界条件和异常情况
- 测试性能是否可接受
使用Google Test的测试示例:
cpp复制TEST(LoggerAdapterTest, ConvertsLevelsCorrectly) {
LegacyLogger logger;
LoggerAdapter adapter(&logger);
testing::internal::CaptureStdout();
adapter.Log("Test", LogLevel::ERROR);
std::string output = testing::internal::GetCapturedStdout();
EXPECT_TRUE(output.find("Legacy Log [3]: Test") != std::string::npos);
}
7.3 常见陷阱
- 适配器膨胀:避免让一个适配器做太多事情,应该遵循单一职责原则
- 循环依赖:当适配器相互引用时可能导致问题
- 接口污染:不要因为适配而添加不必要的接口方法
8. C++17/20中的新特性应用
现代C++提供了更多实现适配器的工具:
8.1 使用std::variant实现多适配
cpp复制class MultiAdapter {
private:
std::variant<LegacyLogger, NewSystemLogger> logger;
public:
void Log(const std::string& msg, LogLevel level) {
std::visit([&](auto&& arg) {
using T = std::decay_t<decltype(arg)>;
if constexpr (std::is_same_v<T, LegacyLogger>) {
arg.WriteToLog(msg.c_str(), ConvertLevel(level));
} else {
arg.Log(msg, level);
}
}, logger);
}
};
8.2 使用概念约束适配器
C++20的概念可以让适配器接口更安全:
cpp复制template<typename T>
concept LoggerAdaptable = requires(T t) {
{ t.Log("", LogLevel::INFO) } -> std::same_as<void>;
// 或其他必要的接口要求
};
template<LoggerAdaptable Logger>
class SafeLoggerAdapter : public Target {
Logger& logger;
// ... 实现
};
9. 设计建议与最佳实践
- 优先组合而非继承:除非必要,否则使用对象适配器而非类适配器
- 保持适配器简单:只做接口转换,不要添加业务逻辑
- 文档化适配关系:明确记录哪些接口对应哪些方法
- 考虑线程安全:如果适配器会被多线程使用,需要适当同步
- 使用依赖注入:通过构造函数注入被适配对象,提高可测试性
一个工业级的适配器实现应该包含:
cpp复制class ProductionReadyAdapter : public Target {
public:
// 使用依赖注入
explicit ProductionReadyAdapter(AdapteeInterface& adaptee,
std::shared_ptr<MetricsCollector> metrics = nullptr)
: adaptee_(adaptee), metrics_(metrics) {}
// 提供移动语义支持
ProductionReadyAdapter(ProductionReadyAdapter&&) = default;
ProductionReadyAdapter& operator=(ProductionReadyAdapter&&) = default;
// 禁用拷贝
ProductionReadyAdapter(const ProductionReadyAdapter&) = delete;
ProductionReadyAdapter& operator=(const ProductionReadyAdapter&) = delete;
ResultType Request(Args args) override {
try {
if(metrics_) metrics_->RecordCall("Request");
// 实际适配逻辑
auto adapted = adaptee_.DifferentMethod(args);
return Transform(adapted);
} catch(const AdapteeException& e) {
throw TargetException(e.what());
}
}
private:
AdapteeInterface& adaptee_;
std::shared_ptr<MetricsCollector> metrics_;
ResultType Transform(const AdapteeResult& result) {
// 转换实现
}
};
10. 从适配器模式看C++设计哲学
适配器模式很好地体现了C++的几大设计哲学:
- 零开销抽象:适配器可以在不修改被适配类的情况下提供新接口
- 资源管理:通过RAII确保适配器资源的正确释放
- 多范式支持:既可以用OOP方式实现,也可以用模板元编程实现
- 向后兼容:适配器模式是保持向后兼容的强大工具
在实际工程中,我经常使用适配器模式来集成旧系统。曾经有一个项目需要将旧的C风格API集成到现代C++系统中,通过精心设计的适配器层,我们不仅实现了平滑过渡,还保留了将来替换旧实现的灵活性。
