虚拟函数是否应该被声明仅为private/protected?
问题导入
我想对于大家来说,虚拟函数并不能算是个陌生的概念吧。至于怎么样使用它,大部分人都会告诉我:通过在子类中重写(override)基类中的虚拟函数,就可以达到OO中的一个重要特性——多态(polymorphism)。不错,虚拟函数的作用也正是如此。但如果我要你说一说虚拟函数被声明为public和被声明为private/protected之间的区别的话,你又是否还能象先前一样肯定地告诉我答案呢?
其实在一开始,我和大家一样,都喜欢把虚拟函数声明为public(我并没有做太多的调查就说了这些,因为我身边的程序员们几乎都是这样做的)。这样做的好处很明显:我们可以轻而易举地在客户端(client,相对于server,server指的是我们所使用的继承体系架构,client指的就是调用该体系中方法/函数的外部代码)调用它,而不是通过利用那些烦人的using声明,或是强加给类的friend关系来满足编译器的access需求。OK,这是一个很不错的做法,简单、并且还能达到我们的要求。
但根据OO三大特性中的另一个特性——封装(encapsulation)来说(另一就是继承),需要我们将界面(interface)与实作(implementation)分开,即向外表现为一个通用的界面,而把实作上的细节封装在模块内不让client端知晓。界面与实作的分离,使得我们得以设计出耦合度更低、扩展性更好的系统来,并且还可以从这样的系统中提取出更多的可重用(reusable)的设计。
对于OO来说,封装是它的头等大事,享有最高的权利,其他的设计如果和它有着冲突,则以符合它的设计为准。这样,问题就出来了,万一我们所希望出现的多态正好是具体的实作细节并且我们不希望把它暴露给client端的话,那我们应该怎么样改动我们的设计以使得它能够适应封装的需求呢?
可行的解决办法
幸好,C++中不但支持public的虚拟函数,也有着private/protected虚拟函数。(在此我不想对于public和private/protected之间的区别多说。)前者是我们常用的形式,我也不多说,我们在此主要关心的是private/protected的虚拟函数。
你可能会有疑惑,既然虚拟函数被声明为private(protected不算,因为子类可以直接访问基类的protected成员),那子类中怎么还能对它进行重写呢?在此,你的疑虑是多余的,C++标准(也称ISO 14882)告诉我们,虚拟函数的重写与它的具体存储权限没有任何关系,即便是声明为private的虚拟函数,在子类中我们也同样可以重写它。因此,碰到上面所说的问题,我们就可以得到如下的设计:
class Base {
public:
void do_something()
{
//......
really_do_something();
//......
}
private:
virtual void really_do_something()
{
//do the polymorphism code here
}
};
class Derived: public Base {
private:
void really_do_something()
{
//do the polymorphism code here
}
};
如果我们需要从上面的设计中得到实际上的多态行为,只要象下面一样调用do_something就可以了:
//client code
Base& b; //or Base* pb;
b.do_something(); //or pb->do_something();
这样我们就得以解决了在开始处提出的那个问题。
问题引申
那就这样完结了吗?没有。相反,至此我们才开始进行我们今天的讨论。首先让我们来看看多态的实现:
void Base::do_something()
{
//......
really_do_something();
//......
}
我们可以发现,在调用真正对多态有贡献的really_do_something()之前及调用后,我们还可以在其中添加我们自身的代码(如一些控制代码等),这样我们“好像”就可以轻而易举地实现了Bertrand Meyers所提出的“Design By Contract”(DBC)[1]了:
void Base::do_something()
{
//our precondition code here
really_do_something();
//our postcondition code here
}
然后,让我们在去看看Template Method这个Pattern[2],发现所谓的Template Method也主要就是通过这种方式来进行的。于是,我们是否可以这么想呢:将所有的虚拟函数都声明为private/protected,然后再使用一个public的非虚拟函数调用它,这样,我们不就得到了上面所列出的所有好处吗?
详细分析
简单看来,好像那么做真的是好处大大的,既不会造成效率上的损失(通过将该public的非虚拟函数inline化,简单的函数转调用的开销就可以被消除掉),又能够获得上述所有的好处。何乐而不为呢?
实际上来看,有不少程序员也正是这么做的(Herb Sutter所调查的结果表明,这里面甚至还包括那些实作标准函数库的程序员们,当然,他们所考虑到的使用这种技巧的理由不会仅仅是我下面所给出的其他人的理由^_^)。有的人甚至还认为,虚拟函数就应该被声明为private/protected(当然,虚拟的析构函数不能够算在其中之列,否则就会有大乱子了)。
但让我们再仔细地考虑一下,想想一些比较极端的例子。假设我们有一个类,它拥有的虚拟函数的个数非常之多(就算它10000个吧),那即使大多数情况下只是简单的函数转调用动作,我们是否还应该为它的每一个虚拟函数都提供一个公开的非虚拟的界面呢?这时,为你的程序提供一个接口类(即没有任何成员变量,所有的方法都是纯虚函数的类)是一个不错的解决方案。
还有,因为这样做的结果将会是:基类中的那个public的非虚拟界面函数必须能够适合所有的子类的情况,这样,我们将所有的责任都推倒基类上去了,这不能算是一个好的设计方法。假设我们有了一个继承体系极深的架构,在对基类进行了多次继承后,我们突然发现,新的子类已经无法适应原有的那个界面了。于是,为了继续执行我们的虚拟函数private化,我们就将不得不把基类的代码给翻出来并改正它。幸运点的是,基类的代码是我们可以得到的,这样我们最起码还是有机会改正的(虽然有的时候,我们已经无法看懂基类中的代码了);糟糕的是,我们的基类是通过我们使用的一个函数库中得到的,而该函数库的代码我们无法获得,这个时候我们该怎么办呢?由此可见,如果在设计可能会被进行深度继承的类继承体系架构时,要想继续使用private的虚拟函数的话,对于设计基类的要求就将会变的非常之高(因为在以后,基类的任何小小改动造成的后果传递到了继承的低端时都将被显著的放大),而让设计人员去猜测以后所有的可能使用情况是件不现实的事情,这样也就容易产生脆弱的、需要被频繁改动的设计。请记住一点:FBC(Fragile Base Class)是一件可怕的事情,在我们的程序中应当避免出现这种情况。
另外,在你决定把你程序中的虚拟函数改为private/protected前,你有没有一个很好的理由呢?如果你只是说:“哦,我不知道,不过这样做可能会在以后的某天产生作用”。不错,时刻让自己的程序保持可扩展性是很好的一件事情,但那都是基于你可以预见未来的扩展之上的(这种预见主要来自于你对于该领域的深刻认识或是你平时的经验)。在没有任何理由的情况下,仅仅靠着一句“它以后可能会有用”就往自己的程序中添加进去某种特性听起来好像很炫,但实际上它可能对你的程序有百害而无一利。在我们现有的各种Framework中,有着很多类似的“以后可能会有用”的特性,结果最终都被证明为没有被使用到,这不能不说是对于开发工作的一种浪费。因此,还是让我们记住在XP[3]中所说的YNGNI(You Never Going to Need It),对于现阶段没有用到的特性,还是不要提供为好。不过,如果你能够预见到以后的扩展的话,还是请你为它留下一个可扩展的便利。
此外,基于编译器的角度来看,当你一旦改动了基类,那么所需要重新编译的就不仅仅是基类本身了,所有从该基类继承下来的派生类也都将被重新编译。这样,我们就不得不又浪费掉大量的编译时间了。尤其是当我们决定大量使用inline的方式来转调用时,所需的时间就更加多了(因为inline函数在编译时会被扩展成实际的调用代码)。这也可以算是一种语法上的FBC问题。此外,当你决定向你的继承体系中增加一个函数,并改变了基类接口的行为,你就有可能破坏了整个继承体系,并使得外部的client端代码也受到了冲击。这种情况可以算是一种语义上的FBC问题。请记住:稳定的代码永远不要建立在不稳定的代码基础之上。
现在,再让我们回到Template Method上面来看。什么时候该使用TM呢?从Design Patterns中得到它的意图为:定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。Template Method使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。这和我们所谈论的虚拟函数是不是应该为private/protected完全是不相干的,虽说在实现TM时我们会用到private/protected的虚拟函数,但并不是所有的private/protected virtual都为TM。
最后,完全使用private/protected virtual还有一个问题就是:OO中所提倡的弹性。我们知道,OO中的弹性通常都是由继承中的多态提供的,但有时我们也会使用组合中的委托。实际上已经有很多的Patterns都是这么做的了,如:Proxy, Adapter, Bridge, Decorator等。如果一味地追求private/protected virtual,势必使得我们只能在程序中使用继承了,为了一棵树而放弃一片森林的事情,我想大家也都不愿意做吧。
结论
说了半天,我也该收工了:-)现在开始进行我观点的归纳:
一般说来,把虚拟函数声明为private/protected是一个很不错的设计方法[4],但如果一旦把它作为一个唯一的Sliver Bullet来使用的话,就会产生许许多多的问题。在这篇文章中我也只是大概的谈了其中的部分,还有其他的一部分内容由于现今还没有完全整理好,也就不多说了。希望能够在下次再把它完善掉。
参考资料
1、Object-Oriented Software Construction,Second Edition, Bertrand Meyer,清华大学出版社出版(影印版)
2、设计模式可复用面向对象软件的基础, GoF, 李红军等译,机械工业出版社出版
3、Conversations: Virtually Yours, Herb Sutter & Jim Hyslop, CUJ
以及网络上相关的资料
后记
写该文的最初冲动来源于newsgroup: comp.lang.c++.moderated上面的一个讨论:Virtual methods should only be private or protected?在观看了Kevlin Henney,Herb Sutter以及James Kanze等几位大师的精彩言论后,总想把自己的感受写下来。在一开始,我倒是写了很多,但没有完全写完。近来由于比较忙的情况,因此也就慢慢地把此事差点给忘记了。不是虫虫催着我要稿的话,我想也不知道要到什么时候我才能把它给写完:-(,即便是现在,由于很久没有复习这些资料,很多的东西也没能写进去,如果大家觉得意犹未尽的话,可以直接到newsgroup中找到该thread,里面有着完整的讨论内容。
[1] 《Object-Oriented Software Construction》 Chapter 11:Design by Contract: building reliable software,国内有该书的影印版出售。
[2] 《Design Patterns: Elements Of Reusable Object-Oriented Software》,国内有该书的中文翻译版售
[3] Extreme Programming,一种轻量级的软件开发方式,注重开发中的灵活性,测试及其他……可以从下面网站上得到有关它的更多信息:www.extremeprogramming.org
[4] 可以参见于Herb Sutter和Jim Hyslop发表的Conversations: Virtually Yours一文,在CUJ站点上可以找到这篇文章,此外,在csdn中也有过它的译文。
本文地址:http://com.8s8s.com/it/it2968.htm