UML面向对象设计基础

978-7-115-27590-5
作者: 【美】Meliir Page-Jones
译者: 包晓露赵晓玲叶天军唐亚东
编辑: 傅道坤龚昕岳

图书目录:

详情

本书是关于面向对象软件设计的基本概念、符号表示、术语、准则以及原理的书籍,介绍了面向对象的基本概念以及面向对象的发展史,统一建模语言,以及面向对象设计的原理。

图书摘要

版权信息

书名:UML面向对象设计基础

ISBN:978-7-115-27590-5

本书由人民邮电出版社发行数字版。版权所有,侵权必究。

您购买的人民邮电出版社电子书仅供您个人使用,未经授权,不得以任何方式复制和传播本书内容。

我们愿意相信读者具有这样的良知和觉悟,与我们共同保护知识产权。

如果购买者有侵权行为,我们可能对该用户实施包括但不限于关闭该帐号等维权措施,并可能追究法律责任。

• 著    [美] Meilir Page-Jones

  译    包晓露 赵晓玲 叶天军 唐亚东

  责任编辑 傅道坤

• 人民邮电出版社出版发行  北京市丰台区成寿寺路11号

  邮编 100164  电子邮件 315@ptpress.com.cn

  网址 http://www.ptpress.com.cn

• 读者服务热线:(010)81055410

  反盗版热线:(010)81055315


Authorized translation from the English language edition, entitled Fundamentals of Object-Oriented Design in UML, 1E, 9780201699463 by Meilir Page-Jones, published by Pearson Education, Inc, publishing as Addison-Wesley Professional, Copyright © 2000 by Addison-Wesley.

All rights reserved. No part of this book may be reproduced or transmitted in any form or by any means, electronic or mechanical, including photocopying, recording or by any information storage retrieval system, without permission from Pearson Education, Inc.

CHINESE SIMPLIFIED language edition published by PEARSON EDUCATION ASIA LTD., and POST & TELECOMMUNICATIONS PRESS Copyright © 2012.

本书中文简体字版由Addison-Wesley授权人民邮电出版社出版。未经出版者书面许可,对本书任何部分不得以任何方式或任何手段复制和传播。

版权所有,侵权必究。


本书是关于面向对象软件设计的基本概念、符号表示、术语、准则以及原理的书籍。

本书第一部分(第1章和第2章)介绍了面向对象的基本概念以及面向对象的发展史。第二部分(第3章至 第7章)较全面地介绍了统一建模语言(Unified Modeling Language,UML),UML已成为描述面向对象设计符号的事实上的标准。第三部分(第8章至第14章)较深入地介绍面向对象设计的原理。本书最后一章(第15章)分析软件构件的特点,包括优点和缺点。

本书针对采用面向对象技术的程序员、设计人员、系统工程师或技术经理,无论您是面向对象技术的初学者,还是具有一定面向对象技术经验的高手,都将从本书中受益匪浅。


如果专门致谢所有应该感谢的人,那么致谢词可能要延伸到94页。因此,我对多年来鼓励、帮助过我的众多同事一并感谢。但对于一些朋友,我要给予特别致谢:


对象已成为当今软件中普遍存在的构件,面向对象也成为软件工程实践中普遍使用的技术。种种有关面向对象的书早已面市,但当本书的第一版What Every Programmer Should Know About Object-Oriented Design(《面向对象设计程序员必读》)出版时,该书立即被公认为出自一位当今软件开发中最明智的思想家和通俗作家之手的、新颖而有独创意义的著作。

在全新的重新修订并重新命名的第二版中,扩充了基础内容,增加了素材,修改了符号表示以符合最新的潮流。本书融入了面向对象开发的新思想,从对于继承使用的剖析,到如何针对对象类之间疑难数据关系建模。Page-Jones不愧为最新智慧的代表。

作为咨询专家和设计者,作者几十年来一直工作在第一线,本书的每一页倾注了作者的经验和教训。我曾与他在同一“战线”中工作,特别是最近在一个大型项目中一起合作,初始的用例(use-case)模型中的用例就超过340个。随着读者对本书的学习,您将看到作者还是一个实用主义者,对基础和细节的注重反映在他的分析和设计研究及其作品中。

事实上Meilir 还是一位有天赋的教师,他擅长澄清复杂、难以理解的概念,形成一个概念上的模式使其变得清晰。他可以列举一大堆问题并将其包揽在一个原形例子中,使得一切都变得十分明朗,让其他人无不为之感叹:为什么我们没想到。以面向对象的方法考虑农场牛奶问题,挤奶的时间到了你会做什么呢?给Cow对象发送一个消息让其挤奶还是给Milk发送一个消息让它自己脱离Cow?瞬间的反映及事件管理的需求使协调挤奶的过程变得十分清晰。他的许多阐述例证已成为面向对象中广为流传的一部分内容,例如来自会议讨论组记录的例子,以及“Person Owns Dog”的难题。

本书恰当地说明了如何从长远利益出发建立好的设计原则,这些原则已在实际中广泛使用,并在最先进的语言和挑战环境中开发面向对象系统被采纳。建立在这种基础上,本书基于实际经验解决复杂的实际问题,通过浓缩的例子所介绍的经验,为新老开发者设计更好的面向对象解决方案提供了指导。

Meilir作为一位咨询专家、一位教师或一位方法学家,在面向对象开发方面积累了丰富经验。他是Synthesis方法的创建者之一,这种方法是早期的面向对象分析和设计的语义方法之一,并且,在建立流行的Uniform Object Notation(统一对象符号表示法)中我们是合作者,这种方法可以在当今众多的面向对象方法和表示法中找到。我们的研究成果甚至得到了Unified Modeling Language (统一建模语言,UML)的认可,UML已经成为事实上的工业标准,本书采用该标准对样例进行说明。

在这里可以找到掌握面向对象设计基础需要的所有内容。不仅包括清晰易懂的设计和建立对象的基本技术,而且包括丰富的范例,详细地讨论一个好的面向对象系统应该怎样而不应该怎样。

Larry Constantine

1999年9月

Massachusetts,Rowley


人们评论本书时向我提出了几个问题,你可能也关心这些问题。下面我回答其中的一些问题。

我是一名程序员。为什么要关心设计?

每个人在编写代码的同时,也在设计(design)代码,不管是好是坏,还是有意或无意。我写本书的目标是鼓励OO专业人士(这类人士越来越多)有意识地在编程之前建立良好的面向对象设计。为此,我介绍符号表示法、面向对象的原则以及术语,使你及你的同事可以对设计的系统进行评估和讨论。

本书将教会我们OO的编程语言吗?

回答是否定的。尽管我偶尔会提到程序,但本书不是关于面向对象编程的。

我正在学习面向对象语言,本书会有帮助吗?

回答是肯定的。如果你目前还不了解一种面向对象语言,可以从第1章中获得面向对象的知识。了解面向对象的主要概念可以加快你对一种面向对象语言的学习,并且能增强你跨入陌生领域的信心。本书后面有关如何设计的章节也将有助于使你以前的程序运行得更好。

另一方面,如果你是一位有经验的面向对象程序员,可以通过本书的第二和第三部分提高设计水平,这点对于成为全面的、专业的软件设计者或编程者是至关重要的。

为什么不用C++编写本书的程序例子?

本书中的程序采用我自己发明的语言编写,该语言融合了4种流行的语言:C++,Eiffel,Java及Smalltalk。我这样做是因为有两类程序员:一类是熟悉C++的,一类是不熟悉C++的。如果你是C++ 的爱好者,会发现本书的程序可以很轻松地转成C++程序。如果你不熟悉C++,可能会厌烦其神秘的语法。有些例子是用Java写的,这是因为非Java程序员接受Java比非C++程序员接受C++要容易些。我希望无论你的编程语言是什么,本书都能适合你。

为什么本书不是针对窗口、图标及菜单设计的?

原因有两个:其一,我不认为面向对象只适用于图形用户界面的设计;其二,在市场上有许多专门针对面向对象窗口设计的书。我希望本书能包括一些其他面向对象的图书没有涉及到的内容。在第7章提供了窗口导航设计的内容。

本书是有关方法学的吗?

回答是否定的。我们知道,开发方法学的内容比设计要多得多。例如,方法学还包括需求分析、库管理等。而且,真正的方法学应该解释各种开发活动是如何有机地结合在一起。内容很多!

因此,我不打算将本书写成和其他许多有关面向对象的书一样内容松散,而集中于一个主题:面向对象设计。

你说了本书许多不涉及的内容,但本书涉及哪些内容呢?

本书主要涉及面向对象软件设计的基本概念、符号表示、术语、准则以及原理。面向对象软件是由对象以及其所属的类构成的软件。一个对象是一个软件构件,其操作(类似函数或过程)与一系列变量(类似数据)有关。类实现了一种类型,它定义属于该类的一组对象。

上述朴素的语句蕴藏着对于软件设计者和编程者非常有用的内涵,由此引出了继承、多态性及二次设计等的设计概念。但由于你问了一个特定的问题,所以我给你一个专门的回答。

本书第一部分(第1和第2章)介绍面向对象。第1章综述了主要概念,并揭开了“多态性”、“一般性”及其他所有OO行话的神秘面纱。第2章将面向对象置于以前的软件开发框架中。如果你已经熟悉面向对象(也许用面向对象语言编过程序),那么可以跳过第一部分。

第二部分(第3至第7章)介绍有关Unified Modeling Language(统一建模语言,UML)的内容,UML已成为描述面向对象设计符号的事实上的标准。此外,第二部分还说明了许多可以在面向对象系统中找到的结构。第3章介绍用UML描述类及其属性和操作。第4章介绍用UML表示关联、聚合、组合对象,以及子类和超类的层次。第5章说明如何用UML表示消息(顺序的和异步的),第6章介绍用UML描绘状态图。第7章总结UML用于系统结构和人机交互界面的窗口的内容。

第三部分(第8至第14章)较深入地介绍面向对象设计的原理。第8章讲述共生性的关键概念及2级封装。第9章探讨各种类所属的域并描述不同的类的内聚程度。第10、11章是第三部分的主要内容,将状态空间和行为的概念应用到既合理又可扩展的类层次结构。

第12章提供了一些轻松的调节剂,从现实世界中检验设计,既有精巧的设计也有可笑的设计(第12章确实列举了设计者有关继承和多态性的可笑设计)。第13章给出一些如何组织给定类操作的方法,并说明提高可重用性和可维护性设计的技术,如混合类和操作环。

第14章触及一个令人头痛的问题:“如何设计一个好的类?”为回答这个问题,第14章描述了从可怕的到美妙的各种类接口。具有典型接口的类易于进行抽象数据类型的实现。如果类的设计遵守前面几章介绍的基本原理,这样的类更具有健壮性、可靠性、可扩展性、可重用性和可维护性。

作为本书结束的第15章分析了软件构件的特点,包括优点和缺点。在商用应用软件的面向对象开发过程中,回顾了在前面几章中介绍的一些面向对象原理。

尽管我在主要的论述中增加了丰富的例子和习题,但必须承认第三部分中有些内容比较难。但不管怎样我不想弱化重要问题。面向对象设计的某些方面内容是比较难的,值得一提以免引起误解。

本书包括了面向对象设计的所有内容吗?

我对此深表怀疑。每天,我都能学到许多有关面向对象的内容,相信你也一样。实际上,一本书可以告诉我们面向对象的一切而不需要我们再学习,是十分荒谬的。本书中的内容一定有不当之处!在我写完几本书之后,随着年龄的增长和阅历的增加会改变我的一些观点。

因此,尽管我在本书中包括了许多重要的设计原理,如果你对面向对象十分重视,应尽可量广泛阅读并挑战你所学的知识。

你提供面向对象设计的课程吗?

是的。我所在的公司Wayland System提供一些面向对象专题的课程。我们的课程经常有变动,有关最新信息请查阅www.waysys.com。

这本书适合我吗?

这是什么问题?你希望我说“不?”如果你已经是或是将要成为采用面向对象技术项目的程序员、设计者、系统工程师或技术经理,那么本书适合你。如果你是面向对象的初学者,将从本书的第一部分学到许多知识,然后做些面向对象编程的练习,再回到本书的第二和第三部分。

如果你是已经掌握了标准的结构化编程的大学生或职业程序员,希望进一步扩大知识范围,本书也适合你。

无论你属于何种角色,希望你能喜欢本书并从中受益。

祝好运!

Meilir Page-Jones

1999年9月

华盛顿Bellevue

