编程那点事编程那点事

专注编程入门及提高
探究程序员职业规划之道!

对象导论:继承

对象这种观念,本身就是十分方便的工具,使得你可以通过概念将数据和功能封装到一起,因此可以对问题空间的观念给出恰当的表示,而不用受制于必须使用底层机器语言。这些概念用关键字class来表示,他们形成了编程语言中的基本单位。

这样做还是有很多麻烦:在创建了一个类之后,即使另一个新类与其具有相似的功能,你还是得重新创建一个新类。如果我们能以现有的类为基础,复制它,然后通过添加和修改这个副本来创建新类那就要好多了。通过继承便可以达到这样的效果,不过当源类(被称为基类、父类或者超类)发生变动时,被修改的“副本”(被称为导出类、继承类、或子类)也会反映出这些变动。

基类和导出类的UML图

类型不仅仅只是描述了作用于一个对象集合上的约束条件,同时还有与其他类型之间的关系。两个类型可以有相同的特性和行为,但是其中一个类型可能比另一个含有更多的特性,并且可以处理更多的消息(或以不同的方式来处理消息),继承使用基类型和导出类型的概念表示了这种类型之间的相似性。一个基类型包含其所有导出类型所共享的特性和行为。可以创建一个基类型来表示系统中某些对象的核心概念,从基类型中导出其他类型,来表示此核心可以被实现的各种不同方式。

以用来归类散落的垃圾的垃圾回收机制为例,“垃圾”是基类型,每一件垃圾都有重量、价值等特性,可以被切碎、融化或分解。在此基础上,可以通过添加额外的特性(例如瓶子有颜色)或行为(例如铝罐可以被压碎,铁罐可以被磁化)导出更具体的垃圾类型。此外,某些行为可能不同(例如纸的价值取决于其类型和状态)。可以通过使用继承来构建一个类型层次结构,以此来表示待求解的某种类型的问题。

第二个例子是经典的在计算机辅助设计系统或游戏仿真系统中可能被用到的几何形例子,基类是几何形,每一个几何形都具有尺寸、颜色、位置等,同时每一个几何形都可以被绘制、擦除、移动和着色等。在此基础上,可以导出(继承出)具体的集合形状---圆形、正方形、三角形等----每一种都具有额外的特性和行为,例如某些形状可以被翻转。某些行为可能并不相同,例如,计算几何形状的面积。类型层次结构同时体现了几何形状的面积。类型层次结构同时体现了几何形状之间的相似性和差异性。

以同样的术语将解决方案转换成问题是大有裨益的,因为不需要在问题描述和解决方案描述之间建立许多中间模型。通过使用对象,类型层次结构成为了主要模型,因此,可以直接从真实世界中对系统的描述过渡到用代码对系统进行描述。事实上,对使用面向对象设计的人们来说,困难之一是从开始到结束过于简单。对于训练有素、善于寻找复杂的解决方案的头脑来说,可能会在一开始被这种简单性给难倒。

当继承现有类型时,也就创造了新的类型。这个新的类型不仅包括现有类型的所有成员(尽管private成员被隐藏了起来,并且不可访问),而且更重要的是它复制了基类的接口。也就是说,所有可以发送给基类对象的消息同时也可以发送给导出类对象。由于通过发送给类的消息的类型可知类的类型,所以这也就意味着导出类与基类具有相同的类型。在前面的例子中,“一个圆形也就是一个几何形”。通过继承而产生的类型等价性是理解面向对象程序设计方法内涵的重要门槛

由于基类和导出类具有相同的基础接口,所以伴随此接口的必定有某些具体实现。也就是说,当对象接收到特定消息时,必须有某些代码去执行。如果只是简单地继承一个类而并不做其他任何事,那么在基类接口中的方法将会直接继承到导出类中。这意味着导出类的对象不仅与基类拥有相同的类型,而且还拥有相同的行为,这样做没有什么特别意义。

有两种方法可以使基类与导出类产生差异。

第一种方法非常直接:直接在导出类中添加新方法。这些新方法并不是基类接口的一部分。这意味着基类不能直接满足你的所有需求,因此必须添加更多的方法。这种对继承简单而基本的使用方式,有时对问题来说确实是一种完美的解决方式。但是,应该仔细考虑是否存在基类也需要这些额外方法的可能性。这种设计的发现与迭代过程在面向对象程序设计中会经常发生。虽然继承有时可能意味着在接口中添加新方法(尤其是在以extends关键字表示继承的Java中),但并非总需如此。

第二种也是更重要的一种使导出类和基类之间产生差异的方法是改变现有基类的方法的行为,这被称之为覆盖那个方法。

要想覆盖某个方法,可以直接在导出类中创建该方法的新定义即可。你可以说:“此时,我正在使用相同的接口方法,但是我想在新类型中做些不同的事情”

“是一个”与“像一个”关系

对于继承可能会引发某种争论:继承应该只覆盖基类的方法(而并不添加在基类中没有的新方法)吗?如果这样做,就意味着导出类和基类是完全相同的类型,因为它们具有完全相同的接口。结果可以用一个导出类对象来完全替代一个基类对象。这可以被视为纯粹替代,通常称之为替代原则。在某种意义上,这是一种处理继承的理想方式。我们经常将这种情况下的基类与导出类之间的关系成为is-a关系,因为可以说“一个圆形就是一个几何形状”。判断是否继承,就是要确定是否可以用is-a来描述类之间的关系,并使之具有实际意义。

有时必须在导出类型中添加新的接口元素,这样也就扩展了接口。这个新的类型仍然可以替代基类,但是这种替代并不完美,因为基类无法访问新添加的方法。这种情况下我们可以描述为is-like-a关系。新类型具有旧类型的接口,但是它还包含其他方法,所以不能说他们完全相同。以空调为例,假设房子里已经布线安装好了所有的冷气设备的控制器,也就是说,房子具备了让你控制冷气设备的接口。想象一下,如果空调坏了,你用一个既能制冷又能制热的热力泵替换了他,那么这个热力泵就is-like-a空调,但是它可以做更多的事。因为房子的控制系统被设计为只能控制冷气设备,所以它只能和新对象中的制冷部分进行通信。尽管新对象的接口已经被扩展了,但是现有系统除了原来接口之外,对其他东西一无所知。

当然,在看过这个设计之后,很显然会发现,制冷系统这个基类不够一般化,应该将其更名为“温度控制系统”,使其可以包括制热功能,这样我们就可以套用替代原则了。这张图说明了在真实世界中进行设计时可能会发生的事情。

当你看到替代原则时,很容易会认为这种方式(纯粹替代)是唯一可行的方式,而且事实上,用这种方式设计是很好的。但是你会时常发现,同样显然的是你必须在导出类的接口中添加新方法。只要仔细审视,两种方法的使用场合应该是相当明显的。


未经允许不得转载: 技术文章 » Java编程 » 对象导论:继承