Shuhari

闲说继承



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

继承好不好?

经典的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关系,或者这种关系是可变的,使用组合。

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


 

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

 

标签: OO, 面向对象

posted on 2009-06-04 11:32 Shuhari 阅读(1479) 评论(24) 编辑 收藏

评论

#1楼 2009-06-04 11:50 都市放羊      

拜读了!~
期望博主再接再厉,多写好文章。
 回复 引用 查看   

#2楼 2009-06-04 11:57 徐少侠      

好东西

以前我就是死抱继承大腿的,后来在几个博友的文章里逐渐发现了组合的概念

现在看到这文章就是有亲切感啊
 回复 引用 查看   

#3楼 2009-06-04 11:58 Nick Wang (懒人王)      

写得很好  回复 引用 查看   

#4楼 2009-06-04 12:17 钧梓昊逑      

描述清晰、准确  回复 引用 查看   

#5楼 2009-06-04 13:38 老姜      

清晰明了,好文  回复 引用 查看   

#6楼 2009-06-04 13:55 lujunjie[未注册用户]

很久没有看到这么好的文章了  回复 引用   

#7楼 2009-06-04 15:37 蒙蒙234[未注册用户]

不管是设计模式解析或者headfist设计模式都能通俗易懂的给出优先使用组合而不是继承的道理。
从结构上看继承和组合:
继承是纵向的上定义子类接口,不灵活的地方就是这依赖父类接口,组合把发展方向横向话,把变化提早包装进最初的父类,然后横向发展。
从使用意图上看继承和组合:
当一个类确实非常像一个,而不有一个的就要用继承了,举个使用组合的例子,先定义一个人类,然后你发先有的人有穿裙子,有的是裸体的,这时你如果用继承也许可以搞定,但是你要知道这里穿裙子的人作为人类的子类是不是感觉怪怪的,如果我们把裙子抽象成衣服这个类,那么是不是一个人类对象包含或者说拥有衣服这个对象比较更加合适,此时感觉组合是比继承更加合适的地方。
ps:楼主画的图很好看,是用什么工具可否告知下。
 回复 引用   

#8楼 2009-06-04 15:39 清风听雨      

mark  回复 引用 查看   

#9楼 2009-06-04 16:39 jacky song      

其实第一个例子没有错吧,一个人不可能同时兼顾sales和manager两种角色,就算是sales升级为manager,那他就是manager啊  回复 引用 查看   

#10楼 2009-06-04 16:47 1-2-3      

非常精彩,拜读了。  回复 引用 查看   

#11楼[楼主] 2009-06-04 16:59 Shuhari      

--引用--------------------------------------------------
蒙蒙234:ps:楼主画的图很好看,是用什么工具可否告知下。

--------------------------------------------------------

用的是NetBeans的UML插件.



--引用--------------------------------------------------
jacky song: 其实第一个例子没有错吧,一个人不可能同时兼顾sales和manager两种角色,就算是sales升级为manager,那他就是manager啊
--------------------------------------------------------

Sales s = new Sales("张三");
// 接下来你怎么把这个Sales变成Manager?
 回复 引用 查看   

#12楼 2009-06-04 17:41 徐少侠      

--引用--------------------------------------------------

Sales s = new Sales("张三");
// 接下来你怎么把这个Sales变成Manager?

--------------------------------------------------------

的确
分清楚固有属性和可变化的部分


 回复 引用 查看   

#13楼 2009-06-04 18:00 我厂制造

继承这个概念,包含了接口继承和实现继承两个方面.
所谓"继承是oo的基石"的继承,是指接口继承,接口继承提供了一种相同接口不同实现的类型机制,这是保证oo能多态的根本;
而所谓"组合优于继承"的继承,是指实现继承,也就是如何对已有类进行复用的策略;
这两者并不冲突.更何来经典oo现代oo之分?

之所以说组合优于继承是因为如果用继承的方式来复用已有类,就必然和已有类的实现产生紧耦合,而组合的方式可以消除这种耦合,也就是说组合的方式可以在运行时替换实现.因此组合优于继承.