meilir@waysys.com


“面向对象”术语本身是无意义的。“对象”大概是英语中最普通的词了。在字典中查找它的定义如下:

对象:被呈现的或可被感官识别的物体。

换句话说,对象可以是任何事物!

“面向”一词也不能说明什么含义,定义为“直接针对”。因此有下面的定义:

面向对象:直接针对你能想到的任何事物。

软件界历史上在讨论“面向对象”的定义上很难达成一致的意见并不奇怪。由于缺乏清楚的定义使得包装成“面向对象”的软件四处传播不奇怪,如此多的有关“面向对象揭密”的培训课程要么老生常谈要么平平淡淡也不奇怪。

我开始步入O.O.领域时,决定一次澄清“面向对象”的定义。我把数十位面向对象的老前辈关在一个没有食物和水的房间里。我告诉他们只有当他们的定义达成一致的意见,并且可以在软件世界发表时才允许他们出去。

在一小时的喧哗过后,房内一片安静。更糟糕的是当我小心翼翼地打开房们时,被眼前的情景惊呆了,老前辈们背靠背谁也不理谁。

显然,每个老前辈都试图通过由来已久反复声明的科学实践来建立面向对象的某种定义。当发现这种方法毫无结果时,每个人都同意列出一个他们认为在面向对象环境中不可缺少的特性。每个人都列出了6至10个重要特性。

此时,大致有两种做法:一种是建立一个长列表,该列表是每个人列表的并集;另一种是建立一个短列表,该列表是每个人列表的交集。他们选择了后者,产生了一个短列表,该列表中的特性在每个人列表中都有。

这个列表确实很短,短到只有“封装”这个词。

因此这群老前辈在激烈的争吵中定义面向对象并没有获得良好的效果。问题在于“面向对象”术语缺乏固有的含义,因此其定义总带有随意性。不过,在第1章我给出了构成面向对象的软件特性列表。你可以认为我的列表是“正确”的,也可以认为该列表碰巧包含了9个由12位名人选择的最流行的特性。

第2章,我列举了一些面向对象的创始人。随后,分析了社会上或文化上对面向对象的态度,并从工程的角度对面向对象展开了讨论。在第2章还简述了面向对象对于软件开发组在软件开发各阶段的好处。


正如前面提到过的,我认为面向对象中有九个非常重要的软件概念。这九个概念是:

用一段面向对象程序来解释这些术语背后的含义是最好的办法。通过本章对这段程序的讨论,你会发现面向对象中的一些术语,其含义并不像看起来那么可怕。实际上,从以前的软件经历中你可能已经熟悉了许多面向对象概念,尽管在叫法上可能有差异,如果你关心面向对象术语在编程语言之间的差异,请参阅本书后面的Blitz Guide to Object-Oriented Terminology(面向对象术语快速指南)。在开始讨论之前,先说明三个注意事项。

第一,我列举的程序是一个非常简单的面向对象应用程序中的一部分。这个应用程序以缩图的方式在屏幕上显示机器人(hominoid)在方格图案中移动(你可能在视频游戏中见到过这种形式)。虽然面向对象并不仅仅局限于屏幕应用,但这种应用确实提供了一个出色的入门样例。

第二,由于我没有仔细推敲这个程序的语法和语义,因此如果一开始对程序的含义不十分清楚,请不要担心。与解释面向对象术语一样,我也会解释程序本身的细节。我采用面向对象伪代码编写该算法,综合了几种主流面向对象语言如C++、Java、Eiffel及Smalltalk的语法(顺便提一下,repeat …until…endrepeat结构与面向对象无关,它是纯结构化程序设计中的中间带有测试的循环)。

第三,尽管这两个类设计并不十分完美,但足够达到本章的目的。如果你对此有任何不满意(如对类Hominoid)请不必着急,在第9章将分析其设计上的缺陷(这种缺陷称为“混合域内聚”)。

现在,我们从软件项目经理写给项目组成员的备忘录来开始这个应用程序。

备忘录

来自:Alinah Koad,软件开发经理

到:Hominoid 软件组

主题:Hominoid-Controlling Software (V 1.0)

我刚从橡木办公室的大屏幕上获悉,我们签定了控制机器人硬件的合同。朋友们,这次我们一定要成功,以弥补导盲机器人走到压路机下面的惨败情形。实际上客户希望在将软件放到硬件上之前,在显示屏上演示一下我们的软件。他们已经安排在下个星期一演示我们的软件。

在该软件的第1版中,机器人(hominoid)只能通过如下图所示的线性路径导航。你可以将路径看作是由许多的方块组成的,一个方块产生一段路径,机器人从START(起始)方块(S)到FINISH(终止)方块(F)。沿着路径每次转弯时旋转一个合适的角度,如图1.1 所示。

图1.1 机器人通过方格的路径

机器人前进一步严格地向前行进一个方块。机器人从START到FINISH要经过路径的每一个方块,更重要的是机器人在行进中不能碰到墙壁。否则,我们看起来会很愚蠢,客户也不会允许我们将该软件安装到真正的机器人硬件上。

幸运的是,我们已经在面向对象库中编写并保存了两个类。这两个类是Grid和Hominoid。因此,到星期一为止你们所要做的就是编写应用这些类操作的面向对象程序。如果你们有任何问题,可以与在Julius Marx Country Club小屋中的我进行联系。周末愉快!

另附上库中的这两个类(Hominoid和Grid)的简短说明。

Class External-Interface Specifications

(of the classes in the library)

Hominoid

  New:Hominoid

  //创建并返回一个新的Hominoid实例

turnLeft

  //机器人向逆时针方向转90°

turnRight 

  //机器人向顺时针方向转90°

advance (noOfSquares:Integer,out advanceOK:Boolean)

  //机器人沿着面对的方向移动一定数量的方格并返回是否成功的值

location:Square

  //返回机器人当前所在的方格

facingWall:Boolean

  //返回机器人是否正对着有墙的方格

display

  //在屏幕上显示机器人图标

Grid

  New:Grid

    //创建并返回一个新的随机Grid实例

  start:Square

    //返回标记为路径起点的方块

  finish:Square

    //返回标记为路径终点的方块

  insertHominoid (hom:Hominoid,location:Square,out insertOK:Boolean)

    //将机器人放到指定位置的方格并返回是否成功的值

  display

    //显示作为屏幕中的图案的方格

关键

举例               含义

advance            以小写字母开头的词表示对象、实例操作及实例属性

Hominoid            以大写字母开头的词表示类、类操作及类属性

insertHominoid(hom:Hominoid,

startSquare:Square,

out insertOK:Boolean)    表示类Hominoid和类Square对象的操作,并返回类

                 Boolean的一个对象(其中out用于分隔输入和输出参数)

:=              赋值操作符

var insertOK          表示程序变量insertOK

项目组牺牲了周末时间,编写了下面的面向对象程序。在本章其余部分,经常用到这段程序作为描述面向对象抽象概念的例子。

var grid:Grid:= Grid.New;         //创建新的Grid实例
var hom1:Hominoid:=Hominoid.New;    //创建新的Hominoid实例
                        //(用hom1表示Hominoid的新实例)
var insertOK:Boolean;
var advanceOK:Boolean;
var startSquare:Square;
const oneSquare = 1;

startSquare:= grid.start;
grid.insertHominoid (hom1,startSquare,out insertOK);

if not insertOK
then abort everything!;
endif;
   //将机器人设置为正确的方向
repeat 4 times max or until not hom1.facingWall 
   hom1.turnLeft;
endrepeat;

grid.display;
hom1.display;

repeat until hom1.location = grid.finish

   if hom1.facingWall
   then hom1.turnLeft;
     if hom1.facingWall
     then hom1.turnRight;hom1.turnRight;
     endif;
   endif;

hom1.advance (oneSquare,out advanceOK);
hom1.display;

endrepeat
   //机器人走到终点—成功!

用这段程序作为例子,让我们再回到前面提到的面向对象的九个特性。在这些特性中首当其冲的概念就是封装。

封装(encapsulation)是将相关的概念组成一个单元,其后可以通过一个名称来引用它。

软件封装的概念几乎与软件本身一样历史悠久。早在1940年,程序员注意到一些相同的指令集在同一个程序中出现多次。人们(如Cambridge University的Maurice Wilkes及其同事)不久意识到这种重复的代码可以放到程序的某个地方,并可以从主程序的不同地方通过一个名字来激活。

由此产生了子程序的概念,此时指令的封装被术语化了。子程序显然是节省计算机内存(当时是非常珍贵的资源)的一种好方式。后来人们又意识到子程序还可以节省人类的记忆:它表示一个概念化物体,使人们可以(至少可在某种程度上)将其作为一个概念来考虑和操纵。图1.2说明了一个贷款应用程序的子程序。

图1.2 子程序

面向对象的封装在某种意义上与子程序的封装有些类似,但封装的结构更为复杂。

面向对象封装(object-oriented encapsulation)是将表示状态的操作和属性包装成一个对象类型,使得对状态的访问或修改只能通过封装提供的接口进行。

一个对象是由一系列操作和一系列属性组成,如图1.3所示。例如,hom1表示的对象操作有:turnLeft,将机器人对象向左转90°,而advance,将机器人对象向前移动。每个操作是一个过程或函数,对其他对象是可见的,即可被其他对象调用。

图1.3 hominoid对象的操作和属性

属性表示对象记忆的信息,它只能通过对象的操作来访问和修改。换言之,其他对象不能通过变量直接访问属性。其他对象需要访问属性信息只能通过对象的操作来访问。

因为只有对象的操作可以读取和修改对象的属性,这些操作以对象的变量为中心形成一个保护环。例如,操作location(可能作为一个函数实现)向hominoid以外的对象提供hominoid的位置(大概是一对x,y坐标形式),我们不能直接访问对象的任何变量(如xLoc和yLoc)直接获取信息。

因此一个对象的结构类似于欧洲中世纪建筑,典型的特点是用保护墙围起来。通过围墙的保护门规定了城市的入口和出口。图1.4所示为以hominoid对象操作命名门的围墙城堡。

在传统的中世纪年代,忠厚老实的农民通过这些门进入城堡。他们在市场上买猪,然后从一个大门出去。只有那些缺德的农民和堕落的无赖胆敢翻墙而入,猛击一头猪,偷窃后从围墙落荒而逃。

为准确起见,应该指出许多面向对象语言允许程序员将每个属性和操作指定为Public(其他对象可以访问) 或Private(只能本对象访问)。除非特殊声明,本书中用到的操作表示典型、公共、可见的操作,属性表示典型、公共、可见的属性。

图1.4 以对象操作命名门的围墙城堡

你可以从对象的外部(“公共角度”)或对象的内部(“私有角度”)两种不同的角度来看待封装单元。好的封装结果是对公共视角封闭大量的细节。这种封闭有两种形式即信息隐藏和实现隐藏。

术语“信息隐藏”指不能被外界察觉的单元内的信息。术语“实现隐藏”指不能被外界察觉的单元内的实现细节。

信息/实现隐藏(information/implementation hiding)是使用封装将某些信息或实现方法限制在封装结构内部,限制外部的可见性。

机器人对象包含一些外界无法访问的私有信息,它说明信息隐藏特性。例如,机器人所代表的方向,从该对象外部可以改变这一信息(也许通过turnLeft),但不能得到它的值,除非假设显示该机器人本身并说明机器人鼻子的朝向。

然而,术语“信息隐藏”只说明好的封装可以隐藏的部分。封装通常提供信息而隐藏实现方法。这点对面向对象是十分重要的:对象内部变量存储的属性信息无需用与属性本身一样的方法实现,其他对象也可利用。

例如,尽管机器人对象(通过location操作)告诉我们它所处的位置,但我们却不知道对象内部如何存储它的位置。可能为(xCoord,yCoord)或(yCood,xCoord)或经纬度坐标或是设计者在半夜1点钟想出的某个重要方法。只要对象能以我们所能接受的方式输出其位置,我们就不会关心它是如何存储位置的。

因此,Hominoid的方向既是信息隐藏又是实现隐藏的例子。我们不知道对象中存储的方向信息是以数字角的方式(取值范围从0°到359°),还是以单个字符(N、E、S及W)或是precentDirection ,表示机器人的朝向占整个圆的百分比(从0到99.999)。

在以后的再设计中,我们也许决定显示方向信息并提供将direction属性输出给其他对象的操作。即便如此,我们仍保留实现隐藏,因为仍不需知道对象内的实现是否与公共信息的实现一样。

