艾伟:闲说继承

简介: 继承已经是一个古老的话题了,不过最近又在一些地方看到有人讨论它,加上自己也有一些想法,因此形成了这篇文章。继承好不好?经典的OO理论说:继承是面向对象的三大基石之一。现代的OO理论说:组合优于继承。这两种说法显然是彼此冲突的。

继承已经是一个古老的话题了,不过最近又在一些地方看到有人讨论它,加上自己也有一些想法,因此形成了这篇文章。

继承好不好?

经典的OO理论说:继承是面向对象的三大基石之一。
现代的OO理论说:组合优于继承。

这两种说法显然是彼此冲突的。如果组合优于继承的话,那么为什么组合没有取代继承成为OO的基石呢?哪一种说法更有道理?
对这个问题,简单的说哪个比哪个更好其实是没有多大意义的。我们应当从技术发展的历史角度去看,这两种说法各自是在什么时期产生的,它们形成的背景是什么,才能对此问题有一个更加深刻的理解。

面向对象的思想形成与上个世纪70年代,但真正在软件开发阵营中流行开则是在80年代末和90年代初的时间。巧合的是,这一时间也正是以Windows 3.x为代表的图形操作系统兴起的时代。于是面向对象当时所面临的主要问题就是:如何以OO的理论封装图形界面的开发?很多重要的早期OO思想都是在这个时期形成的,包括对于继承的使用。

让我们考虑一下图形界面的特点。很容易发现:这个领域确实非常适合使用继承,因为图形对象天生就存在着is-a关系。比如,所有图像对象都是Window,所有对话框都是Dialog,所有按钮都是Button,等等。所以我们可以看到的结果就是:所有的图形界面框架都大量使用了继承,而且继承的层次通常都非常深。例如,下图是WPF中最主要的界面类——Window的继承关系,它的继承层次深达9层!

所有图形框架在继承方面几乎无一例外。Java Swing对图形框架由于较多使用MVC,因此继承的深度要浅一些,但是主要的JFrame类继承深度也达到了6层:

 

至此我们应该理解,为什么早期OO理论要将继承作为面向对象的基石了。因为当时软件开发的领域还比较狭窄,所以很多开发者根据自己在图形领域的开发经验认定:继承是OO必不可少的重要基础,并且应当尽可能的使用。


随着历史的发展,软件开发逐渐进入了两层和三层时代。程序员发现,原来在桌面应用中得心应手的继承突然之间不那么好用了。为什么呢?
原因之一:两层和三层开发的主要工作之一是对实体建模。而现实中的实体大多数是相对独立的,它们之间的关系更多的表现为实体之间的关联,而不是从属关系;
原因之二,很重要的现实问题:多层开发的主要物质基础之一——关系数据库,无法很自然的描述继承关系。事实上这也是ORM出现的重要理由之一。但即使是现在最好的ORM工具,要在数据库中描述继承关系仍然非常复杂。这迫使程序员在相当程度上放弃了继承;
原因之三:分层的开发方式逐渐流行开来,而继承造成的类属关系耦合非常不利于分层。

出于这些考虑,现代的OO理论为什么更加推荐组合而非继承,应该就容易理解了。
那么现代OO理论是不是对于继承的看法就完美了呢?我认为也不是。事实上我认为,现代OO理论存在着忽视继承的问题,很多理论书籍只是简单的告诉我们优先使用组合,而根本就不告诉我们在什么时候应当合理使用继承,什么时候不应当使用。这是从早期OO的过度使用继承跳到了另一个极端,也是不可取的。

接下类我要讲讲对于继承的几个常见的错误观念。

1. “组合优于继承。”
就一般的意义上说,这个讲法是没错的,但问题在于实在太简略了。它并没有告诉我们什么情况下组合优于继承。一个很自然的问题就是,如果组合在任何情况下都优于继承的话,那继承还有存在的必要吗?

有些情况下继承确实比组合要好。再回到图形界面的例子,Button继承于Window(这是早期MFC的叫法;在WinForm/WPF的分类中,Button继承于Control,Window通常用来定义顶层窗口),这是没有问题的,如果一定要用组合来实现Button的话,反而会导致不必要的复杂性。之所以这种情况下继承更好,根本原因是这里存在着确定的is-a关系(Button is a Window)。所以我们可以得出这样一个结论:如果语义上存在着明确的is-a关系,则考虑使用继承;如果没有,使用组合。

需要说明的是,这个结论其实也并不是完整的,原因我在后面还会继续讲到。

2. “继承的目的是为了复用。”
这个说法根本是错误的,但就是这个错误说法的流行程度简直让人吃惊。继承并不是为了复用,继承的根本目的是为了对现实世界进行更好的建模,容易复用只是优秀模型的一个必然结果而已。我们不能倒果为因,特别是,我们不应该为了复用的目的而去继承。

