谈谈继承的局限性
有一种普遍的说法是把封装、继承和多态并称为面向对象的三大特征。如果你很熟悉C++并且对面向对象思想有过一些思考,那么很可能对这个说法有过怀疑,面向对象思想在本质上认为世界是由对象构成的,和面向过程是世界观的不同,而所谓的三大特征实际和面向对象的思想本质没有半毛钱的关系,准确的表述应该是封装、继承和多态是C++相对于C的三大特征。如果你碰巧了解一点C++编译器可能会发现封装也好,继承、多态也好都只是语法糖,技巧层面的东西而已,和思想无关。
以上为废话。
本文主要就C++的继承机制进行一些讨论。很多C++教材在讲到继承时喜欢利用几何上的一些概念,比如对如下的集合关系进行建模:
在一次内部技术培训的时候我提出这个问题,结果大家都沉默不语,于是我特意找了个新手程序员回答,他给出了我预料之中的答案:以四边形作为基类,矩形和正方形依次继承下来。没人表示同意也没人表示反对,可能所有的人第一反应得出的都是这个方案,但老鸟程序员会马上察觉出其中的不妥,即使他给不出更好的方案。
实际上在没有给定需求场景的情况下你永远无法设计出一个类,更不要说设计一组类和这些类的层次关系。很多教材都有这个毛病,上来就设计Student、Teacher而没说要完成的功能是什么,即使是做了许多年C++之后再回过头来看那些例子还是晕忽忽的,何况初学者,——当然这也许仅仅是因为我自己太笨。
我们设定两个简单的需求取边长和计算面积,暂时不考虑继承关系而分别实现三个类,那么它们是下面这个样子的:
1 class Quadrangle 2 { 3 public: 4 int GetSideLength(int index); 5 int GetArea(void); 6 7 private: 8 int m_arrSlide[4]; 9 }; 10 11 class Rectangle 12 { 13 public: 14 int GetWidth(void); 15 int GetHeight(void); 16 int GetArea(void); 17 18 private: 19 int m_nWidth; 20 int m_nHeight; 21 }; 22 23 class Square 24 { 25 public: 26 int GetWidth(void); 27 int GetArea(void); 28 29 private: 30 int m_nWidth; 31 };显然,正方形四边形Square 的实现最简单,四边形Quadrangle的实现最复杂(知道四条边长能确定唯一的四边形吗?)。继承机制有一个特点:派生类总是比基类更复杂,因为派生类是在完整的继承了基类实现的基础上增加新的成员、方法。由四边形到矩形再到正方形却是越来越简单,这就形成了一个悖论,导致我们无法按照继承的层次描述三者的关系。
一个老到的程序员会告诉你最好分别实现三个类,不考虑三个者之间的关系,这在大部分场景中是可行的。如果确实需要描述三者之间的层次关系,我能想到的最好的方式是使用接口:
接口用来描述层次关系,各个类独立实现。由此也可以看出,虽然C++中的接口是用纯虚类继承实现的,但实际上接口机制和继承机制是两种完全不同的东西。
到现在为止,我们可以得出结论:继承机制实际上很难描述现实概念的层次关系,这是它的局限性。对继承的应用很多情况下并不是为了描述真实概念的层次关系,而只是组织代码的一种形式。比如可以尝试下用继承关系描述一棵进化树,你会发现这个基本上很难。
在C++中引入继承机制的目的是什么,大部分资料对此都语焉不详,但是无论如何至少有一半的目的是为了组织和复用代码,继承扩展是很常用的手法,在MFC、WTL等框架中到处可以看到这样的代码。但是在实际的应用中一定要避免单纯为了复用代码而使用继承机制,典型的案例比如窗口和控件。
窗口和控件是两种完全不同的东西,微软为了复用消息机制把两个概念硬是揉在了一起,所有的控件都从窗口继承下来,这直接导致了GUI框架的高复杂度和难以扩展。
代码复用只是良好设计的副产品而不应该是设计本身的目的。
最后总结一下本文的核心观点:
1. 继承机制有很大的局限性,难于描述现实概念的层次关系;
2. 使用继承时避免生搬硬套现实概念的层次关系;
3. 避免单纯以代码复用为目的使用继承
微博:@飞舞的烟灰缸
补充:软件开发 , C语言 ,