例如,我们可能决定对象内部以字符形式保存direction,然后经转换以角的公共形式输出。换言之,提供该属性值的操作可以将特殊的内部表示转换为大多数人希望看到的数字角的形式,与direction属性一样。

信息/实现隐藏是降低软件复杂性的有效技术。对外部观察者而言,可以将对象看作一个黑箱子。换言之,即外部观察者知道对象可以做什么,而不知道对象如何做或对象内部是如何构造的。如图1.5所示。

图1.5 被看做“黑箱子”的Hominoid对象

信息/实现隐藏有两个主要优点:

① 设计决策局部化。私有设计决策(在对象内)对系统的其余部分影响很小或没有影响。因此,这种局部决策的修改对整个系统影响最小。这样限制了“修改波及”的影响。

② 其表示形式减弱了信息的内容。因此,对象外部的信息用户不会受到任何特殊的内部信息格式的困扰。这样,对象的外部用户(如其他程序员)就不用干涉对象内部的事情,也防止无聊的程序员对对象引入不稳定的连接(我知道你不会干这种事情,但你可能交叉运行我曾提到过的软件库)。

面向对象的第三个抽象概念是对象具有保持状态的能力。当传统的过程模块(函数、子程序、过程等)返回到调用者时,不会带来任何负作用,模块运行结束,只是将其结果返回。当同一模块再次被调用时就象是第一次诞生一样。模块对以前的存在没有任何记忆,就像人类一样对以前的存在一无所知。

但对于对象而言,如机器人对象就知道它的过去。对象在其自身内部将信息保留一段时间。例如,一个对象的“调用者”可能给该对象一个信息,后来该调用者或其他调用者又要求该对象再次提供这一信息。也就是说对象执行结束后并没有死:忠于职守,准备再次运行。

从用技术上来讲就是对象保持其状态(状态即对象拥有值的集合,第10章进一步讨论)。例如机器人保持它所在的方块和面向的信息。从1.1节和1.2节我们知道对象如何保持这些信息就是对象自己内部的事了。

面向对象封装、信息/实现隐藏及状态保持是面向对象的核心。但这些都不是新概念。世界各地的勤劳的计算机科学教授们已历经数年,研究过抽象数据类型(abstract data-type ,ADT)中的这些概念。然而,随着面向对象后六个特性(1.4节到1.9节)的出现,面向对象就超越了ADT。

面向对象胜过ADT的第一个十分重要的概念就是对象标识特性:每个对象具有自己的标识。

对象标识(object identity)是指每个对象(不考虑其所属类或当前状态)可以作为不同的软件实体被标识、处理的特性。

对于给定对象可以用唯一的信息将其与其他对象伙伴区分开来。这个“唯一的信息”可以通过对象句柄机制提供,下面通过分析一行hominoid代码来解释这个问题:

var hom1:Hominoid :=Hominoid.New

这行代码的右边创建一个新的Hominoid类的对象,如图1.6所示。注意图中所示的这个对象的句柄为数字602237。句柄就是对象被创建时赋给它的标识符。

图1.6 带有句柄的一个对象

句柄遵守两个规则:

① 在任何情况下,对象在整个生命周期都保持同一个句柄。

② 两个对象不可能具有相同的句柄。系统在运行时无论何时创建一个新对象,都给这个对象赋予一个与其他所有句柄(包括过去、现在和未来)不同的句柄(注:句柄正规地被称为对象标识符(object identifier,OID)。大多数面向对象环境自动创建这个唯一的OID)。因此即使对象具有相同的结构或保存相同的信息也可以将对象区分开来,因为对象具有不同的句柄。

这行代码的左边是var hom1:Hominoid的声明。与通常的程序声明一样,起一个程序员容易记忆的可以保存值的名字(这里为hom1),Hominoid为hom1的类名,在1.6节专门讨论。

你可能已经想到,赋值符号(:= 读做“现在指向”或“现在引用”)使变量hom1保存了右边赋值语句所创建的对象的句柄,“指向”表示一般的含义。术语“指针”含义包括了C++指针,C++引用,Eiffel实体,Smalltalk和Java变量等。

没有人(包括程序员、用户或任何人)能真正看到新对象的句柄(602237),除非用调试程序调试内存。程序员通过其命名的变量hom1访问对象。换言之,hom1就表示句柄为602237的对象。如图1.7所示。

一些面向对象环境使用对象的物理内存地址作为句柄。这样做比较简单,但当对象在内存中移动或交换到硬盘时,这样做就比较可怕了。句柄最好是无意义的、随机的并且是唯一的数字(尽管我们不是编译程序的设计者,无法知道计算机是如何产生句柄值)。

图1.7 hom1表示句柄为602237的对象

比如我们执行另一行类似的代码:

var hom2:Hominoid :=Hominoid.New

该行代码创建类Hominoid的另一个对象,其句柄假设为142857,然后将句柄保存在变量hom2中(参见图1.8)。

图1.8 hom2表示句柄为142857的对象

为说明问题,写出下面的赋值语句:

hom2:= hom1

现在变量hom1和hom2都指向相同的对象(即创建的第一个对象,句柄为602237的Hominoid实例)。参见图1.9。

两个变量指向同一个对象通常没有什么意义。而且更糟糕的是,现在已经没有办法访问第二个对象(句柄为142857)。因此这个对象就消失了,仿佛掉进了一个黑洞!大多数面向对象环境此时起用一个垃圾回收程序将该对象从内存删除。垃圾回收程序是操作环境的一种服务,不是一辆发出气味的大卡车每周五早晨隆隆驶进死胡同,在JAVA和Eiffel中将实现自己垃圾回收程序,但不是在C++环境中。

图1.9 hom1和hom2都指向相同的对象且其他对象不可再访问

通过句柄使每个对象具有自己的标识似乎非常平常。但这种简单的思想却使设计和构造面向对象软件发生了深刻变化。在下一节中将可以看到这种变化。

对象通过消息请求另一个对象执行活动。许多消息还具有将信息从一个对象传送给另一个对象的作用。大多数老前辈都将消息列为重要的面向对象特性。

消息(message)是发送对象obj1向目标对象obj2发送请求的载体,申请对象obj2的一个方法。

本节对消息特性进行了剖析,描述了消息参数的特点,发送消息对象的角色,接收消息对象的角色及消息的三种类型。

消息由几个含义组成,每个含义在面向对象设计中都十分重要。实际上,本书从头到尾会多次用到消息的特性。

对象obj1为给对象obj2发送一个显式消息,对象obj1必须知道三件事情:

① obj2的句柄。显然,你发送一个消息时,应该知道给谁发送。obj1通常将obj2的句柄保存在它的一个变量中。

② obj1希望执行的obj2操作名称。

③ obj2执行其操作时所要求的所有附加信息(参数)。

发送消息的对象(obj1)称为发送者,接收消息的对象(obj2)称为目标对象,也可以分别称为客户机(Client)和服务器(Server)。

机器人软件提供了几个消息的例子,其中一个为:

hom1.turnRight;

其中,hom1表示该消息(含有句柄)的目标对象。回想一下,赋值语句。

var hom1 :=Hominoid.New给hom1赋的句柄)。turnRight是目标对象执行操作的名称。这个消息不需要任何参数:turnRight总是旋转90°。

发送消息和调用传统的函数或过程有些类似。如用“准O.O.”语言,可以写出下面的语句:

    call turnRight(hom1);

请注意这种倒置:使用传统的软件技术,我们先申请一个过程单元,然后向其提供操作的对象;在面向对象中,我们先申请一个对象,然后执行其中一个过程单元。

到此为止,这种区别似乎只停留在语法上,至多是观念上的。然而,当我在1.8节讨论多态性、重载及动态关联时,将体会到“先对象后过程”所引起的面向对象结构和传统结构之间的重要差别。因为不同的对象类可以使用相同的操作名称执行不同特定类的行为,或执行含义不同的类似行为。

与传统的子程序一样,大多数消息都可以传入或传出参数。例如,如果名为 advance的操作返回一个标志,保存行进的输出,于是有下面的形式:

因此,给予目标对象的消息结构由激活目标操作的原形(signature)所定义。这个原形包括三个部分:操作名称,输入参数列表(前缀为 in),输出参数列表,也称返回参数(前缀为out)。每个参数列表都可以为空,在两个列表中可能出现相同的参数或只在前缀为inout的列表中出现一次,但在纯面向对象中很少出现这种情形。为简短起见,通常省略关键字in,将其作为缺省前缀。

消息的参数反映了面向对象软件和传统软件之间的另一个基本区别。在纯面向对象环境中(如Smalltalk),消息参数不是数据,而是对象句柄,因此,消息参数也当作对象。

例如,图1.10a以非正式的图形表示法说明了机器人程序中的一个消息:

hom1.advance(noOfSquares,out advanceOK)。

图1.10a 以非正式的图形表示消息hom1.advance(noOfSquares,out advanceOK)

如果在执行消息时暂停hominoid程序的执行,取出消息参数的值,我们将发现意想不到的结果。如:

 noOfSquare被置为123432
 advanceOK 被置为664730

为什么会得到这些奇怪的数字?因为123432可能为对象(Integer类)的句柄,通常为整数2,664730可能为对象(Boolean类)的句柄,通常为逻辑值true,在面向对象环境中最好不用这些精确的数字,我使用这些数字只是用于说明。

如果你更喜欢图1.10b用UML表示方法来表示这个消息,第3章至第7章将会更深入地讨论这方面内容。

图1.10b 用UML表示消息hom1.advance(noOfSquares,out advanceOK)

另外,再举一个例子,如果我们正执行一个面向对象的个人系统并做相同的事情,可能发现参数empOfMonth被置为441523。441523可能是对象(Employee类)的句柄表示Jim Spriggs先生。

本节讨论面向对象系统中对象的四个角色。一个对象可以是:

一个给定的对象在生存期可以扮演一个或多个角色。在图1.11中,我们可以看到所有这些角色。

图1.11 一个对象操作发送消息给由变量指向的三个对象

从这个图1.11中,我们来看一下对象obj的一个操作op。这个操作发送消息给obj的三个变量指向的每一个对象。第一个消息只有一个输入参数;第二个消息只有一个输出参数;第三个消息既有输入参数也有输出参数。每个参数本身指向一个对象。这个结构非常典型地说明了对象操作如何与对象变量进行交互。

一些作者建议每个对象要么是“发送者”要么是“目标”。但实际不然,如图1.12所示。

图1.12 在两对对象之间的两个消息

对于message1,obj1是发送者,obj2是目标。对于message2,obj2是发送者,obj3是目标。因此我们看到,不同的时刻,同一个对象既可以是发送者又可以是目标。术语“发送者”和“目标”是相对给定消息而言的。它们不是对象本身固有的特性。

在纯面向对象环境中只有对象,每个对象扮演一个或前面提到的四个角色中的几个角色。在纯面向对象中不需要数据,因为对象可以完成数据完成的所有软件功能。在Smalltalk(非常纯的面向对象语言)中,确实没有任何数据!运行时只有指向其他对象的对象(通过变量),通过传递对象的句柄进行对象之间的通信。

但在C++(一种既面向数据或函数又面向对象的混合语言)中,参数可以表示任何信息。如果你的C++程序与Smalltalk一样纯,那么所有的参数都表示对象。但如果你将数据和对象混合在C++程序中,那么有一些参数就是简单的数据(或数据的指针)。对于Java语言也是如此,尽管Java语言远远不如C++那样随意。

对象可以接收三种类型的消息:报告消息,询问消息及祈使消息。本节对每一种消息进行举例说明。再一次借用机器人程序。在本书的最后第12章,讨论通信对象不同的设计方法时,再回到这些消息类型。

报告消息(informative message)是指向对象提供自我更新信息的消息(也称更新、向前或推出消息)。这是一种“面向过去”的消息,通常通知对象已经发生的事情。

报告消息的一个例子是employee.got(MarriageDate:Date)。这个消息告诉一个雇员对象某个雇员已经在某个日期结婚。通常,报告消息告诉一个对象由该对象表示的在现实世界中已经发生的事情。

询问消息(interrogative message)是请求一个对象显示自身一些信息的消息(也称为读、向后或回拉消息)。这是一种“面向现在”的消息,向对象询问当前信息。

询问消息的一个例子是hom1.location,向机器人询问当前所在的方格位置。这类消息实际上不改变任何事情,通常是向目标对象询问其表示的信息。

