本原则所叙述的内容比较复杂涉及到设计模式相关的内容。它主要讲的就是以non-virtual函数代替virtual函数的策略。
作者举了一个例子,说有这么一个游戏的血量计算函数,但是你知道啊,这个游戏里面有很多不同的怪物,像什么石头人、萨特一家、人马等,它们计算血量的方式是不同的。那就涉及到一个多态继承的问题了,就是父类中有一个virtual函数,不同的子类去重写这个virtual函数,如果不重写那也就只有继承父类的默认实现了,毕竟virtual函数存在的意义就在于此啊。
作者在这里没有提及为啥要寻找一个方案去替代virtual函数,而是直接说要去寻找一个替代方案,这不免有些让人不解。很显然作者的态度就是不解释。不过经过阅读我大概了解到作者就是想另辟蹊径,就是想脱离OOP的老套路,我想这是不是有点哗众取宠呢,不过他的方案的确有可取之处。
在这里作者推荐了第一种解决方案——用NVI(Non-VirtualInterface)去实现TemplateMethod模式。在这个过程中,作者讲了一个他的前人的方法,他的前人们认为,所有的virtual函数都应该是private的,这个是前提。为了实现这种前提,他的前人们决定使用一个非virtual公有成员函数接口,然后让这个公有接口去调用它内部的私有virtual函数。那么这种通过non-virtual函数接口去掉用privatevirtual函数的手法就是所谓的NVI了,而且这个NVI还属于什么TemplateMode模式的范畴。而这个non-virtual的公有成员函数接口被称为wrapper(外覆器)。
在这种设计模式下,多数子类都会重新定义父类中的virtual函数,因为这是它们自身特定的实现代码,但是父类中的virtual函数不排除被子类调用的可能。
不过,NVI父类中的virtual不一定非要是private的,这个要根据场合而定。例如有些继承体系要求要在子类的virtual函数的实现中去调用父类的virtual函数,那如果父类的virtual函数还是private的,那这条路就行不通了,此时父类的virtual函数就应该是protected的。再比如,具备多态性质的集成体系中,父类的析构函数就必须是virtual的并且是public的,因为此类对象需要调用父类的析构函数去析构父类成分。因为如果直接调用virtual函数的话,必要的事前和事后工作就不一定能做的了。而如果virtual函数是被调用的函数,那么在主调函数中就可以做一些事前工作和事后工作了,这就是采用NVI方法的原因。
在这个原则当中作者是在类内部直接去实现成员函数的,这样一来这个成员函数就是内联函数了,这涉及到了原则30的一些内容。在类体外定义的函数要加inline关键字才能变成内联函数,但是函数定义中含有循环、switch、goto、static或者该函数是递归函数,那么编译器就不允许它成为内联函数。
第二方法是用函数指针FunctionPointers去实现Strategy模式从而绕过virtual函数。还是先前那个例子,但是换了一个角度。反正都是计算怪物的血量嘛,那么我们何不抽取其共性做成一个函数,然后弄一个接口去接受指向这个函数的指针呢。
首先一个怪物肯定属于一个类,这个怪物有一个血量的属性,而这个血量的属性又属于另一个类。那么就可以把这个血量类的对象设成是这个怪物类的一个私有成员。这个怪物类的构造函数可以接受一个指向默认生命值计算的函数的指针作为缺省参数,就是如果有自己的生命值计算函数就用自己的,没有就用默认的。再用这个指针去给这个血量对象进行初始化,最后还是提供若干个公有接口返回这个血量对象,而这若干个接口分别表示若干种不同情况的血量。这个什么Strategy模式到底是咋回事,我现在还不明白,这里只不过是一个大致的推测而已。
那这种方式的好处是什么呢?那就是血量的计算方式和具体的对象之间并无绑定关系,使用起来相对灵活自由。另外,如果这个怪物类提供了一个设置计算血量的setXXX函数,还可以实现在运行期改变血量计算方式。
从封装性的角度讲,这个独立的血量计算函数与怪物类是相对独立的,那么这个函数就不会去访问怪物类的非public成分。但是有时你为了让血量计算函数得以执行就需要更多的信息,而这些信息是从public接口中获得的,所以可能会遇到弱化封装性的要求。
第三种方式是用trl::function来完成Strategy模式。这个trl::function是个神马玩意以我现在的见识来看还真不知道,不过从作者的描述来看,这个东西是一个容器,它可以装载函数、函数对象、成员函数指针等,这样一来它包容的空间就大了许多。trl::function的好处可兼容能通过隐式转换转换成参数函数的参数,并且也能像参数中那个函数一样提供一个返回值,但是它的兼容性更好,也就是提供了一种需求上的弹性。
在这里作者又说了一大堆有关trl::bind的内容,在这里我只大致说一下我的理解就好。这个trl::bind就是能将某个参数与接受这个参数的函数绑定,并且是以明确的方式绑定。其他的我就真不知道了。
说了这么多那到底为什么要去实现Strategy模式呢?因为在Strategy模式中血量计算的部分与游戏人物的类是相对独立的,这样你怎么改动血量计算的方式游戏人物类面对的也只是一个单一的接口。这样血量计算类就可以担负起解决virtual函数的问题的重任了,这是一种问题转移的策略。也是解决virtual函数问题的第四种方式。