接口继承是为了多态,实现继承是为了复用,所以说"继承就是为了复用"虽然以偏概全,但也不能说就是错误,继承就是为了复用也不等于复用就必须继承.

至于何时使用继承,何时使用组合,不要过分执着于is-a这样含混的自然概念,从已有类和新类的职责,接口,实现之间的关系去分析更贴近oo思想的出发点
比如组合虽然优于继承,但并不等于我们任何时候都要消除继承带来的耦合,任何时候都需要在运行时替换父类的实现,这种时候使用继承就比组合方便而且类之间的联系更直观,比如.net基本框架类,已有类都非常稳定,不必考虑其实现发生变化,新类型往往是为了在已有类的实现基础上扩展更多的功能,而不是为了替换已有类的实现,因此不必使用组合直接继承即可.


感觉楼主对oo的分析太依赖于现实中存在的概念和关系了
 回复 引用   

#14楼[楼主] 2009-06-04 19:37 Shuhari      

--引用--------------------------------------------------
我厂制造: 继承这个概念,包含了接口继承和实现继承两个方面.
所谓"继承是oo的基石"的继承,是指接口继承

--------------------------------------------------------

"继承、封装和多态是面向对象的三大基石",这种说法早在上世纪80年代就出现了,而当时接口的的概念在OO语言中(主要是C++)根本还不存在。你这种提法我不知道是从哪里来的,也无法评论。

类和接口的关系是另外一个维度的问题,我自己并不把基于接口的实现叫做继承,因为不希望将实质上完全不同的两个概念都叫做同一个名字,以免混淆。关于类和接口的问题我同意你的看法,不过这已经不是本文的主题了。
 回复 引用 查看   

#15楼 2009-06-04 19:50 Ltaos      

面向对象的思想,其实很简单,实质就是复用。然后,根据复用,再进行思考和展开。无论是从日常生活和工作中,使用这样的思维非常有效“继承的目的是为了复用。”这样的表达不好,狭隘了。最好还是面向对象即复用的思想要好用些。以后无论是继承、还是接口,都是在复用的思想之后。
实用例一:比如我们学习编程的时候,如果一味的学习,能够理解,但是却不利于回顾。如果按照复用的思想,进行整理,就相当有效。把它按主题封装成了一个对象,以后可以回顾和调用。
实用例二:MSDN上面的表达。有时候,看上去不那么连贯,断断续续的。后来明白了面向对象的思想后,理解了,那样表达是为了易复用、好升级。
 回复 引用 查看   

#16楼 2009-06-04 21:03 EricZhang(T2噬菌体)      

很精辟!我以前没有意识到继承需要用在稳定的成员上面,看了这篇文章,让我对继承的认识又加深了不少。  回复 引用 查看   

#17楼 2009-06-04 21:43 htqx[未注册用户]

本文作者提出根据是否本质属性来划分如何用继承和组合设计,感觉不错,值得参考。

但是我觉得,继承也许是一种误用,设计价值不高,因为继承实际上等于接口+实现,而没有完全的抽象。我觉得,如果语言修改的比较友好,将继承完全用接口来取代,会更加合理。
 回复 引用   

#18楼 2009-06-04 23:31 自由飞      

--引用--------------------------------------------------
htqx: 本文作者提出根据是否本质属性来划分如何用继承和组合设计,感觉不错,值得参考。

但是我觉得,继承也许是一种误用,设计价值不高,因为继承实际上等于接口+实现,而没有完全的抽象。我觉得,如果语言修改的比较友好,将继承完全用接口来取代,会更加合理。

--------------------------------------------------------
这个过了吧?
纯接口如何实现复用呢?
 回复 引用 查看   

#19楼 2009-06-05 09:16 月照孤周      

也可以理解为,到底将员工类型作为一个 属性 来表达,还是作为 类 来表达,但要区分这个并不容易  回复 引用 查看   

#20楼 2009-06-05 12:58 我厂制造

