授课:John Lee 整理:缥缈九哥 通俗的说“开闭原则”,就是写过的代码,不能再修改。要新的功能,只能添加,除非纠bug,或者重构之类。 那么只能添加,如何做? 从基类派生啊 接口尽量考虑周到 继承 多开闭原则的定义倡导对抽象基类的继承。接口规约可以通过继承来重用,但是实现不必重用。 在面向对象之类的语言中,比如已存在的某个功能,想扩展一下,请看看下面的例子。 大家都知道虚函数的作用吧? 用来扩展的 派生类中如果不重新定义虚函数,则调用基类定义的虚函数 它是用来扩展的,虚函数就是C++实现“开闭原则”的“开”的机制,“闭”就如上撰述。虚函数的开销还是一些的。你们发现look里有没有虚函数?数量不多,开销还可以承受。 用Mixin, 怎么扩展都行。把功能块写好,再组装。 如果功能在模块自己呢?自己如何组装?虚函数是一种方法,可是粒度太细,开销太大,而且粒度越细,开销越大, mixin的另外一种实现机制,是CRTP,就是JamesCoplien在1995年提出的奇异递归模板模式(CRTP)。 CRTP和虚函数各有优缺点,虚函数的优点是基类不需要知道派生类的信息,只管调用虚函数就是,这个得益于虚函数的“晚捆绑”实现机制。 虚函数的缺点就是“晚捆绑”要一定的空间开销和运行开销。 而CRTP正好相反,它不使用“晚捆绑”机制,因此没有这些开销,但缺点是基类需要知道派生类的信息,才能调用派生类的函数。但是派生类是不可预料的。而CRTP模式,正是为了把派生类信息传递给基类。正因为是编译绑定才没有多余的开销。CRTP只是看起来比较怪异,可以看这个例子: https://bbs.21ic.com/icview-551444-1-1.html 奇异递归模板模式(CRTP) 自身扩展,是mixin的一个重要方面,而传统的OOP,是借助虚函数实现的。继承基类,覆盖虚函数,就完成了自身扩展。 自身扩展,就是从基类的接口(函数)进入,调用到了派生类的函数。如果不把派生类的信息传递到基类,基类如何调用派生类的函数? 基类为何要调用派生类的函数? 派生类自己调用不能实现功能吗? 这时派生类都还没写出来呢,基类不能调用派生类的函数。实现功能的入口(起点)是在基类,而中间部分实现可能有变化(可以扩展)的。 可是派生类的存在不就是为了对对象进行封装和扩展吗,如果让基类调用派生类,不是破坏了封装吗 那你还没有理解虚函数的真正意义 虚函数的真正有用的,是用基类指针调用虚函数。如果已经知道了派生类,那么虚函数是无意义的。 从派生类中调用基类的函数,是很普通的,因为派生类就包含了基类的信息,编译器可以使用早捆绑。 但如果扩展都必须从派生类作为起点,那么这种所谓的“扩展”没有什么意义。 通常的做法就是从基类开始一层一层的派生,一直到最派生类,层层的继承和扩展。 一层一层打包 比如模块A(class A),与模块B(class B)聚合,class A调用class B的某个功能,两者没有is-a的关系,就是class A中的一个函数,调用class B中的一个函数。现在你想修改一下功能。但根据“闭”原则,你不能修改 class B。那么你从class B派生了class C来实现修改的功能。但是,你拿着class C 去跟class A说,这是新功能,你要用class C。class A能认识class C 吗? 不认识!class A只认识class B。所以,你的class C要装成 class B,去“骗”class A。那么class A 如何能通过class B访问到class C呢? 因为class C是class B的派生类,所以class C已经有了class B的全部信息,所以当class A遇到class C时,它还是认为遇到的是class B。 如果你在class C中覆盖了这个功能(函数),但class B中的原函数没有定义为虚函数,那么显然,class C中的函数,和class B中的函数不是一回事,即使它们同名。class A仍然会调用class B(现在它是class C的基类)中的原来的函数。 其实class C有两个函数(同名的),分别位于基类(class B)和派生类(class C)。 因为作用域的不同,所以不会因为同名同参数引起二义性? 例如: struct B { void foo(); }; struct C { void foo(); }; C c; B* p = &c; p->foo(); 上面的p->foo()调用哪个?是调用B::foo()?还是C::foo()? 它会调用B::foo(),虽然是C类型的变量c,但赋值给B类型的指针p时,就发生了up cast,类型收窄了。 当用p指针调用foo()时,因为p的类型是B*,所以会调用到B::foo()。 所以,如果class B的函数没有定义为虚函数的话,那么该功能是没有留出扩展的余地。 你即使派生了class C并写了同名的函数,也没有任何作用。class A仍然只认B::foo()。最后访问的还是基类的成员函数。 实际使用虚函数时,绝大多数是这种情况,几乎100%的。classA在不认识class C的情况下,能够调用到class C的函数,是C++“晚捆绑”机制实现了这种效果。 那纯虚函数的意义又何在?只是为了实现统一的接口吗?最后派生类中都要实现纯虚函数的啊,干脆直接在派生类中定义不是一样的吗 那就是剩下有没有“默认处理”的问题了。有,就不用纯虚函数,没有就用。但是有些功能,有很多种实现,但不好让某一种做成默认,所以就让它空着。比如look的调度器。 Protected: virtual task_t* dispatch() = 0; vritual task_t* ready(sync_t&sync,task_t* task = 0) = 0; vritual void block(sync_t& sync) = 0; look有几种调度算法,但不好说哪种能够做成默认调度算法。那样有失偏颇。所以就做成了纯虚,必须要用户选择一个派生类(调度算法)。 回到CRTP,由于CRTP不使用虚函数,那么让基类调用派生类函数的唯一方法,就是让基类知道派生类的信息。调用时,使用down cast,把基类强制成派生类,就可以调用派生类的函数了。如何将派生类的信息传递给基类呢?正确的答案是,把基类做成类模板, 模板的一个参数,就是派生类。实例化时就有了正确的信息。 template<typenameT> class A { public: void foo() { static_cast<T*>(this)->bar(); } void bar() { .... } }; class B : public A<B> { public: void bar() { ... } }; typename T就是派生类,在定义是只是一个模板参数,而当定义classB时,继承了class A,并把 B 作为模板参数传递到了class A。 在调用foo()时,实例化后的情况是: void foo() { static_cast<B*>(this)->bar() } 由于A是B的基类,所以down cast(static_cast)会成功。这就保证的安全性。 模板参数 是可以任意类型吗? 不是派生类,不让down cast。 class C : publicA<C> { ... }; C c; c.foo(); // 调用了bar吗? 它调用了哪个bar?在class C中,没有定义bar函数。C没有定义foo,我们再来实例化一下: void foo() { static_cast<C*>(this)->bar(); } A是C的基类,如果一个函数在派生类中找不到,那么自动调用基类的函数。这就有点虚函数的味道了。 定义了,就被调用,不定义,就使用基类函数。关键是,这是编译时捆绑的,没有多余开销。 有了CRTP,你可以把一个功能分成很细的粒度,每步都做成down cast调用,并且每步都有默认处理(inline的),用户想在哪步修改逻辑,就把哪步抓出来覆盖。这个是虚函数不敢想的。就像一个链条,每步都可以修改。 例子: template<typename T> struct A { void function() { static_cast<T*>(this)->major(); } void major() { static_cast<T*>(this)->minor1(); static_cast<T*>(this)->minor2(); } void minor1() { static_cast<T*>(this)->step1(); static_cast<T*>(this)->step2(); static_cast<T*>(this)->step3(); } void minor2() { static_cast<T*>(this)->step4(); static_cast<T*>(this)->step5(); static_cast<T*>(this)->step6(); } void step1() { ... } void step2() { ... } void step3() { ... } void step4() { ... } void step5() { ... } void step6() { ... } }; function()是整个功能,里面调用了major,major的默认处理调用了minor1,minor2。minor1, minor2 分别调用了各个step, 这就是整个骨架,你可以抓出某个step改写,也可以抓出minor改写,甚至抓出major改写。控制粒度可以从“整个”到“极细”。 A只是提供一套CRTP给你改写“皮肤”,“肌肉”,“骨架”的规则。 可以再看看这个USB实现的mixin withCRTP 的复杂系统例子: http://code.google.com/p/lestl/source/browse/usbd/core.hpp (下课....) |