适配器模式也是在开发一些业务框架时经常会使用到的模式。
1. STL 的适配器
其实该模式在我们使用 STL 标准模板库时也经常碰到过。比如,not1、not2 去翻适配器,bind1st、bind2nd 绑定适配器等,我们回顾下绑定适配器的使用,如下代码所示:
#include <iostream> #include <vector> #include <functional> #include <algorithm> using namespace std; void print_vector1(int number) { std::cout << number << " "; } void print_vector2(int number, int additional) { std::cout << number << " " << additional; } void test() { vector<int> v = { 10, 20, 30, 40, 50 }; for_each(v.begin(), v.end(), print_vector1); for_each(v.begin(), v.end(), print_vector2); } int main() { test(); return 0; }
上面示例代码中,24行代码运行是没有问题的。这是因为 for_each 算法要求传入的函数有且只能有1个参数。如果你定义的函数有 2 个参数的话,for_each 算法就会报错,报错的原因可以从其算法实现来知晓原因:
template <class _InIt, class _Fn> _CONSTEXPR20 _Fn for_each(_InIt _First, _InIt _Last, _Fn _Func) { // perform function for each element [_First, _Last) _Adl_verify_range(_First, _Last); auto _UFirst = _Get_unwrapped(_First); const auto _ULast = _Get_unwrapped(_Last); for (; _UFirst != _ULast; ++_UFirst) { _Func(*_UFirst); } return _Func; }
如果我们仅仅是为了让 25 行代码能够正常执行,可以强制将其函数指针类型进行转换,但是需要注意的是,这种强转后的结果可能会超出我们的预期。如下所示:
for_each(v.begin(), v.end(), (void(*)(int))print_vector2);
上述代码执行后的结果:
122788047220 -65409933630 -65409933640 -65409933650 -654099336
虽然不报错了,但是结果也不是我们想要的。所以,我们经常会用到 STL 中的绑定适配器。
for_each(v.begin(), v.end(), bind1st(ptr_fun(print_vector2), 100)); for_each(v.begin(), v.end(), bind2nd(ptr_fun(print_vector2), 100));
for_each 算法要求一个参数的函数,而 print_vector2 是 2 个参数的函数,我们可以通过适配器将其转换为 1 个参数的函数,这样就能符合 for_each 算法的要求。从这里,同学们应该能够明白了,适配器最普遍的作用就是对函数进行适配,将其接口转换为符合目标算法要求的函数。
上面的示例代码中,ptr_fun 本质也是一个适配器,只不过其作用是将一个普通函数转换为函数对象(仿函数),因为在 STL 中绑定适配器要求的参数必须是函数对象。
2. 适配器实现原理
我们有很多不同场景,需要实现的适配器也有所不同。STL 中的 bind1st、bind2nd 是基于模板来实现的适配器,其可以适应不同数据类型的场景,同学们可以参数 vs 版本 STL 的源码实现来学习编码技巧。我们可以简单给同学们介绍下如何基于面向对象思想来实现适配器。
在写之前,先明确下目标和意图:适配器模式用于将接口转换为另外一种接口,解决原本接口不兼容的问题。业务函数(要求传递一个数字和一个带返回值的单个参数的函数)如下:
template<class T> void do_logic(int number, T show) { std::cout << show(number) << std::endl; }
我们使用模板来定义 do_logic 函数,是由于该函数既可以传递函数指针,也可以传递函数对象,我们需要传递给 do_logic 的函数参数如下:
int my_show1(int a) { return a; } int my_show2(int a, int b) { return a + b; }
很明显,my_show2 并不适配 do_logic 第二个参数的要求。此时,我们就可以实现一个适配器,来将 my_show 接口进行转换。
class Adaptor { public: Adaptor(int(*show)(int, int), int addtional) { this->show = show; this->addtional = addtional; } int operator()(int number) { return this->show(number, this->addtional); } public: int(*show)(int, int); int addtional; }; void test1() { do_logic(100, my_show1); do_logic(100, Adaptor(my_show2, 200)); }
Adaptor 就是用于将 my_show2 函数进行转换的适配器类,该类接受相应的参数并返回一个能够被 do_logic 正确调用的函数对象,从而完成将 my_show2 接口适配到 do_logic 目标函数的目的。从代码实现,同学们应该能够很清晰的看到,所谓的适配就是对不符合要求的接口进行一层代码包装,在包装内部完成适配的转换过程。另外,同学们也要了解上面代码的局限性,即:它只能适配参数为 int 类型的函数。如果想编写支持不同类型的适配器,需要使用到 C++ 模板泛型技术。