改善基础类
尽管extends关键字暗示着我们要为借口"扩展"行功能,但实情并非肯定如此。为区分我们的新类,第二办法是改变基础类一个现有函数的行为
。我们将其作称作"改善"那个函数。为改善一个函数,只需为衍生类的函数建立一个新定义即可。我们的目标是:"尽管使用的函数借口未变,但它的新版本具有不同的表现"。
等价与类似关系(衍生类 子类)
则衍生类型就是基础类完全相同的类型,因为都拥有完全相同的借口。我们完全能够将衍生类的一个对象换成基础类的一个对象!可将其想象成
一种"纯替换"。在某种意义上,这是进行继承的一种理想方式。通常认为基础类和衍生类之间存在一种"等价"关系--因为我们可以理直气壮的
说:"圆就是一种集合形状"。
但在许多时候,我们必须为衍生类型加入新的借口元素,所以不仅扩展了借口,也创建了一种新类型。这种新类型仍可替换成基础雷西兴,
但在许多时候,我们将其称作"类似"关系;新类型拥有旧类型的接口,但也包含了其他函数,所以不能说它们完全等价的。
举个例子来说,让我们考虑一下制冷机的情况。假定我们的房间连好了用于制冷的各种控制器;也就是说,我们已拥有必要的“接口”来控制制冷。现在假设机器出了故障,我们把它换成一台新型的冷、热两用空调,冬天和夏天均可使用。冷、热空调“类似”制冷机,但能做更多的事情。由于我们的房间只安装了控制制冷的设备,所以它们只限于同新机器的制冷部分打交道。新机器的接口已得到了扩展,但现有的系统并不知道除原始接口以外的任何东西。
认识了等价与类似的区别后,再进行替换时就会有把握得多。尽管大多数时候“纯替换”已经足够,但您会发现在某些情况下,仍然有明显的理由需要在衍生类的基础上增添新功能。通过前面对这两种情况的讨论
1.6多形对象的互换使用
通常,继承最终会以创建一系列收场,所有类都建立在统一的接口基础上。
对这样的一系列类,我们要进行的一项重要处理就是讲衍生类的对象当作基础类的一个对象对待。。这一点是非常重要的,因为它意味着我们只需编写单一的代码,令其忽略类型的特定细节,只与基础类打交道。这样一来,那些代码就可与类型信息分开。所以更易编写,也更易理解。此外,若通过继承增添了一种新类型,如“三角形”,那么我们为“几何形状”新类型编写的代码会象在旧类型里一样良好地工作。所以说程序具备了“扩展能力”,具有“扩展性”。
void doStuff(Shape s) { s.erase(); // ... s.draw(); }
这个函数可与任何“几何形状”(Shape)通信,所以完全独立于它要描绘(draw)和删除(erase)的任何特定类型的对象。如果我们在其他一些程序里使用doStuff()函数:
Circle c = new Circle(); Triangle t = new Triangle(); Line l = new Line(); doStuff(c); doStuff(t); doStuff(l)
那么对doStuff()的调用会自动良好地工作,无论对象的具体类型是什么。
doStuff();
此时,一个Circle(圆)句柄传递给一个本来期待Shape(形状)句柄的函数。由于圆是一种几何形状,所以doStuff()能正确地进行处理
。也就是说,凡是doStuff()能发给一个Shape的消息,Circle也能收到。所以这样做事安全的,不会造成错误。
我们将这种把衍生类型当作它的基本类型处理的过程叫作“Upcasting”(上溯造型)。其中,“cast”(造型)是指根据一个现成的模型创建;而“Up”(向上)表明继承的方向是从“上面”来的——即基础类位于顶部,而衍生类在下方展开。所以,根据基础类进行造型就是一个从上面继承的过程,即“Upcasting”。
s.erase(); // ... s.draw();
注意它并未这样表达:“如果你是一个Circle,就这样做;如果你是一个Square,就那样做;等等”。若那样编写代码,就需检查一个Shape所有可能的类型,如圆、矩形等等。这显然是非常麻烦的,而且每次添加了一种新的Shape类型后,都要相应地进行修改。在这儿,我们只需说:“你是一种几何形状,我知道你能将自己删掉,即erase();请自己采取那个行动,并自己去控制所有的细节吧。”