祈使消息(imperative message)请求对象对本身、另一个对象或系统环境执行某些操作(也称强制或动作消息)。这是一种“面向未来”的消息,请求对象执行将来的某些操作。

祈使消息的一个例子是hom1.advance,使机器人向前移动。这种消息通常使目标对象执行一些重要的算法。

类似地,假设向机器人发送下面的祈使消息:

hom1.goToLocation(square:Square,out feasible:Boolean)

该消息请求机器人只要可行,就走到特定的方格(机器人执行的计算量非常大)。

在实时的面向对象系统中,对象需要控制一些硬件,通常包含许多祈使消息。这些系统清楚地说明将要执行的祈使消息。下面是取自机器人世界的一个例子:

robotLeftHand.goToLocation(x,y,z:Length,theta1,theta2,theta3:Angle)

这个消息将使机器人的左手抬到某个位置并在空间定位。该算法可能要求机器人的手、机器人的胳臂或机器人本身移动。六个参数表示了手的六个自由度,以三维空间表示。

下面让我们从消息转到面向对象公认的基本属性即对象类。

回想一下在机器人软件中,通过执行Hominoid.New创建了一个对象(表示一个机器人)。Hominoid(类的一个例子)作为创建机器人对象(如句柄为602237 的对象)的模型。每当执行语句Hominoid.New时,示例一个与用Hominoid.New创建的每一个对象结构相同的对象。“结构相同”,指每个机器人对象与其他对象有相同的操作和变量,特别是当程序员编码写Hominoid类时的操作和变量。参见图1.13

图1.13 来自相同类的三个对象例示

类(class)是创建(示例)对象的模板。从类示例出的每个对象具有相同结构和行为。

如果对象obj属于类C,则称“obj为C的一个实例”。

同一个类的对象之间有两点不同:一是每个对象具有不同的句柄;二是在任何特定时刻,每个对象可能有不同的状态(指存储在变量中的不同“值”)。

开始你可能对一个类和一个对象之间的区别比较模糊。下面提供了区别两者的简单方法:

流行软件包对于类和对象提供了非常好的类比。比如你从Wallisoft Corp购买了电子表格软件包Visigoth 5.0。软件包本身好比类。由此创建的电子表格好比对象。每个电子表格具有作为类Visigoth 的一个实例应有的“电子表格结构”。

在运行时,一个类如Hominoid可以产生3个、300个或3000个对象(即Hominoid的实例)。因此一个类类似于一个模板:一旦模板的形状被剪裁,就可以仿制上千次。所有的仿制品都是相同的,当然与原来的模板形状也是相同的。

为清楚地说明这个问题,让我们更进一步地看一下从一个类产生的对象。正如我们所知,一个类的所有对象都具有相同的结构:相同的一组操作和属性(注:方法是操作的实现。用编程术语讲,可以将方法看作是过程或函数体的程序。类似地,变量是属性的实现,句柄是对象标识符的实现)。因此类的每个对象(事例)都具有自身实现操作的一组方法和实现属性的一组变量的拷贝。在给定的时间内,原则上讲有多少对象被示例,就有多少方法和变量被拷贝。如图1.14所示。

图1.14 相同类的三个对象的方法、变量和句柄,以及对象的内存需求

如果读者不介意,我将深入到计算机内部实现,进一步解释相同类(如类C)的一组对象的真实结构。假设实现图1.14中的操作的每个方法占100个字节,每个变量占2个字节,每个句柄占6个字节。因此,object1将占416个字节的内存(4*100+5*2+6)。因此三个对象一共占用1248个字节(3*416)。十五个这样的对象将占用6240个字节(15*416)。

但用这种方法给对象分配内存是非常浪费的,因为15个对象的15组方法是相同的。由于每组方法只包含过程代码,一个代码集可以被所有的对象共享。因此,尽管原则上讲每个对象具有自己的操作方法,但实际上(为节省空间)它们都共享同一个物理拷贝。

另一方面,尽管每个对象的句柄和变量在结构上是相同的,但他们不能被对象共享。因为它们在运行时必须含有不同的值。

因此,由于C的所有对象都共享同一组操作,C的15个对象实际占用内存空间只有640个字节(一组方法占用400字节,15组变量占用150字节,15个句柄占用90个字节)。640字节比6240字节要节省得多,这也是面向对象环境中给对象分配内存的通常方式。参见图1.15。

图1.15 相同类的15个对象占用的实际内存(640字节)的示意描述

在本章提到的几乎所有操作和属性都属于每个对象。它们被称为对象实例操作和对象实例属性或简称为实例操作和实例属性。然而,还有类操作和类属性。对于给定类总是存在一组类操作和类属性,不管该类产生了多少对象。

类操作和类属性需要应付不能被任何对象表示的状态。类操作最著名的例子就是New,它示例一个给定类的新对象。

消息New 不能发送给某个对象。例如,我们有类BankCustomer的三个对象,表示银行的实际客户(假设这三个对象分别为bob,carol和ted),我们想示例一个新BankCustomer对象(如alice)。给哪个对象发送消息New呢?没有什么理由发送给bob,发送给carol或ted 也没道理。更糟糕的是,永远不能示例第一个银行客户,因为开始时没有任何BankCustomer 类的对象可以向其发送New消息。

因此,New是一个必须发送给类而不是发送给对象的消息。机器人游戏中的Hominoid.New就是一个例子。这是一个发送给类Honinoid的类消息,执行类操作New建立一个新对象,即类Hominoid的新实例。

类属性的一个例子是noOfHominoidsCreated:Integer。New每执行一次,该值增加一次。然而尽管有许多机器人对象,但只有该类属性的一个拷贝。可以设计一个类操作供给外界访问该类属性。

图1.16示意了具有两个类操作(每个方法占100字节)和三个类属性(每个变量占2字节)的类C的内存结构。“类结构”的字节数(本例为206)仍然不变,无论C示例了多少对象。随着类实例的增加,C及其15个对象一共占用846个字节(即206+640)内存空间。

图1.16 15个对象和“类结构”占用的实际内存(846字节)的示意描述

注意原则上和实际中每个类都只有一组类方法。这点与实例方法不同,因为其原则上每个对象有一组方法(只是为了节省内存使对象的操作共享相同的方法)。类变量和实例变量的区别十分明显:每个类只有一组类变量,而类的每个对象无论在原则上还是在实际上都只有一组实例变量。

如果你学习过抽象数据类型(ADTs),可能想知道类和ADT 之间的区别。则答案是:ADT描述的是接口。它只描述向ADT用户提供的功能,但并不说明ADT如何实现这些功能。而类是实现ADT的具体内容(或至少具有内部设计和代码)。实际上,对于给定的ADT,可以设计和建立几个不同的类。例如,一些类可以产生运行效率非常高的对象,而对于同一个ADT的另一些类可以产生占用内存少的对象。

本书的第三部分更详细地介绍有关抽象数据类型、类及它们之间的区别。 在此之前,可以将“类”和“ADT”看作是同义词。请把这一点记在脑子里,现在让我们转到讨论继承的重要概念。

如果你写了一个类C,后来又发现一个类D 除一些额外的属性和操作外与类C几乎是一样的,你会怎么办呢?一种办法是简单地复制C的所有属性和操作,然后将其放到D中。但这种方法不仅增加了额外的工作,而且复制本身也存在维护的麻烦。更好的方法是让类D向类C“请求使用其操作”,这种方法称为继承(inheritance)。

继承(从C到D)是指类D在类C中隐式地定义其每个属性和操作,就好象这些属性和操作是在类D本身中定义一样。

C称为D的超类。D称为C的子类。

换言之,通过继承,类D的对象可以充分利用类C对象的属性和操作 。

继承代表着面向对象与传统系统方法区别的另一个主要方面。使你可以更加有效地构造软件:

下面的例子有助于说明上述原理。假设在航空应用中有一个类Aircraft。Aircraft可以定义名为turn的实例操作,实例属性名为course。

类Aircraft处理与任何种类的飞行器有关的活动或信息。然而一些特殊的飞行器执行特殊的活动,因此需要特殊的信息。例如,滑翔机执行特殊的活动(如释放拖链)可能需要记录特殊的信息(如是否连接拖链)。

因此,我们可以定义另一个类Glider,使其继承Aircraft。Glider有一个名为release Towline的实例操作和一个名为WeatherTowlineAttached的实例属性(属于类Boolean)。图1.7给出了结构图,空心箭头表示继承。

现在让我们来设想一段面向对象程序的继承机制,首先从类Aircraft和Glider生成对象,然后给这些对象发送消息。在这段程序后面讨论了标记为从(1)到(4)的四条语句。

 var ac:Aircraft:= Aircraft.New ;
 var gl:Glider:=Glider.New;
 …
 ac.turn(newCourse,out turnOK);   (1)
 gl.releaseTowline;           (2)
 gl.turn(newCourse,out turnOK);   (3)
 ac.releaseTowline;(4)

图1.17 Glider是从其超类Aircraft继承的子类

① ac表示由ac指定的对象接收消息 turn(newCourse,out turnOK) ,执行操作turn (带有适当参数)。因为ac是Aircraft的一个实例,ac将直接使用在类Aircraft中已经定义的操作turn。

② gl表示由gl指定的对象接收消息releaseTowline,执行操作releaseTowline(不需要参数)。

因为gl是Glider的一个实例,gl将直接使用在类Glider中已经定义的操作releaseTowline。

③ gl表示由gl指定的对象接收消息turn(newCourse,out turnOK),执行操作turn(带有适当参数)。如果没有定义继承,这个消息在执行时会出错(如无定义操作-turn),因为gl是Glider的实例,而Glider中没有名为turn的操作。

然而,由于Aircraft是Glider的超类,对象gl也被允许使用Aircraft的任何操作。如果Aircraft有一个超类FlyingThing,gl将被允许使用FlyingThing的任何操作。因此,标记为(3)的代码行可以正确运行,在Aircraft中定义的操作turn也会被执行。

④ 这行代码不会执行!ac为Aircraft的实例,没有定义名为releaseTowline 的操作。继承在这里用不上,只有Glider类定义了releaseTowline操作,并且Glider是Aircraft的子类。由于从Glider 到Aircraft方向没有继承,系统将由于错误而停止运行。这样设计是合理的,因为ac可能表示大型喷气式飞机,操作releaseTowline对其没有意义。

在1.6节讨论了类和对象的区别。现在我们知道对象和实例之间也存在细微的差别。尽管目前为止我们将“对象”和“实例”几乎同样对待,在某种意义上继承允许一个对象同时是多个类的实例。

在现实世界中也是如此。如果你拥有一个滑翔机,当然完全拥有一个尾部具有标识(句柄)的对象。这架滑翔机(显然)是滑翔机的一个例子,同时也是飞行器的例子。概念上讲,你拥有的这个对象是Glider的实例,也是Aircraft的实例。

实际上,上述例子说明了一个有效使用继承的有效测试。如果可以说“某个D是某个C”,则D差不多就是C的子类。因此,既然可以说“滑翔机是某种飞行器”,则类Glider应该是类Aircraft(飞行器)的子类,在第10、11和12章将进一步讨论继承的正确使用方法。

让我们进一步探讨继承。gl表示由gl引用的对象在运行时是两部分的融合。一部分是为Glider定义的实例操作和实例属性;另一部分是为Aircraft定义的实例操作和实例属性。如图1.18所示。

在大多数语言中,继承子类继承超类所提供的一切信息;子类不能有选择地继承。但可以通过技巧使子类略去继承的操作,这将在第1.8节讨论。

图1.18 类Glider的对象可访问的实例操作和实例属性

采用好的面向对象语言实现继承的实际代码非常直观。只需声明每个子类继承哪个超类。例如:

class Glider inherits from Aircraft;
…

这个继承的例子为单继承,指每个类至多有一个直接超类。此外,还有多继承的情形。多继承指每个类可以有任意多个直接超类。

多继承能将单继承的继承树转换为继承框架,如图1.19所示。

图1.19 多继承——一个子类具有两个或两个以上的超类

多继承引入了一些设计上的难题,因为子类从多个父辈继承,可能引起继承操作或属性的冲突。冲突的操作具有相同的名字,继承子类不能容易地表述继承的是哪个操作。

诸如命名冲突问题影响了多继承的声誉。多年来,对多继承的谴责和赞成已经到了炙热化的程度。我声明我是赞同多继承的,因为现实中需要频繁使用多继承的子类。正如图1.19所示,类PassengerAircraft合理地从类Aircraft和类PessengerVehicle获得继承。

