UML面向对象设计基础(txt+pdf+epub+mobi电子书下载)


发布时间:2020-05-18 12:30:24

点击下载

作者:[美]MeliirPage-Jones著

出版社:信息技术第一出版分社

格式: AZW3, DOCX, EPUB, MOBI, PDF, TXT

UML面向对象设计基础

UML面向对象设计基础试读:

前言

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

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

每个人在编写代码的同时,也在设计(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-Jones1999年9月华盛顿Bellevuemeilir@waysys.com第一部分引言“面向对象”术语本身是无意义的。“对象”大概是英语中最普通的词了。在字典中查找它的定义如下:

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

换句话说,对象可以是任何事物!“面向”一词也不能说明什么含义,定义为“直接针对”。因此有下面的定义:

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

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

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

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

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

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

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

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

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

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

封装。

信息/实现隐藏。

状态保持。

对象标识。

消息。

类。

继承。

多态性。

一般性。

用一段面向对象程序来解释这些术语背后的含义是最好的办法。通过本章对这段程序的讨论,你会发现面向对象中的一些术语,其含义并不像看起来那么可怕。实际上,从以前的软件经历中你可能已经熟悉了许多面向对象概念,尽管在叫法上可能有差异,如果你关心面向对象术语在编程语言之间的差异,请参阅本书后面的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

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

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

封装(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 以对象操作命名门的围墙城堡1.2 信息/实现隐藏

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

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

信息/实现隐藏(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对象

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

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

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

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

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

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

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

面向对象胜过 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都指向相同的对象且其他对象不可再访问

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

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

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

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

1.5.1 消息结构

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

对象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节讨论多态性、重载及动态关联时,将体会到“先对象后过程”所引起的面向对象结构和传统结构之间的重要差别。因为不同的对象类可以使用相同的操作名称执行不同特定类的行为,或执行含义不同的类似行为。

1.5.2 消息参数

与传统的子程序一样,大多数消息都可以传入或传出参数。例如,如果名为 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.5.3 消息中的对象角色

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

消息的发送者。

消息的目标。

表示另一个对象中的变量(见1.4节)。

表示消息中传入和传出的参数(见1.5.2节)。

一个给定的对象在生存期可以扮演一个或多个角色。在图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++那样随意。

1.5.4 消息的类型

对象可以接收三种类型的消息:报告消息,询问消息及祈使消息。本节对每一种消息进行举例说明。再一次借用机器人程序。在本书的最后第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)

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

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

回想一下在机器人软件中,通过执行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”看作是同义词。请把这一点记在脑子里,现在让我们转到讨论继承的重要概念。1.7 继承

如果你写了一个类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给出了结构图,空心箭头表示继承。

试读结束[说明:试读内容隐藏了图片]

下载完整电子书


相关推荐

最新文章


© 2020 txtepub下载