艾伟_转载:闲说继承

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

继承好不好?

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

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

 

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

时间: 2024-10-01 00:52:30

艾伟_转载:闲说继承的相关文章

艾伟_转载:WCF版的PetShop之三:实现分布式的Membership和上下文传递

本系列文章导航 WCF版的PetShop之一:PetShop简介 WCF版的PetShop之二:模块中的层次划分 WCF版的PetShop之三:实现分布式的Membership和上下文传递 通过上一篇了解了模块内基本的层次划分之后,接下来我们来聊聊PetShop中一些基本基础功能的实现,以及一些设计.架构上的应用如何同WCF进行集成.本篇讨论两个问题:实现分布式的Membership和客户端到服务端上下文(Context)的传递. 一. 如何实现用户验证 对登录用户的验证是大部分应用所必需的,对

艾伟_转载:基于.NET平台的Windows编程实战(五)—— 问卷管理功能的实现

本系列文章导航 基于.NET平台的Windows编程实战(一)--前言 基于.NET平台的Windows编程实战(二)-- 需求分析与数据库设计 基于.NET平台的Windows编程实战(四)-- 数据库操作类的编写 基于.NET平台的Windows编程实战(五)-- 问卷管理功能的实现 基于.NET平台的Windows编程实战(六)-- 题目管理功能的实现 首先,为了使我们的界面更加便于操作及布局,我们引入第三开源控件DockPanel[当然也可以不引入控件而直接进行开发],你可以从这里:Do

艾伟_转载:基于.NET平台的Windows编程实战(六)—— 题目管理功能的实现

本系列文章导航 基于.NET平台的Windows编程实战(一)--前言 基于.NET平台的Windows编程实战(二)-- 需求分析与数据库设计 基于.NET平台的Windows编程实战(四)-- 数据库操作类的编写 基于.NET平台的Windows编程实战(五)-- 问卷管理功能的实现 基于.NET平台的Windows编程实战(六)-- 题目管理功能的实现 申明:本系列课程是专为新手们写来入门练习用的,目的是想通过一个完整的问卷调查管理系统的案例开发来让新手们了解.加深或是熟悉软件项目的开发流

艾伟_转载:数组排序方法的性能比较(上):注意事项及试验

昨天有朋友写了一篇文章,其中比较了List的Sort方法与LINQ中排序方法的性能,而最终得到的结果是"LINQ排序方法性能高于List.Sort方法".这个结果不禁让我很疑惑.因为List.Sort方法是改变容器内部元素的顺序,而LINQ排序后得到的是一个新的序列.假如两个排序方法的算法完全一致,LINQ排序也比对方多出元素复制的开销,为什么性能反而会高?如果LINQ排序的算法/实现更为优秀,那为什么.NET Fx不将List.Sort也一并优化一下呢?于是今天我也对这个问题进行了简

艾伟_转载:一个MVC分页Helper

本人写的一个分页Helper,支持普通分页(也就是,首页.上一页.下一页.末页等),综合分页(普通分页和数字分页的综合).下面是分页效果: 分页代码: PagerHelper.cs 代码   1 using System;  2  using System.Collections.Generic;  3 using System.Collections.Specialized;  4 using System.Linq;  5 using System.Web;  6 using System.

艾伟_转载:Cookies揭秘

一,前言 Cookies想必所有人都了解, 但是未必所有人都精通.本文讲解了Cookies的各方面知识, 并且提出来了最佳实践.这是笔者在日常工作中的积累和沉淀.   二,基础知识 1.什么是Cookies Cookie 是一小段文本信息,伴随着用户请求和页面在 Web 服务器和浏览器之间传递.Cookie 包含每次用户访问站点时 Web 应用程序都可以读取的信息. 例如,如果在用户请求站点中的页面时应用程序发送给该用户的不仅仅是一个页面,还有一个包含日期和时间的 Cookie,用户的浏览器在获

艾伟_转载:把委托说透(2):深入理解委托

在上一篇随笔中我们通过示例逐步引入了委托,并比较了委托和接口.本文将重点剖析委托的实质. 委托在本质上仍然是一个类,我们用delegate关键字声明的所有委托都继承自System.MulticastDelegate.后者又是继承自System.Delegate类,System.Delegate类则继承自System.Object.委托既然是一个类,那么它就可以被定义在任何地方,即可以定义在类的内部,也可以定义在类的外部. 正如很多资料上所说的,委托是一种类型安全的函数回调机制, 它不仅能够调用实

艾伟_转载:趣味问题:你能用Reflection.Emit生成这段代码吗?

众所周知,Reflection.Emit是非常强大的工具,可以在运行时动态生成各种程序集.类型和方法的IL代码,几乎无所不能.原先我也是这样认为的,但是看了某个人的博客之后我发现想要用Reflection.Emit做一些特殊的事情还是很需要技巧性的.假设你还没有看过那个人的博客(暂时先不公开--)可以尝试一下这个问题.下面的代码可以用vbc.exe正确编译(当然等价C#程序也可以经试验C#编译器无法处理该逻辑,各位参照VB的行为吧)... Class AImplements B.IEnd Cla

艾伟_转载:探索.Net中的委托

废话 我本来以为委托很简单,本来只想简简单单的说说委托背后的东西,委托的使用方法.原本只想解释一下那句:委托是面向对象的.类型安全的函数指针.可没想到最后惹出一堆的事情来,越惹越多,罪过,罪过.本文后面一部分是我在一边用SOS探索一边记录的,写的非常糟糕,希望您的慧眼能发现一些有价值的东西,那我就感到无比的荣幸了. 委托前世与今生 大家可能还记得,在C/C++里,我们可以在一个函数里实现一个算法的骨架,然后在这个函数的参数里放一个"钩子",使用的时候,利用这个"钩子"