因为多继承可能建立复杂和难以理解的结构,因此,使用多继承比使用单继承要更加慎重。目前为止,C++和Eiffel两个主要语言支持多继承,而另外两个主要语言Java和Smalltalk则不支持多继承。

“polymorphism(多态性)”一词来自两个希腊词,分别表示“许多”和“形态”。多态是指具有许多形态的特性,正如Red Dwarf(异形人)中的情节,宇宙飞船中的全体船员不断被一个可以迅速从一个形体变为另一个形体的异己分子袭击。

面向对象教科书有两种多态性的定义,没有一种能像Red Dwarf的比喻那样生动。下面描述了用符号(A)和(B)标记的两个定义。这两个定义都有效,而且多态性的这两个特性密切相关,并且在面向对象中十分有用。本节的后部分进一步解释这两个定义。

(A)多态性(polymorphism)是一种方法,这种方法使得在多个类中可以定义同一个操作或属性名,并在每个类中可以有不同的实现。

(B)多态性(polymorphism)是一种特性,这种特性使得一个属性或变量在不同的时期可以表示不同类的对象。

假设我们有一个类Polygon,表示二维图形,如图1.20所示。

图1.20 一些平面的但并非普通的多边形

我们可能对Polygon定义名为getArea的操作,该操作将返回Polygon对象的面积(注意,area是定义在Polygon中的属性,通过继承也是Polygon的子类的属性)。操作getArea的算法非常复杂,因为要考虑不规则的多边形,如图1.20所示。

现在让我们增加一些类,如Trigangle、Rectangle和Hexagon,都是Polygon的子类。这样做是合理的,因为三角形是多边形;矩形是多边形等。参见图1.21。

图1.21 Polygon及其三个子类

注意在图1.21中,类Triangle和Rectangle都有名为getArea的操作。这些操作完成与Polygon中的getArea相同的任务:计算由边长围起来的总面积。

但程序的设计者和编程者实现Rectangle的getArea与实现Polygon的getArea存在很大的区别。为什么?因为计算矩形的面积非常简单(长×宽),Rectangle的操作getArea代码非常简单和高效。然而,计算任意复杂多边形的面积的算法则复杂而低效,我们不用该算法计算矩形的面积。

因此,如果我们写一些代码向twoDShape对象发送下列消息:

 twoDShape.getArea;

我们可能不知道哪个计算面积的算法将被执行。因为不知道twoDShape属于哪个类。存在五种可能性:

① twoDShape是Triangle的实例。定义为Triangle的操作getArea将被执行。

② twoDShape是Rectangle的实例。定义为Rectangle的操作getArea将被执行。

③ twoDShape是Hexagon的实例。由于Hexagon没有名为getArea的操作,通过继承定义为Polygon的操作getArea将被执行。

④ twoDShape是一般的任意形状的Polygon的实例。定义为Polygon的操作getArea将被执行。

⑤ twoDShape是类C(如Customer)的实例,不是上述的四个类之一。由于C可能没有定义名为getArea 的操作,发送getArea消息将引起编译或运行时错误。这样是合理的,因为twoDShape不应该表示一个客户。

你可能对此感到奇怪,对象不知道发送消息的目标对象是哪个类。然而,这种情况是十分普遍的。例如,下面的最后一行代码,编译时我们不能告诉对象P运行时指向哪个类。实际所表示的对象在最后时刻由用户选择确定(由if语句检测)。

 var p:Polygon;
 var t:Triangle:=Triangle.New;
 var h:Hexagon:=Hexagon.New;
 …
 if user says OK
 then p:=t
 else p:=h
 endif:
 …
 p.getArea;      //p可能表示Triangle或Hexagon对象
 …

注意在上面面向对象的代码段中,不需要判断p.getArea执行的是哪一个getArea。这是一个非常便捷的隐藏实现。使得不用改变代码就可增加一个新的Polygon的子类(如Octagon)。隐含着目标对象“知道如何给出面积”,因此发送者不必担心。

再看一下声明语句 var p:Polygon 。这是对于变量 P的多态性的安全约束。在这里使用的编程语法中,P 只表示类Polygon的对象(或Polygon子类的对象)。如果P被赋值Customer对象或Horse对象的句柄,程序将出现运行错而停止执行。

getArea定义为几个类的操作,它为前面列出的标记为(A)的多态性提供一个很好的例子。变量P可以表示几个不同类的对象(如Triangle和Hexagon),是标记为(B)的多态性好例子。整个例子展示了多态性的两个方面是如何一起工作从而简化程序设计。

面向对象环境经常通过动态绑定(dynamic binding)实现多态性。在这种环境中,当消息发送后,在运行期间尽可能靠后地检查消息的目标对象。

动态绑定(dynamic binding)(或运行时绑定或最后绑定)是一种在运行时(而不是在编译时)确定被执行代码的技术。

在上述例子中,操作getArea被定义为Polygon和Triangle的操作,同样说明了覆盖(overriding)的概念。

覆盖(overriding)是指类C定义的方法在C 的一个子类中被重定义。

操作getArea原来是在Polygon中定义的,在Triangle 中被覆盖。Triangle中的操作具有与原来的操作名字相同,但算法不同。

你可能会偶尔使用覆盖技术在C的子类中取消类C的一个操作。可以简单地重定义它返回一个错误来取消一个操作。如果需要大量使用取消操作,源于超类/子类层次结构可能不可靠。

与多态性相关的一个概念是重载(overloading) ,请不要与覆盖(overriding)混淆。

名字或符号的重载(overloading)是指在同一个类中定义的几个操作(或操作符)都具有同一类的名字或符号,我们称该名字或符号为重载。

多态性和重载都要求在运行时选择指定的操作。正如上面的一小段代码中,目标对象的确切类(即将被执行操作的特定实现)直到运行时才知道。

多态性和重载的一般区别在于多态性允许使用相同的操作名在不同的类中定义不同的操作,而重载允许相同的操作名在相同类中定义几次,通常是在相同的名字空间。

选择哪个多态性操作依赖于消息发送的目标对象类。但对于重载操作而言,问题是如何在运行时将正确的程序绑定到操作名?答案是使用消息参数(参数的类或数目)。下面有两个例子:

1a. product1.markDown
1b. product1.markDown (hugePercentage)
2a. matrixl*i
2b. matrixl*matrix2

在第一个例子中,通过操作markDown来降低产品的价格。如果markDown被激活时不带参数(如1a),则该操作使用标准的折扣比例;如果markDown被激活时带有一个参数(如1b中的hugePercentage参数),则操作使用提供的hugePercentage值。

在第二个例子中,乘操作符*被重载。如果第二个操作数是整数(如2a),则操作符*为标量乘。如果第二个操作数是另一个矩阵(如2b),则操作符*为矩阵乘。

一般性(genericity)指一个或多个类内部使用的类C的结构,仅在运行时(即示例类C的对象时)才提供。

说明一般概念的最好方式是讲述一个不堪回首的故事。当时我还是一名大学生,学习一门数据结构(Data Structures)的课程101。有一个学期,Rossini 教授给我们留了一项作业,设计和编程整数有序平衡二叉树(见图1.22)。平衡二叉树的主要特点是所有的叶子在同层上拉平。

图1.22 排序整数的平衡二叉树

在往树中插入另一个整数(如5 ,见图1.23)之前,一切顺利。插入整数5后,树变得不平衡,你不得不做一些痛苦的节点调整直到树再次平衡为止。

图1.23 变为不平衡的树

经过多次的桌面设计和联机调试,我们中间大多数人的算法可以正常运行了。带着满意的笑容交出程序去度假,花费几夜的努力去忘却那个有序平衡二叉树。

但不幸的是,Rossini教授的平衡树作业进一步扩大范围。作为下学期应用的一部分(商业应用101,Business Applications 101),需要将客户和产品形成排序列表。我们这些学习Data Structures 101的学生,还没有完全忘记有序平衡二叉树,径直调出老程序,复制二份。在一份副本中,用CustomerID替换Integer;在另一份副本中,用ProductID替换Integer。

这种对老程序的复制极大地提高了生产率。但这种方法并非十分理想,因为存在严重的复制隐患。这种隐患是现在我们不得不维护三个几乎相同的程序。

因此,如果我们发现了一个更好的平衡树算法,就不得不修改三个代码。这样不仅增加了额外的工作量,而且维护三个版本也有一定的难度(除非我们有自动的复制修改程序)。我们需要一种方法,只编写一次基本的平衡树算法结构,然后当我们需要对整数、客户、产品或其他进行处理时可以应用多次(不是简单的复制)。

此时此刻,一般性就像一匹疾驶而来的白马拯救了我们。如果将BalancedTree 定义为参数化类(正规称为一般类),则说明至少在BalancedTree中有一个类直到运行时才赋值(参数化类在C++中称为模板类)。可以猜到这个类就是我们实例化时,存储在特定平衡二叉树对象的节点中项所构成的类。

因此可以将类BalancedTree写为如下形式:

 class BalancedTree<ClassOfNodeItem>;
 …
 var currentNode:ClassOfNodeItem.New;
 …
 currentNode.print;
 …

注意参数化类的参数ClassOfNodeItem。这是一个形式参数,其实际“值”在运行时提供。例如,当实例化类BalancedTree的一个新对象时,将提供一个真正的类名作为参数,如:

 …
 var custTree:BalancedTree :=BalancedTree.New<Customer>;
 var prodTree:BalancedTree :=BalancedTree.New<Product>;
 …

因此,custTree现在表示一个在节点中保存类Customer实例的对象(即BalancedTree的实例),如图1.24所示。当然对于prodTree是类似的。

图1.24 Customer对象的平衡二叉树(我在节点中保存的是客户的名字),这棵树是变量custTree表示的对象

下面的程序看起来好像将第一段程序复制了两次(一次是为Customer,一次是为Product):

 class BalancedCustomerTree;
  …
  var currentNode:Customer:=Customer.New;
  … 
  currentNode.print;
  …
 class BalancedProductTree;
  …
  var currentNode:Product:=Product.New;
  … 
  currentNode.print;
  …

最后,注意currentNode.print语句。这是多态的一个好例子,因为当我们在参数化类BalancedTree中写这个语句时还不知道currentNode的类是什么。因此,实例化BalancedTree时操作print应根据用于实例化特定树的类而定义。

另举一个例子,如果你设计一个参数化类HashTable< c>,则应指出任何向C提供实际参数的类(如Symbol)必须定义操作hash 。在第12章详细讨论一般性可能带来的危险。

你可能意识到一种不用一般性也不用复制编写BalancedTree的方法。可以让平衡树的节点接受一个超类/子类层次结构中最顶层的类对象。如果将该类命名为类Object,则该代码段为:

 class BalancedTree;
  …
  var currentNode:Object:=Object.New;
  … 
  currentNode.print;
  …

现在平衡树的每个节点将接受任何一个对象的加入。甚至可以将客户、整数、产品、多边形、牛仔及马混在相同的树中。这几乎没有什么意义。更糟的是让这些不同的对象类理解消息print是不太可能的。

BalancedTree和HashTable都是容器类(container class)的例子。容器类用在某些(通常比较复杂)结构中保存对象。一般性常用在面向对象中设计这样的容器类。尽管不是必须使用一般性为容器类编写可重用代码,但它确实比复制的程序或将任意类的对象混合在同一个容器的脆弱设计要好。

因为“面向对象”在英语中就没有绝对优势的定义,因此,针对面向对象定义的特性从来就没有一致的意见。我本人认为以下特性对面向对象是至关重要的:封装、信息/实现隐藏、状态保持、对象标识、消息、类、继承、多态性及一般性。

面向对象的封装产生一个由表示对象状态的属性及其操作组成的软件结构(“对象”),用实现的术语讲,就是操作的方法处理保持对象状态的变量。封装确保对对象内部信息的任何修改(或访问)都必须通过对象的操作。

信息/实现隐藏是对好的封装的一种回报。好的封装使得信息对于一个对象而言是局部的,因此可以将对象内部实现的设计策略保护起来,不受外界的干扰。

状态保持特性指一个对象具有保持信息能力,包括激活操作之间的间隔。

对象标识给予每个对象一个唯一且永久的标识,它与对象的当前状态无关。对象句柄是赋予对象标识的通常机制。

发送消息的对象必须知道目标对象的句柄。消息由目标对象的操作名及操作的输入、输出参数组成。参数可以是数据或数据指针。但在纯面向对象环境中,参数仅指对象。

