作为一名程序员,会不可避免地碰到不同的类具有相同的特点,或者看起来相互之间有一定的关系。面向对象的编程语言提供许多技术来处理类间的这种关系。比较令人迷惑的部分就是理解 这些关系到底是什么?有两种主要的类间关系--复合关系(has-a)与继承关系(is-a)。
1、复合关系(has-a)
具有复合关系(has-a)的类就是模式A拥有B,或者是A包含B。在这种关系中,你可以认为一个类是另一个类的一部分。就是我们前面定义的部件,一般来说,就是一个复合(has-a)关系,因为它描述的类都是由其他类组成的。
现实世界中的这种关系的例子比如动物园与猴子。你可以说动物园里有猴子或者动物园中包含了猴子。在代码中模拟动物园就是一个有猴子部件的动物园类。
通常情况下,考虑一下用户接口的场景对理解类之间的关系很有帮助。因为即使不是所有的用户接口都使用了OOP(但现如今,大部分都是),屏幕上的可视化的元素都转化成了类。一个对于复合关系的UI类比就是窗口包含一个按钮。按钮和窗口很明显是两种单独的类,但是它们很明显在某种程度上相关。因为按钮在窗口中,你可以说窗口中有一个按钮。
下图展示了真实世界中用户接口的复合关系。
复合关系有两种:
- 聚合关系:对于聚合关系,聚合对象(部件)在聚合器被破坏的情况下仍可生存。例如一个动物园对象包含有许多动物对象。当动物园对象因为破产被破坏了,动物对象理想情况下不会被破坏,因为他们可以转移到其他动物园。
- 组合关系:对于组合关系,如果组合了其他对象的对象被破坏了,那些其他对象也就被破坏了。比如一个包含了按钮的窗口对象被破坏了,按钮对象也就被破坏了
上述关系还可以用一个更简单易懂的例子来说明,前苏联是由许多加盟共和国组成,前苏联与加盟共和国的关系就是组合关系,前苏联解体了,各加盟共和国纷纷独立,也就不存在了。而存在过的一段时间的独联体与各加盟共和国之间的关系就是聚合关系,短暂的独联体消失了,对各加盟共和国几乎没有什么影响,各加盟共和国还依然存在。
2、继承关系(is-a)
继承关系(is-a)在面向对象编程中是一个非常基础的概念,除了继承这个命名之外,还有像派生关系,子类关系,扩展关系等等。类将现实世界中包含了具有属性与行为的对象模型化。继承关系模型化了那些以层次结构进行组织的对象。这种层次结构展示了继承关系。
从最基础的讲起,继承就是模式A是B或A非常像B--是不是有点懵逼。还是前面说的那个动物园的简单的例子,但是呢,假设除了猴子还有其他动物。这句话本身就构造了这种关系--猴子是动物。同样的,长颈鹿是动物,袋鼠是动物,企鹅是动物。然后呢?好吧,继承的魔法就来自于你意识到猴子、长颈鹿、袋鼠和企鹅有共同的东东。这些共同性就是动物的共通的特点。
对于程序员来说就意味着可以定义一个Animal类来包装所有这些适用于每一种动物的属性(大小、位置、饲料等等)以及行为(移动、吃、睡觉)。某种动物,比如猴子,就成为了Animal的具有动物的所有特点的派生类。记住,猴子是一种加上了具有另外使其与众不同的特点的动物。下图展示了动物的继承关系。箭头显示了继承的方向。
就像猴子与长颈鹿是不同类型的动物一样,用户接口也有不同的按钮。例如,选择框也是一个按钮。假定一个按钮只是一个简单的UI元素,可以单击执行一个动作,选择框扩展了按钮类,增加了状态--勾选与未勾选。
当分析继承关系的类关联时,目标就是将共同功能全部放入基类中,其他类由基类扩展。如果你发现派生类具有类似或者就是一样的代码,考虑要把其中一些或者全部代码移到基类中。这样的话,所有需要对基类的修改只需要改一次,以后派生类就可以“免费”获得这些共享的功能了。
有时候,类之间是复合关系还是继承关系是很清晰的,但也有时候不是那么清晰。如果必须选择的话,还是要先用复合关系,我们以后慢慢讲吧。
2.1、继承技巧
前面的例子只是点到了一些用于继承的技巧,但没有详细讲述。对类进行派生时,程序员有几种方式可以将一个类与它的父类,也可以叫做基类或超类进行区分。派生类可以使用一种或多种技巧,通过完成“A是B,B怎么样...”的句子就可以达到。
2.1.1、增加功能
派生类可以通过增加另外的功能来扩大父类。例如,猴子是一种可以在树上爬来爬去的动物。除了Animal的成员函数外,猴子类也有一个swingFromTrees()的成员函数,并且它只对猴子类有效。
2.1.2、替换功能
派生类可以完全替换或者覆盖父类的一个成员函数。例如,大部分动物通过走路进行移动,所以你可能给Animal类一个move()的成员函数来模拟走路。如果是这样的话,袋鼠就不是通过走路而是通过跳来进行移动的动物。而Animal基类其他的属性和成员函数依然有效,但袋鼠派生类只是改变了move()成员函数走路的方式。当然了,如果你发现替换了基类的所有的功能,那就意味着继承可能就不是一个正确的选择了,除非基类是一个抽象基础类。抽象基础类强制每一个派生类去实现在抽象基础类中没有实现的所有成员函数。不能创建抽象基础类的实例。
2.1.3、增加属性
派生类也可以增加继承于基类的新的属性。例如,企鹅除了具有动物的所有属性外,还可以有一个喙的长度的属性。
2.1.4、替换属性
C++提供了类似于覆盖成员函数的方式来对属性进行覆盖。然而,这么做基本上不合适,因为它隐藏了基础类的属性;也就是说,基础类对于特定名字的属性有了一个特别的值,而派生类可以对另一个具有同样名字的属性有了一个另外的值。说白了,就是你楞是要给手机的颜色属性赋一个江苏的值,你就说你迷糊不迷糊吧?
2.2、多态
多态是指对象符合可以替换的标准属性集与成员函数的符号表示。类的定义就像类和与其交互的代码之间的合同。根据定义,猴子对象一定要支持猴子类的属性与成员函数。
这个符号扩展到了基础类。因为猴子是动物,所以猴子对象也支持动物类的属性与成员函数。
多态是面向对象编程中一个很美的部分,因为它真正地获得了继承提供的优势。在动物园的类比中,你可以编程去循环动物园中的所有动物,让每个动物移动一次。因为所有的动物都是动物类的成员,他们都知道怎么去移动。有些动物覆盖了移动的成员函数,但这就是最棒的部分----代码只是告诉动物去移动而不用知道也不关心它是什么类型的动物。每个动物只有自己知道怎么去移动。
可能有的同学在看最近的博文的时候可能会提一个问题,怎么没有代码了。这个问题算是提到点儿上了。我们是要学C++的精髓,而不是C++的语法。在漫长的C++的学习过程中,工欲善其事必先利其器。而这个器在我的笔记中,更多的是从根儿上要彻底地掌握C++的精髓。C++我也用了好多年,也学了好多遍,从内心讲,觉得还是没有掌握,为什么呢,就是因为以前的学习和使用太过于关注那些表面的东东,那些细节的语法。而在最根本的OOP方面,却反而没有掌握到家。成大事者,除了孟子的那些苦其心志、劳其筋骨之类的鸡汤之外,我觉得掌握核心的东西才是本心。我们要在最基本的OOP的思想上面花费一些功夫,也算是磨刀不误砍柴工吧,愿你我在C++的学习之路上共同奋斗,以此共勉!!!