@Shuhari
先不说为什么接口和继承是不可分割的,我只说你文章荒谬的地方.
所谓"类型可变的场合继承不适用",请问什么叫类型可变,如果是说一个sale类型的对象不能把自己变成一个manager类型,那么在你使用策略模式的例子中,一个salesstrategy对象就能把自己的类型变成一个managerstrategy类型吗?不可能吧,变的是employee.salarystrategy的引用而已,如果你说的可变是指这种对象引用可变,那么使用继承以一个employee引用一个sale对象,一样能变成引用一个manager对象,也能实现sale的薪水计算方式变换成manager的薪水计算方式.凭什么继承层次就崩溃了?
再所谓"如果语义上存在着明确的is-a关系,并且这种关系是稳定的、不变的,则考虑使用继承;如果没有is-a关系,或者这种关系是可变的,使用组合。"
一个sale是一个employee,这是明确的稳定的吧?一个manager也是一个employee,也是不变的吧,至少在你的例子中没体现出不稳定,那么何以sale和manager继承自employee就不妥当了?你这不是自相矛盾吗?
 回复 引用   

#21楼[楼主] 2009-06-05 14:28 Shuhari      

@我厂制造:

你的问题很容易回答,但是我先不作答,因为我觉得你现在的口气不是在讨论。

我希望你先冷静一下,自己想想这个问题。不要把我放在敌对的立场上说话,那样讨论就没有意义了。


 回复 引用 查看   

#22楼 2009-06-05 19:08 我厂制造

@Shuhari
讨论是个很好玩的事情,干嘛要敌对,干嘛要不冷静:)
我是觉得继承这个话题其实很有嚼头,而你的文章我感觉有一些地方不太明确,有些地方值得商榷,有一些地方我不认同,所以在上一个回复写了一大段不同意见就是希望一起来讨论,但是似乎针对性不够强,你好像并没有明白我的疑惑在哪里,所以就改为直接对我不理解的地方提问而已.
 回复 引用   

#23楼[楼主] 2009-06-06 21:18 Shuhari      

@我厂制造:

好的,那么我回答一下你的疑问。

第一个问题,策略对象生来就是为了被替换,这在设计模式中开宗明义已经讲明了。而策略对象之所以有用,正是因为策略所应用的主体通常不具有可替换性。雇员对象是一个实体,实体只能够代表它自己,任何人不管职位怎么变,他还是他自己,不会无缘无故变成另一个人。你不能够把一个雇员Kill掉,换上一个经理,然后说这个经理就是原来的雇员,那是非法的(也是违背人道的)。但是替换策略则是完全合理、也是理所应当的,就像你每天都可以换衣服一样。

第二个问题,你陷入了一个传统OO的一个典型陷阱:一个模型是否合法,不能够静态、孤立的看,而必须从所处的实际场景来判断。“Sales是一个Employee”,这在生活常识里是完全合理的,但在这个对象建模的情况下则是错误的,因为它无法应对变化。这个结果充分说明,有时候OO得出的模型就是与直觉相悖的。再举个大家常说的例子:在几何学上正方形是长方形的一种,但是为什么OO却得出结论说,正方形不是长方形呢?我觉得你之所以有这个问题,应该是误解了“稳定”的意思。这里的稳定是说在具体的场景下,作为实体对象能否保持其所属关系稳定不变。不是像你理解的那样,简单看两个类之间有什么关系,就认为它是不是是稳定了。像我前面说的,一个静止的模型无法判断它是否合理,必须从实际的使用场合去判断。
 回复 引用 查看   

#24楼 2010-12-31 10:49 一线风      

解惑了,哎,我也一直郁闷为啥老是看到组合比继承好,具体怎么个好法却没有人说。  回复 引用 查看   

<2009年6月>
31123456
78910111213
14151617181920
21222324252627
2829301234
567891011

导航

统计

公告

昵称:Shuhari
园龄:2年8个月
粉丝:1
关注:0

搜索

 
 

常用链接

我的标签

随笔分类

随笔档案

相册

最新评论

阅读排行榜

评论排行榜

推荐排行榜