从相同类衍生的对象共享相同的结构和行为。类是一个设计和编程结构的模板,在运行时从模板可以实例化对象。类可以有一组类操作和类属性。

原则上每个对象有自己的一组方法实现实例操作及一组变量实现其实例属性。然而实际上为节省内存空间,相同类的对象通过共享每个实例方法的相同副本。

多个类可以形成一个超类和子类的继承层次结构(或框架)。继承允许类的对象使用其超类所具有的一切信息。必要时,类的操作可以在子类中重定义(“覆盖”)。

多态性指多个不同的类可以定义同一个操作名,并且在每个类中具有不同的实现。多态的另一个特性是允许一个属性在不同的时间表示不同类的对象。

多态性增加了一种新的实现隐藏方法使面向对象更加有效。例如,发送对象在发送消息时可以不知道具体的目标对象类。只要设计者知道所有可能访问的具有正确名字和参数操作的类,在运行环境中确定选择哪个特定操作就可以了。

重载是一个类似于多态性的概念,运行时通过检测消息的数目和/或参数的类,从操作的多个不同实现中选择一种实现。多态性和重载典型地属于动态(或运行时)绑定。

一般性允许参数化类,以类作为参数实例化一个对象。一般性使得建立“一般”容器类非常简单,这些容器类作为框架类等到运行时再增添具体内容。参数化类具有复制程序的优点,但没有复制维护开销。

①(a)重写机器人hominoid-navigation算法,使其更健壮。

(b)你能发现在Grid中定义的操作insertHominoid(hom:Hominoid,location:Square,out insertOK:Boolean)中的问题吗?

② 对象知道自己的句柄吗?如果知道的话,对象如何表示其句柄?

③ 为什么在消息参数中很少使用相同的参数名既作为输入参数又作为输出参数?假设参数表示具有句柄的对象。

④ 在 1.5.3节中,我说过“在纯面向对象环境中,不需要数据。”换言之,一切都是对象(对变量操作的封装),操作本身通过实现操作的变量指向对象。但确实存在一些数据“归根到底”就是数据。因此,任何东西确实都是对象吗?整数和实数及其成千上万的实例也是对象?如何创建它们?

⑤ 实例操作可能涉及一个类变量。然而在纯面向对象环境中,类操作不能直接涉及对象内的实例变量。为什么?

⑥ 执行1.7节的Glider.New,创建了多少对象?

⑦ 面向对象程序是如何进行初始化的?

⑧ 关掉计算机时,对所有的对象会有什么影响?

⑨ 关掉计算机时,对所有的类会有什么影响?

⑩ 你能想出某种语言如C++中有关面向对象的健壮的封装机制的一种简单方法吗?

⑪ Peter Wegner 在一篇顶级论文中,将环境划分为对象结构(object-structured)、基于对象(object-based)、基于类(object-class)或面向对象(object-oriented)几个范畴。第一个范畴只具有封装和状态保持特性;第二个范畴增加了对象标识特性;第三个范畴增加了类的概念;最后一个范畴增加了继承和本章提到的其他特性。判断你目前使用的语言最接近这四个范畴中的哪一种。

⑫ 我在本章中曾提到过Java语言支持单继承但不支持多继承。因为类修饰符extends最多扩展一个其他类。但类内部implements(实现)许多接口。因此,我的说法正确吗?如果你熟悉Java语言,请解释一下Java语言中,继承机制中的extends和implements之间的区别。

⑬ 请你选择一种面向对象编程语言重写机器人伪代码。

⑭ 考察你或你的公司购买的一种厂商宣称为“面向对象”的软件。软件的哪些特性声明厂商是“面向对象”的?你认为厂商的宣称恰当吗?如果是这样,你得益于产品吹捧的哪些面向对象特性?

①(a) 有两个建议:一是假设START方块完全被墙包围,二是假设某人在方格中忘记标记FINISH方块。修改算法处理这两种情况以及其他任何你考虑到的异常情况,如没有对advanceOK是否为真安全检查。

(b)该操作中的问题是:Grid对象中的insertHominoid操作需要location:Square (Hominoid对象的开始位置)的信息吗? 应该使用该信息告诉Homoid对象它的初始位置,但Homoid没有定义setLocation操作!因此,不应在Grid中定义insertHominoid操作,而应在Homoid中定义insertIntoGrid(grid:Grid,location:Square,out insetOK:Boolean)操作。此外,还需要在Grid中定义isAWallLocation:Boolean操作。

② 回答是肯定的。对象有一个不必声明的变量(实际上是常量),保存自己的句柄。该变量由关键字self,this,this 或Current(分别对应Smalltalk,C++,Java或Eiffel)来命名。

③ 因为这样做会使消息的目标改变了其中一个参数的句柄,这是一种拙劣的设计方法。消息的发送者有权保证其变量的句柄在发送消息的前后保持一致。少数语言禁止这种方法;在其手册中有这样的说明:“参数通过传送值进行对象访问,且不能被目标操作代码改变。”

④ 在彻底的面向对象语言如Smalltalk中,所有一切的都是对象;实际上,Smalltalk坚决地进行“不含数据”的定位。例如,在Smalltalk中下面的加法表达式

x <- 5 + 7

解释为“向参数对象5和对象7发送消息plus。”赋值操作符(<-)解释为“用变量x替换对象12的句柄。”然而并不是所有面向对象语言都像Smalltalk那样严格。在类似Eiffel的语言中,仍然有数据类型(如integer,real,char,Boolean等)。但任何主要的结构在运行时是创建对象实例而不是数据实例的类。由于编程原因,Eiffel妥协了“所有都是对象”的原则:将整数、字符等作为可以与C代码接口兼容的数据类型。在C++中,具有标准数据类型的标准C程序可以与C++程序混合使用。因此,在C++中所有顾虑可以打消!但当我告诉人们纯面向对象中没有数据时,经常受到人们的抨击。最普通的例子是人们用Integer和Date反对我。为什么这些是类而不是传统的数据类型?在使用数字5之前,是否必须先声明Integer.New,而在使用1066年9月25日之前,是否必须先声明Date.New ?回答是否定的。诸如Integer或Date这样的类被称为文字类(literal classes)。属于文字类的对象称为文字对象(literal objects)。文字对象就是其表示的值。大多数文字对象是不变的:从不改变值。尽管每种面向对象语言都有其处理文字类的方式,但多数语言假设文字类的所有实例是预先定义的(或在原处通过转换类似“3月15日”的文本串创建的)。在将这种“类”作为标准数据类型的语言中,实例实际上是传统的数据值而不是对象。在下面两种情况下,从文字类进行实例化是不必要的也是非法的:

  Integer.New;    // 非法代码!
  Date.New;      // 在大多数语言中是非法代码!

⑤ 困难在于类要知道“哪个对象的属性”。回忆一下,对于一个给定类,在运行时可能会有上千个对象。想要获取该类的某个对象的唯一方法是知道该对象的句柄,然后发送消息给它。尔后该对象友好地将属性消息返回给发送消息的类。

⑥ 创建了一个对象(每当执行类操作New时,总是创建一个带有句柄的对象)。然而,这个程序中命名为gl的对象是Glider的实例,通过继承它也是Aircraft的实例。

⑦ 面向对象程序的初始化依赖于你所使用的语言和操作系统。操作系统可以通过三种普通的方式将控制转移给应用程序:

⑧ 当关掉计算机时,丢失了内存中的所有对象及其所包含的信息。如果这样对你有影响,则必须在面向对象程序终止前,将信息保存在磁盘上。在面向对象数据库管理系统(ODBMS)中,多少可以直接存储对象。但如果你使用的是关系型数据库,就不得不在存储信息之前将对象信息“分解”成通常的表格形式。

⑨ 当关掉计算机时,对类没有什么影响(对大多数环境而言)。你的类已经编译并保存在永久的磁盘上。这也说明了类和对象之间的区别,前者是永久的形式,而后者是易变的运行单元。

⑩ 在一些语言中,不守规矩的设计者可以设计一些方法,使外界可以“跳过对象的围墙”并直接处理变量。一种方法是通过C++的“友元”方法。与多数其他设计过失一样,这种方法通常具有影响大的罪名。

⑪ 如果你已经为你目前所使用的语言选择了一种术语,考察一下哪个面向对象属性你认为最有价值。如果你选用的语言不是完全面向对象的,你最希望你的语言应具有本章提到的哪些特性?为什么?

⑫ Java实际上只支持本章描述的“继承能力”意义上的单继承。假设类S有下面的语句:

  extends C implements I1,I2

表示S可以访问C的所有操作(Java方法)。换言之,通过extends的构造,S不仅继承了C的接口,而且还继承了C的能力(使接口工作的代码)。然而,通过implements 构造,S继承了责任,但没有继承能力。在这个例子中,S的设计者或程序员对于定义在接口I1和I2中所有操作,负责提供可行的Java方法。

⑬ 下面是你可能用Java重写的机器人程序。假设该算法包含在操作navigate中,它将是类Grid的一个操作(对方格对象的任何访问都通过this)。还假设方格对象被正确地初始化(如以前插入的任何机器人已被删除)。

Public boolean navigate( )

{// 在起始方格中放入一个新机器人,并将机器人导航到正确的结束方格,每一步都显示机器人。如果算法有问题,则返回false;否则返回true。


 Hominoid hom = new Hominoid ( );// 创建Hominoid的新实例,由hom表示

  int oneSquare = 1;        // 常量
  int initialTurnCount;

 Square startSquare = this.start;
 Boolean insertOK = this.insertHominoid(hom,startSquare);
          //如果成功insertHominoid返回true
 if (!insertOK) return false
          //如果机器人没有将OK放入方格中,则终止。

     // 将机器人设置为正确的方向
     // 将机器人最多转4次或直到机器人在其前方有清楚的路径 

 initialTurnCount = 1;
 while (initialTurnCount <= 4 && hom.facingWall)
     { hom.turnLeft();initialTurnCount++;}
// endwhile结束

this.display();      //显示方格
hom.display();      //显示机器人

while (hom.location != this.finish)
{ if (hom.facingWall)
   {hom.turnLeft();

 if(hom.facingWall) { hom.turnRight();hom.turnRight();}
 // endif结束
} // endif结束

 hom.advance(oneSquare);
 hom.display;
} // endwhile结束
               //机器人成功结束!
return true;

 } //结束导航


前面讨论了面向对象的固有特性,现在来看一下面向对象是如何适用于软件开发的广阔领域。

当听说Wolfgang Pauli教授提出一种新的基本粒子(μ介子,μ- meson或muon)时,Isidor I .Rabi教授曾经立即予以反驳:“谁承认这种说法?”鉴于此事例,我在本章开头部分列出一些对面向对象有贡献的人士。下面将面向对象置于社会环境中,讨论对这种软件方法的看法。然后,将面向对象置于工程环境中,将面向对象与电子学做一个类比。最后,阐述面向对象对企业中的程序员、系统分析员及经理们究竟能带来哪些益处。

与人类发明史上的许多创举不同,面向对象不是在瞬间出现的。面向对象不是某个人在浴缸中的突发其想,而是许多人历经多年研究积累的产物。第1章中介绍的面向对象概念,就像几个支流通过历史的变迁而汇集到一起,最后形成面向对象的河流。

下面列举了(按年代排序)在理论研究方面和在工程实践中,对面向对象做出了重大贡献的人士,如有遗漏请谅解。

谈到任何软件范畴的贡献者都会提到Larry Constantine,因此就从资深的Larry Constantine说起。虽然20世纪60年代,Constantine并没有在“面向对象”的旗号下做任何事,但他却致力于研究软件设计的基本准则(参见本书后面参考文献中列出的[Constantine,1968][1],此写法为文献的缩写形式)。实际上,他是最先提出软件在编程之前应该进行设计的几个人之一。Constantine的许多著名观点(如耦合和内聚)一直沿用到现今的面向对象领域。

Dahl 和Nygaard引入的几个概念现在已成为面向对象的组成部分。类的概念就是一个最好的例子,这个概念首次出现在Simula语言中(参见[Dahl 和Nygaard,1966])。

Kay,Goldberg及其同事经过几年的研究,于1970年左右在Xerox公司的Palo Alto研究中心设计出了Smalltalk语言(参见[Kay ,1969])。这一研究成果提出了许多现在成为面向对象核心的概念(如消息和继承)。许多人至今仍认为Smalltalk语言和环境(参见[Goldberg and Robson,1989])是面向对象完美的实现。

