<软件架构与设计模式>侯捷老师关于Adapter类在STL中的深入解析和模式探讨
题外话:侯捷老师难得一年就来上九堂课就要会宝岛,特此留念签名赠语及合照以自勉。
学海无涯,为勤是岸
<正文开始>
昨天晚上连上了3个小时的大课探究单单讲了Adapter一个类,幸运的是本人恰好在大一的时候接触过比如<functioinal>库类中的bind1st,bind2nd这些函数对象的使用方法,毕竟若要使用<algorithm>的话,里面几乎每一个函数都需要我们把模版中的函数对象比如Comparato之类的重写一下,但是真的没有想到,这些库这些类使用起来明明那么简单但是在老师的讲解下里面的结构和设计人员的思路原来真的是复杂缜密。
具体说Adapter(适配器)这种设计之前必须要先简单说说LSP(里氏替换原则),其内容是:子类(derived typed)必须能够替换掉它的基类(base type)。这一句话就有点让我摸不着头脑了,既然是子类已经从基类继承过来了,那岂不就是子类必然能够替换掉父类么?不然多态(polyporphism)还从和谈起?如果是在java里的话,(我觉得)也许这点是毫无争议的因为java有强大的继承体系一切类都要从Object类继承而来(lambda表达式的目标对象也不是一种类,所以不算从Object继承而来),然而在C++语言之中另有一种独特的设计--private继承,这种独特的继承方式会是父类中无论是protected还是public继承抑或是多重继承过来的指针引用都转变为子类的private数据,也就是说这些数据在继承树的这条分支上已经被宣告了终结。所以象在MFC这样的库的建立的时候LSP原则是必须遵守的,不然会给程序带来(严重的)隐患。
课件中所示,Adapter设计一般有三种(昨天只讲了两种):
(1)Container Adapter
这个库的底层容器我们可以看到选择的是deque,值得注意的是在这套设计之中queue和deque是复合的关系,但是和我猜测的相反,queue包含了deque类也就是说queue是在deque之上架构起来的,至于为什么明明deque和(双驱)list有相同的“效果”甚至有相同的方法命名却选择了deque来作为默认的底层容器,老师谦虚地讲他也不敢下定论,但是我们至少可以大胆地推测deque的速度或者效率是优于list的。而且在这里面queue“HAS-A”deque这样的关系,在这个结构之中架构起来的更多的容器基本上都是以底层容器出发,改写方法添加约束从而完成架构。在《More Effective C++》中的编程准则之中有提到过,建议编程者将一切的数据写入private之中,但是这并不意味着这个类在继承链上部分数据的残缺,因为我们会在public或者protected中声明它们的getter&setter(是否想起了java呢:-P)来不断地继承过来对这个数据的操作权从而实现安全架构。
##stack容器的实现和queue的实现是极其相似的,这里不多赘述##
(2)Function Adapter
这部分较上部分较难懂的,例子中的Adapter简单的来讲就是要实现一个功能:参数绑定。有的函数对象作为参数可能只需要一个参数来进行调用,但是我们这里却只用一个双参的函数对象无法直接使用作为参数,那么最效率的解决方法就是使用Adapter。当然不要自信地认为会用了这个binder系列not系列函数就是理解了Adapter,深入了解架构,才能帮助我们写出自己想要的Adapter,了解STL的规范才可以帮助我们写出的Adapter更好地与上层容器/上层对象相兼容。比如要写出一个三参绑为一个四参绑为两个这些特殊的情况才会有良好的方案去解决。
例子是假设我们要使用count_if函数,这个函数在<algorithm>库中的作用是if xx then count计数,其中的第一个参数和第二个参数当然是Iterator类型,然而第三个参数我们需要一个单参的Predicate函数对象或者函数指针。如下图所示,其实在这里面Predicate只是template中的一个命名,它可以是T,Fxxx各种名字,但是一个Predicate的名字就可以让其他人看出这个函数对象最后需要返回的是一个布尔值,如果一旦模版实例化为一个将操作符重载为例如void operator()(Paramlist..)这样的函数时,那么在count_if内部的判断分支语言就会遇到麻烦,而且由于c++的灵活性如果将返回值重载为int /long类的话,这样count_if就不会报错程序照常执行的同时,会给貌似正确程序埋下隐患。
那么Adapter是怎么样架构起来的呢?下面这张图很好地概括了下来。如下图所示(其实那个f和x之间按理来说应该有个逗号Anyway~)Adapter的实现过程就是在构造的时候将需要绑定的参数绑定下来,然后重载操作符使之成为一个新的函数对象。是的,貌似非常简单,但是接着向下看,如果考虑到functionAdapter是从unary_function和binary_function继承而来的模版结构的话,会大大增加复杂度,整个搭建起来adapter的过程也是不断地向父类“询问”typedef类型的过程,这些约束是adapter中最难理解的部分。
最后我们再来看一下具体的实现宏观过程:
##课后提问##
我:在C++11的新特性中已经允许了自动类型推断,而为什么黑板上强调的bind2nd的模版类型必须和参数类型表明一致?:::(黑板)bind2nd<less<int>>(less<int>(),12)
侯捷:因为C++11的自动类型推断只能运用在模版函数之中,但是对于模板函数对象,我们必须前后表明参数和函数对象的模版。