在花费了很多的时间学习多态之后,我们有必要通过一个示例向读者展示多态技术在实践中不可或缺的重要价值。我们从大家常用的文字编辑软件说起。

版权声明

本文可以在互联网上自由转载,但必须:注明出处(作者:海洋饼干叔叔)并包含指向本页面的链接。

本文不可以以纸质出版为目的进行改编、摘抄。

要点🎯


  抽象类的最大用途在于为相关的扩展类定义一个统一的接口(interface)。


  想象一个Word类软件,WPS或者Open Office之类,用户会加入非常多的界面元素在文档中,包括但不限于:三角形、箭头、段落、圆形、矩形、艺术字、图片。在面向对象程序设计中,这些元素都会使用类来描述,并且,设计者一定会为这些界面元素提供一个统一的父类。作者把这个类结构简化成图18-6的模样,读者要明白真实的情况比这个要复杂得多,但基本结构大体如此。

shape

图18-6 Shape继承结构

  可以看到,所有的界面元素,三角形、圆形、… 、文本段落(paragraph),被描述成拥有一个共同的祖先类(Shape)。这个祖先类可以是抽象类,这意味着系统不允许你创建Shape类的对象。抽象类什么具体的工作也不做,只是描述了他的全部后代的模样:至少实现描绘自身的draw()以及获取元素在页面中的空间尺寸的getSize()这两个方法。这种描述是强制性的,它的后代必须实现这两个方法或函数。

  Triangle类用三个点坐标来描述自己的结构,除了实现必须的draw()和getSize()方法外,还实现了一个getArea()方法以计算自身所占面积。Circle类则用一个圆心坐标以及一个半径来描述自己,也实现了额外的getArea()函数。文本段落(Paragraph)类则用一个字符串sContent来存储其文本内容,还额外实现了setFont()函数来设置文本的字体和字号。

  下述C++代码给出了上述继承结构的极简版实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
//Project - Word
...
class Shape {
public:
//virtual Size getSize() = 0;
virtual void draw() = 0;
virtual ~Shape(){}
};

class Triangle:public Shape{
public:
//point0, point1, point2
//Size getSize(...)
//float getArea(...)
void draw(){ cout << "Triangle::draw()" << endl; }
};

class Circle:public Shape{
public:
//ptCenter, iRadius
//Size getSize(...)
//float getArea(...)
void draw() { cout << "Circle::draw()" << endl; }
};

class Paragraph:public Shape{
public:
string sContent;
//Size getSize(...)
//void setFont(...)
void draw(){ cout << "Paragraph::draw()" << endl; }
};

  让我们把关注点放在这三个类的draw()函数上。Office类软件会用文档来组织这些界面元素,在这里我们把文档想像成一个数组,在这个数组里包含了很多个三角形、矩形、段落、图片、艺术字、公式、图表对象,这些对象都是Shape类的子对象,都实现了draw()方法。

  当一个页面被显示出来时,软件会遍历这个数组,然后逐一调用数组内Shape子对象的draw()方法,以便把每个界面元素画出来,也就是每个对象自己画自己。因为三角形类了解三角形的数据表达形式,掌握描绘一个三角形的全部信息,由这个类的draw()方法来承担这个职责再合适不过了。圆形、段落这些类也是类似情况。我们设想一下,假设在页面上描绘三角形的任务不是由三角形类来完成,而是由外部代码来完成,那么外部代码就必须清楚并访问三角形对象内部的全部细节,如果这件事情真的发生的话,对于软件工程而言是灾难性的:外部代码知道太多关于三角形内部实现的细节!内部实现的细节变成了接口的一部分!三角形类接口不再简洁明了!以后你如果想修改三角形的内部数据结构,这几乎不可能,因为外部代码也要跟着改,涉及的外部程序和修改点可能太多。这种复杂的情况,我们称之为紧耦合(tight coupling),而程序的松散耦合(loose coupling),才是我们的目标。

  我们通过下述C++程序来模拟一个文档内的全部界面元素被逐一绘制的场景。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//Project - Word
...
void renderDocument(Shape* shapes[], int n){
for (auto i=0; i<n;i++)
shapes[i]->draw();
}

int main(){
Shape* shapes[] = {new Triangle(),new Triangle(),new Circle(),
new Circle(), new Paragraph()};

renderDocument(shapes,5);

for (auto x:shapes)
delete x;
return 0;
}

上述程序的执行结果为:

1
2
3
4
5
Triangle::draw()
Triangle::draw()
Circle::draw()
Circle::draw()
Paragraph::draw()

  在这段代码里,我们创建了两个三角形、两个圆形、一个段落,然后把它们放入一个数组中,这个数组就是一个文档的简化表达形式。renderDocument()函数遍历这个数组,逐一执行其元素的draw()方法。我们看到,renderDocument()函数并不清楚数组元素shapes[i]的具体类型,它只认为它是指向Shape对象的指针,而Shape对象实现了draw()方法,至于该对象到底是三角形、圆形或者别的什么界面元素,完全不关心。但是,我们发现,shapes[i]指向什么类型,就会执行对应类型的draw()函数,并打印出对应的文字。这就是多态,这些变量类型未知,但自动展现出与类型对应的恰当行为。

  本节展示的类结构为程序的扩展提供了极大的便利。如果Office软件试图建立一种全新的界面元素,比如某种直方图图表,软件设计者所要做的,就是继承Shape类并设计一个新的类,然后在新的类里实现全部虚函数。接下来,上述renderDocument()函数一个字符都不用修改,即可以拥抱新的界面元素的加入所带来的变化,以不变应万变!

  想象一下没有多态时的可怕场景,上述renderDocument()函数可能需要修改成下述伪代码所描述的模样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void renderDocument(Shape* shapes[], int n){
for (auto i=0; i<n;i++){
x = shapes[i];
if x 指向的是三角形
执行三角形的draw函数
else if x 指向的是圆形
执行圆形的draw函数
else if x 指向的是矩形
执行矩形的draw函数
...
else if x 指向的是艺术字
执行艺术字的draw函数
}
}

  就Word而言,其界面元素类型多达数百种甚至更多,第10行的省略号的背后,可能是上千行效率极其低下的难以维护的多分支代码。