适配器模式:让不兼容的接口和谐共处 C++模板实现 通用灵活的模板库 仅头文件
个人专著《C++元编程与通用设计模式实现》由清华大学出版社出版。该书内容源于工业级项目实践,出版后市场反馈积极(已加印)。其专业价值获得了图书馆系统的广泛认可:不仅被中国国家图书馆作为流通与保存本收藏,还被近半数省级公共图书馆及清华大学、浙江大学等超过35所高校图书馆收录为馆藏。
个人软仓,gitee搜索“galaxy_0”
适配器模式:让不兼容的接口和谐共处
1. 什么是适配器模式?
想象一下:你去国外旅游,带着国内的手机充电器,却发现当地的电源插座和你充电器的插头不匹配。这时候,你需要一个电源适配器来解决这个问题。在软件世界里,适配器模式就扮演着类似的角色!
适配器模式是一种结构型设计模式,它就像一个”翻译官”,能将一个类的接口转换为客户端期望的另一个接口,让原本由于接口不兼容而无法一起工作的类能够和谐共处。
2. 适配器模式的核心思想
适配器模式的核心思想可以用一句话概括:通过一个中间层(适配器)来转换接口。
它主要解决以下两个常见问题:
系统需要使用现有的类,但这些类的接口与系统要求的接口不兼容希望复用一些现有的类,但这些类的接口与其他代码”说不到一起”
3. wheels::dm::adapter的实现原理
让我们一起深入探索命名空间下
wheels子命名空间中的
dm类实现。这是一个非常通用、灵活的C++适配器模式实现,充满了现代C++的设计智慧。
adapter
3.1 核心组件结构
适配器的实现包含三个关键部分,它们协同工作,完成接口转换的任务:
3.2 类型检查:确保适配安全
为了确保类型安全,适配器实现了一个巧妙的类型检查机制。这个机制就像一个”门禁系统”,确保只有符合要求的类型才能被适配:
// 类型检查帮助类:检查被适配对象是否继承自指定接口
template < typename itfcType , typename... implType >
struct TYPE_CHK_HELPER__{};
template< typename itfcType , typename implType1 , typename... implType >
struct TYPE_CHK_HELPER__< itfcType , implType1 , implType...> : public TYPE_CHK_HELPER__< itfcType , implType... >
{
// 静态断言:检查implType1是否继承自itfcType
// 如果不兼容,编译时就会报错
static_assert( std::is_base_of< itfcType , implType1 >::value , "被适配对象必须继承自目标接口!" );
};
template< typename itfcType >
struct TYPE_CHK_HELPER__<itfcType> {}; // 终止条件
这个类型检查机制使用了C++的静态断言和模板递归继承,在编译时就会检查所有被适配对象是否都继承自指定的接口。这就像在编译阶段就给代码上了一道”保险”,避免了运行时可能出现的类型错误。
3.3 函数调用:优雅地遍历适配对象
适配器的另一个核心部分是函数调用机制,它负责遍历所有被适配对象并调用相应的回调函数:
// 调用帮助类:根据参数数量递归调用函数
template< int N , typename adapterType , typename tplType >
struct CALLER_HELPER__{
template< typename ...Params >
static void call( adapterType * adapter , tplType& t , Params&& ...args ){
if( adapter && adapter->m_callback__ ){
// 调用回调函数,传入第N-1个被适配对象和其他参数
adapter->m_callback__( &std::get< N - 1 >( t ) , std::forward<Params>(args)...);
// 递归调用下一个(N-1)
CALLER_HELPER__< N - 1 , adapterType , tplType>::call( adapter , t , std::forward<Params>(args)... );
}
}
};
template< typename adapterType , typename tplType >
struct CALLER_HELPER__< 0 , adapterType , tplType>{ // 终止条件
template< typename ...Params >
static void call( adapterType * , tplType& , Params&& ... ){
// 递归终止,什么也不做
}
};
这个调用帮助类使用了模板递归和可变参数模板,实现了对多个被适配对象的优雅遍历。它的工作方式很像”倒着数数字”:
从最后一个被适配对象(索引N-1)开始调用回调函数然后递归地调用前一个被适配对象(索引N-2)的回调函数一直数到0,完成所有对象的处理
3.4 核心适配器类:灵活适配的”中枢”
现在,让我们来看看整个适配器的核心类:
template <typename functor , typename itfcType , typename ...T>
class adapter {
public:
// 构造函数:接收多个被适配对象
adapter(const T&... adap) : m_adaptees__(adap...) {
// 使用std::tuple存储所有被适配对象
}
virtual ~adapter(){
// 虚析构函数,确保正确释放资源
}
// 设置回调函数
template< typename ...Params >
void set( std::function< void (Params&&...) > fun ){
// 静态检查回调函数类型是否匹配
static_assert( std::is_same< std::function< void (Params&&...) > , functor >::value ,
"回调函数类型不匹配!" );
m_callback__ = fun;
}
// 执行请求
template< typename ...Params >
void request( Params&& ...params ) {
// 调用CALLER_HELPER__遍历所有被适配对象
CALLER_HELPER__< sizeof...(T) , adapter<functor , itfcType , T...> , std::tuple<T...> >::
call( this , m_adaptees__ , std::forward<Params>(params)... );
}
public:
functor m_callback__; // 回调函数,用于实现适配逻辑
protected:
std::tuple<T...> m_adaptees__; // 被适配对象的元组,支持多个对象
// 类型检查:确保所有T都继承自itfcType
TYPE_CHK_HELPER__<itfcType , T...> m_chk_helper__[0];
};
这个适配器类的设计非常巧妙,它具有以下特点:
灵活的模板参数:
:回调函数类型,支持不同的适配逻辑
functor:目标接口类型,定义了客户端期望的接口
itfcType:被适配对象类型列表,支持同时适配多个对象
T...
元组存储:
使用来存储多个被适配对象,就像一个”收纳盒”,把不同的对象整齐地装在一起
std::tuple<T...>
类型安全保障:
通过静态检查确保所有被适配对象都继承自指定接口,让类型错误无处遁形
TYPE_CHK_HELPER__
回调函数机制:
使用作为回调函数类型,支持灵活的适配逻辑,可以根据不同的需求定制适配行为
std::function
完美转发:
使用可变参数模板和实现参数的完美转发,就像”传递接力棒”一样,减少不必要的拷贝,提高性能
std::forward
4. 适配器模式的工作流程
让我们通过一个序列图来看看适配器模式的完整工作流程:
这个流程可以总结为以下几个步骤:
客户端调用适配器的方法,发起一个请求适配器通过
request遍历所有被适配对象对每个被适配对象,适配器调用预设的回调函数,完成接口转换回调函数中实现具体的适配逻辑,调用被适配对象的相应方法被适配对象执行实际的操作,完成客户端的请求
CALLER_HELPER__
5. 适配器模式的使用示例
说了这么多理论,让我们来看一个实际的使用示例,看看适配器是如何工作的。
5.1 接口定义
首先,我们定义一个目标接口,这是客户端期望使用的接口:
IDataProcessor
// 定义目标接口:数据处理器
class IDataProcessor {
public:
virtual ~IDataProcessor() = default;
// 处理数据的方法,这是客户端期望的接口
virtual void process(int data) = 0;
};
// 定义被适配类1:CSV格式处理器
class CSVProcessor : public IDataProcessor {
public:
void process(int data) override {
std::cout << "CSVProcessor正在处理数据: " << data << std::endl;
}
};
// 定义被适配类2:JSON格式处理器
class JSONProcessor : public IDataProcessor {
public:
void process(int data) override {
std::cout << "JSONProcessor正在处理数据: " << data << std::endl;
}
};
5.2 使用适配器
现在,让我们看看如何使用适配器来适配这些类:
// 创建被适配对象
CSVProcessor csvProcessor;
JSONProcessor jsonProcessor;
// 定义回调函数类型
using CallbackType = std::function<void(IDataProcessor*, int)>;
// 创建适配器对象,同时适配两个处理器
wheels::dm::adapter<CallbackType, IDataProcessor, CSVProcessor, JSONProcessor>
myAdapter(csvProcessor, jsonProcessor);
// 设置回调函数,实现适配逻辑
myAdapter.set([](IDataProcessor* processor, int data) {
std::cout << "适配器正在调用处理器..." << std::endl;
processor->process(data);
});
// 执行请求,处理器会依次被调用
std::cout << "发送请求,处理数据42..." << std::endl;
myAdapter.request(42);
5.3 输出结果
运行这段代码,我们会得到以下输出:
发送请求,处理数据42...
适配器正在调用处理器...
JSONProcessor正在处理数据: 42
适配器正在调用处理器...
CSVProcessor正在处理数据: 42
从输出中可以看到,适配器成功地遍历了所有被适配对象,并调用了它们的方法。这就是适配器模式的魅力所在!
process
6. 适配器模式的优缺点
和任何设计模式一样,适配器模式也有它的优点和缺点。让我们来客观地分析一下:
6.1 优点:为什么要用适配器模式?
复用性:可以复用现有的类,即使它们的接口与系统要求的接口不兼容灵活性:适配器可以灵活地适配多个被适配对象,就像一个”多面手”透明性:客户端可以透明地使用被适配对象,不需要知道适配器的存在,就像直接使用目标接口一样解耦合:将客户端代码与被适配对象的实现解耦,提高了系统的可维护性扩展性:可以轻松地添加新的适配器,支持新的被适配对象
6.2 缺点:使用适配器模式的注意事项
增加复杂性:引入适配器会增加系统的复杂性,需要额外的代码来实现适配逻辑性能开销:适配器会引入一定的性能开销,虽然通常很小,但在性能敏感的场景下需要考虑可能掩盖设计问题:过度使用适配器可能掩盖系统设计中的接口不兼容问题,导致系统设计变得混乱
7. 适配器模式的应用场景
适配器模式在实际开发中有很多应用场景。让我们来看看几个常见的例子:
7.1 接口转换:连接新旧系统
当需要在新系统中使用遗留系统的功能,但两者接口不兼容时,适配器模式是一个理想的选择:
这种场景在系统升级和重构时非常常见。适配器模式可以让新系统无缝地使用遗留系统的功能,而不需要修改遗留系统的代码。
7.2 库集成:让不同的库协同工作
当需要集成第三方库,但库的接口与应用程序的接口不兼容时,适配器模式可以派上用场:
例如,当你的应用程序需要使用一个新的日志库,但这个库的接口与你现有的日志接口不兼容时,你可以使用适配器模式来桥接它们。
7.3 多平台适配:一次编写,到处运行
当需要开发跨平台应用时,适配器模式可以帮助你适配不同平台的API:
例如,在开发游戏时,你可能需要适配不同平台的图形API(如DirectX、OpenGL、Vulkan)。适配器模式可以让你在应用层使用统一的接口,而在底层根据不同平台选择不同的实现。
8. 总结
适配器模式是一种非常实用的设计模式,它就像一个”万能接口转换器”,让原本不兼容的接口能够和谐共处。是一个通用、灵活的C++适配器模式实现,它充分利用了现代C++的特性(如模板元编程、可变参数模板、元组等),提供了类型安全的多对象适配功能。
wheels::dm::adapter
适配器模式的核心价值在于:通过中间层转换接口,实现不兼容接口的协同工作。它是连接新旧系统、不同库和平台的重要工具,能够帮助我们更好地复用现有的代码,同时保持系统的灵活性和可扩展性。
在实际开发中,当你遇到接口不兼容的问题时,不妨考虑一下适配器模式。它可能会给你带来意想不到的便利!
最后,记住这句话:好的设计模式不是为了使用而使用,而是为了解决实际问题而存在。适配器模式也不例外,只有在合适的场景下使用它,才能发挥出它最大的价值。