Dijkstra的“软件正确性的理念(Conscience of Software Correctness)”,使人们几十年来一直耿耿于怀。在Dijkstra的早期研究中,提出了用抽象层构造软件的观点,在两个相继的层之间用严格的语义区分。这实际上是一种封装的形式,也是面向对象的主要概念之一。

在20世纪70年代,Liskov使抽象数据类型(ADT)的理论和实现有了重大的进展,奠定了面向对象的基础。Liskov的最著名的研究成果就是CLU语言,支持隐藏内部数据表示方法,参见[Liskov et al.,1981]。

在具有划时代意义的论文中,Parnas提出了模块软件构造原则(参见[Parnas,1972])。尽管面向对象的构造优于传统的过程模块,但Parnas的信息隐藏的许多基本思想仍然可以应用到面向对象的系统中。

Ichbiah与其研究小组开发了“Green”编程语言,是一种被美国国防部所采纳的Ada语言(现在称为Ada-83)。Ada-83中的两个概念(一般性和包)也是面向对象中非常重要的内容。这个语言的最新版本Ada-95更为全面地支持面向对象。

C++语言有一个有趣的家谱。以前由Martin Richards开发了一种BCPL语言[Richards 和Whitby-Strevens,1980]。由此产生B语言,BCPL的缩写。由B语言产生C语言,经过Stroustrup的研究由C语言产生了面向对象的C++语言。

下面引用一段Stroustrup在“C++的产生”[Stroustrup,1991,p.4]中的一段话:

C++主要是为编程人员而设计的,使其编程时不一定必须使用汇编,C或各种现有的高级语言。其主要目的让每个编程人员可以更容易更愉快地写出好的程序。历来没有有关C++设计的论文,设计、文档及实现都是同时进行的。

由于C++的面向对象是从早期非面向对象并且十分低级的语言移植而来的,因而它的语法并不十分清晰。然而,尽管有Java语言的出现,C++仍然是最广泛使用的面向对象语言。由于C++的前身是C,因此在许多机器和操作系统平台上具有可移植性,从而极大地推动了面向对象语言的流行。从这个意义上说,Stroustrup的对该领域的贡献是巨大的。

Meyer的贡献是将最佳的计算机科学思想与最佳的面向对象思想融合起来。其结果是产生一个称为Eiffel的语言和环境。Eiffel在软件界确实是个珍品,因为它对理论、软件工程及希望优化代码的人们都具有吸引力。无论你的企业选择哪种面向对象语言,如果希望成为真正的面向对象专家,就应该学习Eiffel中的概念,[Wiener,1995]是学习Eiffel的一本好书。

这三个人物一同被冠以“The Three Amigos”的绰号。尽管他们在面向对象领域都有各自的主张,但在20世纪90年代后期,他们通力合作将面向对象用合理的符号表示,产生了统一建模语言(Unified Modeling Language,UML)。 这个图形化的建模语言既有可视的表达形式,又有严谨的语义支撑。本书第3章至第7章介绍UML。

本节介绍软件工业如何促进面向对象时代的真正到来。 

老生物学家们常爱发表这样的言论:个体生物重演了生物发展史。其含义是个体生物胚胎的发展通常演绎着生物整体发展的进化过程(如人类胚胎的发育过程)。当然在时间上存在很大的差异。个体生物的发展可能只有几个月,而整体生物却世代繁衍。

尽管老生物学家的说法无庸置疑,但在软件工程中却出现了一种新观点:面向对象软件工程的历史重演了传统软件工程的历史。当然在时间跨度上也存在很大的差异。形成成熟的过程和数据库结构花费了几十年的时间,而钻研面向对象软件只有几年的时间。

软件开发几乎是从编程开始的。随着系统规模扩大及人们经验的不断丰富,人们意识到编写应用程序代码仅仅依靠个人水平是有问题的。即便这样开发出来的应用程序可以奇迹般地运行,但由于代码缺乏规范而使得对代码的任何修改几乎是不可能的。

由此而引入设计。软件设计是在编写代码之前,对代码的相关部分进行规划。这种根本的改进甚至可使人们解决潜在的维护问题。

到目前为止一切顺利,现在可以生产出精致的软件。但一些敏锐的人发现这些精致软件大部分不能满足用户的需求。为满足用户对于可用软件的不断追求,有规律地以及更加严格的分析方法应运而生。

最终,我们有幸得到了计算机辅助软件工程(Computer-Aided Software-Engineering,CASE)工具。起初,这些工具的名声并不乐观。但随着工具的改进,逐渐改变了状况,并在联邦保护软件(Federal Protection Program)中恢复了名誉。今天,以前的CASE工具称为自动建模工具。建模工具帮助我们进行需求分析、软件设计和软件构造,并且使软件开发和维护更加便于管理。

在软件发展的整个历史中,人们一直试图提高软件的可重用性。但不幸的是大多数代码过程单元不是独立的,因此难以独立地重用。时至今日,面向对象的应用使软件重用获得生机。

然而,面向对象并不是万能的。如果对象的类不按照本书后面介绍的指导精心设计,那么面向对象也不可能提供可重用和可靠的软件。发生这种不幸情形,多数源于管理者对面向对象缺乏真正的认识。

如前所述,面向对象的历史是与软件的主流史并行发展的。然而对面向对象而言,从实现到抽象的发展异常迅速。面向对象编程在20世纪80年代开始流行。在同一时期引入了面向对象设计和面向对象分析。大约在90年代出现了面向对象数据库管理系统(ODBMS)和面向对象建模工具。

面向对象领域的迅猛发展使人们患上了奇怪的健忘症。一些人经历了面向对象的个体发展而一时忘记了整个软件主流的发展。他们的口号是:“1990年以前的任何知识都不必了解!”他们是热血而又执着的面向对象革命者。谴责当时使用已过时的COBOL语言建立的软件就像是纸老虎,而认为通向叶卡特琳堡的路只有一条。

现在他们的革命热情有所降温。对象革命已经取得胜利,其煽动者和拥互者自身已成为建设者。现在软件市场中的许多工具和技术或多或少地依赖于面向对象技术。客户/服务器领域和其他分布式系统更是如此。

软件王国总是充满着革命。下一场解放程序员的革命,分布式组件软件已经影响着我们。从一些热烈的讲演中得知,使用分布式组件可以获得比起初的对象革命更多的益处。 

第15章讨论从面向对象到组件软件的应用。

20世纪80年代,Brad Cox用某种类似现代生活中用到的硬件集成电路(IC)的方式看待软件对象(参见[Cox,1986])。当华盛顿大学的考古学家们在我办公室的一堆论文中寻找时,我想起了这个类比。他们发现一本Merrill Skolnik著的《雷达系统导论》(Introduction to Radar System),在书中Skolnik先生提出下面的观点:

电子工程可以根据以下分类: 组件;技术;系统。组件是可以组合的基本构造单元,通过使用适当的技术而产生系统。

如果对上面的一段话进行一下替换,用“软件”替换“电子”,用“类”替换“组件”,则变为如下面描述:

软件工程可以根据以下分类:类;技术;系统。类是可以组合的基本构造单元,通过使用适当的技术而产生系统。第15章讨论的软件构件类似于电子印制电路板,这种构件实际上称为组件。

尽管这种想象比较吸引人,但我们不应忘记,选择有用的电路封装在芯片中有赖于工程师对电路的正确标识。人们会冲出去购买IC用于操作放大器、音频放大器、计时器、线驱动器等,但没有人愿意去购买晶体管、感应器及电阻的超大规模IC从头做起。在制成第一个有用的IC之前,继电子系统之后,工程师们经过几十年的时间才意外地发现了这个有用的方案。

同样,在软件中我们必须确保开发的类有效、健壮、易于抽象。例如Customer类和可爱的Stack类可能大受欢迎;而Egabragrettu类则可能令人想起路边的垃圾。

Skolnik先生的第二个观点是关于技术的。因为,IC不能被组合则几乎是无用的,幸运的是电子工程师已经生产出将IC集成在一起的印刷电路板。

类似地,开发面向对象软件,必须进行“宏”一级的设计,在这个层次上处理类(及类在运行时产生的对象)之间的联系。显然类的内部设计与更高层次的类间设计紧密相关。道理很简单,因为PCB的布局依赖于集成在其上的IC的设计程度。

在类的内部层次和类间层次上都存在着面向对象设计的优劣。因此,好的面向对象系统与好的电子系统一样,不仅取决于高质量的抽象而且取决于建立这些抽象的高水平技术。本书第二和第三部分详细讨论这些问题。但首先我们必须清楚一个基本的问题:面向对象的适用范围是什么?

本节的题目既迎合愤世嫉俗者又符合盲从者。

一些反对者可能会说面向对象没有什么优点;它仅是一种流派或是一场从西方一些地区引发起的全球性阴谋。而一些激进派则宣称面向对象是一流的并且是所有软件成功的唯一途径。面向对象不仅适用于Windows系统,而且还适用于无所不能的分布式Web体系结构。

这两种说法都太极端。作者认为面向对象是有用的,但不能神化,它还不够完美,其特定实用程序依赖于在软件开发过程中的使用方法。

没有一种有价值的软件工程方法可以成为“当年时尚(Fad of the Year)”。当年时尚指某种方法在几个月或一年内变得十分风行(有关“Fad of the Year”的详细信息参见[Page-Jones,1991])。盲从者歇斯底里地指望“当年时尚”可以解决所有的软件问题。怀疑论者则给盲从者泼冷水而坐等其观。当不加选择地使用这种方法而效果平平后,盲从者则放弃这种方法蜂拥地转向下一个当年时尚。如果你的企业在“技术的浪尖上摇摆”,则应马上扭转盲从的局面,可能会从面向对象的技术中获得一些收益。

面向对象不是万能的解决方案,愚蠢的解决问题方案也会使你的企业步入困境。然而正如在本书中将要看到,面向对象尽管充满挑战,但确实是一种有效的软件开发方法。一个成熟而职业化的企业不应该以极端的方式对待面向对象,而应该认真研究面向对象方法并将面向对象纳入开发专业软件的长期计划中。

下面讨论面向对象对企业的六个主要软件活动的内在影响。

结构化技术的过程分析和数据分析之间的边界在哪儿从未解决。数据流图的过程世界与实体关系图的数据世界难以共存。过程和数据分析在某些场合可以满足要求,而在某些场合就会发生冲突。这种冲突在实时系统模型中尤为突出,如控制过程与数据模型的对应关系经常变得不清晰。

面向对象方法在生命周期的早期就将过程和数据研究融合在一起。尽管不能明确地称为“过程和数据分析”,但在谈论面向对象(如第1章所述)时称“动态和静态分析”更为妥当,使用面向对象概念将这两方面的分析很好地协调起来。难怪有人将面向对象中过程和数据的融合比作Einstein的相对论中空间和时间的融合,尽管这种比喻有些过分。

在软件设计中,面向对象既有优势,又有不足。

面向对象的优势是使设计者将软件中的棘手问题利用封装特性隐藏起来,这些问题包括:费解的数据结构、复杂的组合逻辑、详细的过程和数据之间的关系、高深的算法及可怕的设备驱动程序。

面向对象的缺点是应用封装和继承特性使结构本身变得复杂。在面向对象中很难创建一个戈尔地雅斯吊床难结,要么不可建立,要么使得系统的运行像一匹负重的赛马。避免出现这些问题正是面向对象设计者所面临的挑战。

本书旨在提供一些思想、技术和原则使读者可以应付面向对象的设计挑战。本书第二部分介绍 UML的大多数有用特性,UML是描述和探索设计问题的最流行方法。

第三部分介绍一些设计原则和准则,据此您可对设计进行评估。使用这些原则和准则可以创建面向对象的框架,由此可构造协调一致的系统并可独立进行维护。尽管面向对象的设计有时是非常艰辛的,但一旦完成它,对处理大量复杂单元所带来的益处要多于采用其他设计技术。

采用面向对象方法建立系统最常考虑的质量要素为: 可重用性、可靠性、健壮性、可扩展性、分布性和可存储性。

(1)可重用性

面向对象通过在类的级别上而不是在各子程序级别上提高代码重用来改进软件可重用性。在企业中通过开发和建立适合企业应用的类库,这种方式实际上是建立一种新的符合特定需求的非常高层的语言。

在实际中,对象类是一个足够复杂的有机体,可以作为独立的软件单元从公司中的一个应用移植到另一个应用。至少类可以利用辅助类的框架来完成其特定功能(在9.1和9.2节将进一步解释)。