举一个现实的例子。汽车可以复用轮子的一些特性(比如可以Run和Stop),那么我们应当让汽车从轮子继承吗?我看到真的有一些人就是这么建模的。但是从逻辑上想一想就知道,这是非常不合理的,汽车并不是轮子。我们建立了一个错误的模型,这会让我们在以后付出代价——比如说,要让汽车能够换轮子怎么办?只好傻眼了。

再次强调:继承的目的不是复用,不应当为了能够复用而使用继承。你应当尽力去建立一个逻辑合理的模型,不应该仅仅为了方便而扭曲这个模型。

3. 只要存在is-a关系就应当使用继承
在第一点我说过:如果语义上存在着明确的is-a关系,则考虑使用继承;如果没有,使用组合。我还补充说这个结论并不完整,这里就会说明原因。

我们还是从一个例子说起。下面是许多OO书籍都会提到的一个经典例子:

 

在这个模型中,Sales和Manager都是Employee,但是它们计算薪水的方法是不同的。不同的记薪方法可以通过重载getSalary()方法来实现。

这么经典的例子有没有问题呢?有!我们可以这样想,“如果雇员被提升为经理,会怎么样?”


问题来了。在OO的世界中,对象所属的类型是这个对象的本质属性,任何对象在生命期间无法改变自己所属的类别。但是现实中对象的身份很多时候是可以改变的。我们从这里可以发现继承的一个重大问题:一旦对象的身份发生改变,那么继承层次就完全崩溃了。

那么图形界面中为什么可以使用继承呢?因为图形界面领域的对象身份是相当稳定的。Button就是Button,它不会突然变成一个顶层窗口。所以这里使用继承不会发生任何问题。但是对于类型可变的场合,继承是不适合的。

从建模的角度,我们也可以这样理解:是Sales还是Manager,并不是一个人的本质属性,它是可变的。一个人的本质属性只有他自身(姓名、性别事实上都是可变的)。我们不能够把非本质属性应用到继承层次上面。

所以上面的结论应该这样表述才算完整:如果语义上存在着明确的is-a关系,并且这种关系是稳定的、不变的,则考虑使用继承;如果没有is-a关系,或者这种关系是可变的,使用组合。

我们可以使用策略模式来将上面的例子重构为使用组合,如下图所示:

 

从上述结论我们可以看到,继承的使用的确是受到很多限制,在很多情况下也确实是组合优于继承。但是不分场合、不论条件的认为组合一定比继承好,也是过于教条主义的表现。合理的做法只有一个:具体问题具体分析。

目录
相关文章
|
6月前
|
Java C++
C++-带你初步走进继承(2)
C++-带你初步走进继承(2)
48 0
|
算法 编译器 C++
类与对象知识总结+封闭类+const+this指针 C++程序设计与算法笔记总结(三) 北京大学 郭炜(中)
类与对象知识总结+封闭类+const+this指针 C++程序设计与算法笔记总结(三) 北京大学 郭炜(中)
60 0
|
存储 算法 编译器
类与对象知识总结+封闭类+const+this指针 C++程序设计与算法笔记总结(三) 北京大学 郭炜(上)
类与对象知识总结+封闭类+const+this指针 C++程序设计与算法笔记总结(三) 北京大学 郭炜(上)
49 0
|
6月前
|
编译器 程序员 C++
C++-带你初步走进继承(1)
C++-带你初步走进继承(1)
36 0
|
5月前
|
Java
震惊!Java子类竟能“继承”父类财富,还能“自立门户”添新招!
【6月更文挑战第16天】Java的继承支持类的层次结构,子类(如`SportsCar`)继承父类(如`Car`)的属性和方法。`SportsCar`不仅拥有`Car`的基本功能,还能添加独特属性(如最高时速、加速时间)和方法(如氮气加速),并能重写父类方法以扩展功能。此机制促进代码复用,提高效率。例如,`SportsCar`调用`super.accelerate()`执行基本加速,并添加跑车特有的加速效果。
38 8
|
缓存 算法 安全
类与对象知识总结+封闭类+const+this指针 C++程序设计与算法笔记总结(三) 北京大学 郭炜(下)
类与对象知识总结+封闭类+const+this指针 C++程序设计与算法笔记总结(三) 北京大学 郭炜(下)
44 0
|
6月前
|
存储 编译器 程序员
【C++学习】类和对象(中)一招带你彻底了解六大默认成员函数
【C++学习】类和对象(中)一招带你彻底了解六大默认成员函数
|
存储 Java 编译器
【JavaSE】类和对象重点知识荟萃
【JavaSE】类和对象重点知识荟萃
青出于蓝-了不起的继承类 | 带你学《Java面向对象编程》之三十六
本节带领读者提出问题,引出疑惑后,提出了解决问题的方法-继承,为读者首次介绍了面向对象的第二大特征-继承性。
青出于蓝-了不起的继承类   | 带你学《Java面向对象编程》之三十六