(2)可靠性

可靠代码的运行具有可重复性和一致性。仅当能用某种方法证明代码的正确性时,代码才可达到这些质量要求。面向对象代码采用类的不变式(class invariants)确信的断言,借助自身进行验证。类的不变式是指给定类中的每一个对象必须满足的条件。例如,类Person的不变式可能为 dateOfBirth <= todaysDate。

类的不变式(第10章介绍其他有关内容)使得彻底地验证代码成为可能。在静态分析或检查中,可以验证设计或其结果代码是否满足设想的不变式条件。虽然不可能证明(即使采用面向对象)代码绝对正确,但面向对象确实使检查代码的行为变得更加容易。

(3)健壮性

软件的健壮性是指软件发生故障时的完全恢复能力。典型故障为声明错误、内存错误、外部设备错误及算法溢出。健壮的软件可以捕获异常并执行故障恢复程序(通常称为异常处理程序或营救程序)。

许多现代的面向对象语言和环境都支持错误检测和处理功能,因此有利于开发健壮的软件。获得健壮的面向对象代码的有效方法是将推断和恒定条件的概念与异常处理的概念相结合。在某些面向对象环境中,在运行时可以监控恒定类和其他推断,当推断发生错误时,软件可以轻松地恢复。

对异常处理的另一种方法就是不进行异常处理:不检测异常,当异常发生时,只是简单地让软件崩溃。当然,也无健壮性可言!

(4)可扩展性

软件的可扩展性简单地用技术术语描述即“说明域与实现域之间是同构的”。用通俗的话说,即解决问题的模型应该满足问题的模型。为此,必须保证用户的一些小的改变不会导致主要系统灾难性的后果。并且,当修改面向对象代码时,很少会引发其他部分产生的莫名其妙的问题。由于面向对象基于更高层次上建立软件单元,它更接近生活的抽象,因此比传统技术更容易建立“同构”。

可扩展性和继承性经常一起使用。用户常在已经声明的主题中增加变量对系统进行扩展。例如:“不仅仅只定义客户,现在需要区分国内客户和国外客户”。使用面向对象技术,可以在已有的超类下增加继承子类的方法实现扩展。

(5)分布性

1989年,面向对象管理组织(Object-Management Group,OMG)承担了一个十分艰巨的任务:将几十个主要硬件和软件厂商统一在面向对象的可互操作标准上。我曾对他们所做的努力表示怀疑,但他们取得的成功使我感到震惊!

最为显著的成果就是公共对象请求代理体系结构(Common Object Request Broker Architecture, CORBA),这种软件体系结构支持分布在多个平台上的面向对象系统(想了解有关CORBA的更多知识,参见[Mowbray and Zahavi,1995]和 [Orfali et al.,1996])。这种体系结构引起了人们的很大关注。CORBA还可使对象“互相交谈”,不仅可以在类似的机器上,而且可以在运行不同操作系统或连接不同网络的机器上进行通信。 

在CORBA环境中,甚至可以用不同的语言编写类,然后用不同的编译程序编译的方式建立对象。此外,最重要的是CORBA提供了各种标准服务(如复制、代理、关系处理和事件仲裁等服务),这样省去了编写许多分布式系统必需的冗长代码。

换言之,CORBA使平台的分布式和异构对用户及应用设计者和编程者透明。编写消息时可采用类似通常的单处理器的方式,让 CORBA服务处理许多繁杂的底层细节。

(6)可存储性

如果不提及面向对象数据库管理系统,那么本节就不能算完整。建立任何面向对象应用,ODBMS都非常有用,特别是应用涉及到声音和图像时更是如此,因为这些数据不适合用标准的表格形式存储。

ODBMS可以存储任意的对象类(不仅包括诸如String、Real、Integer和Date类,而且还包括Customer、Aircraft 、CityMap、VideoClip等),此外,它还提供面向对象封装、继承、多态及其他重要的面向对象特性。大多数 ODBMS提供查询语言(如面向对象查询语言,OQL)来代替关系DBMS的SQL。

可重用性、可靠性、健壮性、可扩展性是软件维护的四大支柱。许多企业在软件维护上花费很高。由于面向对象可以提高这四方面质量特性,因此能在以下方面降低系统的维护开销:

图形应用一直是面向对象的主要选择。通常人们通过面向对象实现图形用户界面(GUI)。这样做有两个原因:其一是概念;其二是实现。

在概念上,面向对象的隐喻较好地符合典型的窗口/鼠标/图标界面。比如屏幕上有一个图标,该图标可以表示一个对象如客户。用鼠标点击图标选择该客户,弹出一个菜单,菜单选项与应用于该客户的方法一致。如有一个选项对应changeAddress,而另一个选项对应reassessCreditLimit等。而且,国内客户的菜单可以与国外客户的菜单不同。每个菜单只列出特定客户类型的商业行为。

甚至多态性也可以出现在用户界面中,多态性指一个方法对于不同的类可以有不同的含义和不同的实现。如屏幕上有一个图标表示电子表格对象,而另一个图标表示文档对象。当用户双击Open菜单项时,可以根据这两个图标哪个被加亮,对该对象执行电子表格程序或文本处理程序。换言之,Open方法的特定版本的执行依赖于被加亮的类是Spreadsheet还是Document。

在实现上,允许用户可以建立窗口/鼠标/图标界面的许多商用软件库都是用面向对象语言编写的。由于窗口本身具有许多面向对象的属性,因此大多数窗口界面的开发工具,都有通过窗口运行面向对象的痕迹。

因此,如果说面向对象本身增加了软件的易用性不太准确,但可以准确地说,好的图形用户界面增加了软件的易用性,而面向对象是建立支持GUI软件库的最佳途径。

目前为止,本书涉及的大部分内容是面向技术人员的。但针对经理的内容是什么?面向对象的另一个技术特色是否一定会慢慢消亡或转到另一个企业?或者面向对象仅仅是使经理搬起石头砸自己脚的绊脚石?

回答是否定的。面向对象不仅适合普通人员,也适合经理们。例如,降低维护开销的技术可以释放管理者的资源,将其投入到待处理的应用中。在经理们看来,面向对象不是纯技术的,面向对象既能给企业的组织也能给经理的工作带来变化。

当一个企业采纳了面向对象,其组织将发生变化。 类的重用需要类库和类库管理人员。 每个程序员都要加入到两个组中的一个:一个是设计和编写新类组,另一个是应用类创建新应用程序组。面向对象不太强调编程(重用性意味减少新代码),需求分析相对地将变得更加重要。

新的符号语言如UML也将带来重大影响。尽管需求分析和软件设计是不同的两个方面,但UML被广泛地应用到这两个模型中。这使现代的O.O.符号化,而结构技术在这方面是非常缺乏的。直到我遇到一家新的面向对象企业的经理告诉我下面一段话,我才意识到这两方面是密不可分的:

过去,技术人员在画圆形图(数据流图中的过程)时,知道他们在做分析,转到画方框图(结构图中的模块)时,知道他们开始设计。现在我从不知道他们在做什么并对此十分担心!

作为转向面向对象企业的经理,应该意识到组织的变化。让职员适应新的角色,在这些角色中需要管理参与工作的人员,鼓励重用而不主张重复编码。需要给技术人员充足的时间考虑类的设计使得构造的类可以满足重用的要求。总之,应该使用不同的术语、不同的工具和不同生命周期以及新的目标来管理项目。

如果经理将面向对象作为一种方法而不是作为一种目标,将会取得良好的效果。面向对象是有可维护性、可扩展性、健壮性、分布性、GUI支持,减少交付时间等诸多益处。作为经理应始终确立目标,并将面向对象作为一种技术来达到预定的目标。

如果在头脑中没有目标,那么面向对象的所有事物开销(金融、组织、社会及感情)似乎都是昂贵的代价。然而,如果不仅知道做什么,而且知道为什么这样做,你就能实现你所追求的面向对象目标。

在面向对象中有一种说法,即面向对象构件与电子集成电路有些类似。至少在软件中有机会象现代电子工程师那样构造系统:可以通过预制构件的连接实现有效的抽象。但为了实现这一点,首先必须标识正确的软件抽象,然后以构造的方式进行联接。

要想掌握“软件集成电路”的技术,就必须广泛而深入地研究面向对象。“面向对象”是一个术语,融合了从20世纪60年代到现在许多软件研究者的思想。但并非所有人都赞成这个观点。一些激进派认为面向对象是对过去的一个完全突破。而另一些反对派则持相反观点:面向对象至多是“换汤不换药”。

根据我的体会,面向对象既不是“换汤不换药”,也不是对我们所了解的软件的完全突破。而是朝着满足开发日益复杂软件的挑战迈进了一步。

面向对象还解决了结构化技术存在的两个问题。一个是过程和数据的分离以及需求分析模型和软件设计模型的不匹配。另一个是所谓的信息与实时系统开发方法的分离。我认为好的面向对象方法可以处理这两类系统。

构造可靠、健壮的面向对象软件需要建立各种断言,如类的不变式,可被运行时的异常处理所监控。软件维护得到改进不仅在于可重用性“减少了维护代码”,而且在于软件建立在良好的抽象基础之上。

尽管面向对象并不仅仅适用于图形,它确实对建立图形用户界面十分有效。因此面向对象的抽象适合众多用户所希望的友好、自然的界面。

最后需要强调的是,只有实施科学的管理,面向对象软件的开发才可获得成功。经理们应逐步引入面向对象技术,并认真地管理企业需求(如用软件工程的方法)和结果(如可重用的结果)。

① 本书引用了大量比喻。随着对本书内容的体会,分析软件对象类与电子集成电路之间类比的缺陷。

② 根据你对面向对象的了解,你属于反对派、进化派、激进派中的哪一种?通过面向对象的开发与传统主流软件开发的对比,调整你的位置。

③ 你认为企业有必要选择一种语言(开发环境)具有第一章描述的面向对象的所有特性吗?换言之,你赞同一些面向对象的激进派对一些企业选择部分面向对象特性而不是全部面向对象特性持讥讽态度吗?

④ 回顾一下2.1节提到的面向对象功名录。是否有遗漏?如果是这样,他或她对面向对象的贡献是什么?

① 这种类比的一个不足是抹杀了用大多数现代面向对象语言连接软件IC的方式。电子IC之间是相互匿名的。IC之间通过插座连接,IC并不“知道”其连接的其他IC;只“知道”自己的针而不知道其他IC的针。

对于类和对象却不然。类通过显名与其他类联系。例如,类A继承类B,则类A中含有一行代码:inherits from ClassB。对象通过命名另一个对象的操作来发送消息。这点与用焊接内部针的方式将一个IC与另一个IC连接起来有些类似。 

Peter Wegner在其论文的6.1.3节中,进一步讨论了这一概念,参见[Wegner,1990] 。在本书的后部分将更详细地讨论类之间的联系。

② 如果你是面向对象的反对派,则回头看一下第一章介绍的面向对象的主要抽象概念。仔细对照每一个概念是否与一些以前的面向对象出版物中的概念类似。如果你是面向对象的激进派,则应阅读诸如Yourdon和Constantine著的结构设计书[Yourdon and Constanine,1979]。你可以举出该书中的主要概念(如耦合和内聚)与大胆、全新的面向对象世界无关的例子吗?

③ 我认为,争论“我是否比你更面向对象”过于刻板,工程并不关心无谓的争论。工程关心的是面向对象所带来的何种益处对于企业达到目标是最重要的?部分面向对象的环境具有面向对象的一些软件工程上的优势,而缺乏另一些优势。因此,企业应十分了解自身需求并为满足需求选择环境而做出好的权衡。

④ 为确定加入我的面向对象功名录的其他人选,你可能需要仔细考察你公司所使用的方法学、语言或工具的作者。一种可能是需要研究Sun Microsystems公司的Java语言的早期历史。

[1] 译者注:本书的参考文献以[作者名,年份]的形式在正文中标引(详见书末的“参考文献”)。


相关图书

现代软件工程:如何高效构建软件
现代软件工程:如何高效构建软件
UML基础、案例与应用(第3版)(修订版)
UML基础、案例与应用(第3版)(修订版)
持续交付:发布可靠软件的系统方法(英文版)
持续交付:发布可靠软件的系统方法(英文版)
团队软件过程(修订版)
团队软件过程(修订版)
掌握需求过程(第3版)
掌握需求过程(第3版)
术以载道——软件过程改进实践指南
术以载道——软件过程改进实践指南

相关文章

相关课程