第2章 对象建模

h2 { margin-top: 0.46cm; margin-bottom: 0.46cm; line-height: 173%; page-break-inside: avoid; }h2.western { font-family: "Arial",sans-serif; font-size: 16pt; }h2.cjk { font-family: "黑体","SimHei"; font-size: 16pt; font-style: normal; }h2.ctl { font-family: "DejaVu Sans"; font-size: 16pt; }p { margin-bottom: 0.21cm; }

 

 

2 章 对象建模

对于软件是什么以及程序如何工作,面向对象编程语言和设计语言有一个共同的理解。对象模型 是UML 和面向对象编程语言共享的公共计算模型。尽管编程语言和设计语言是在不同的抽象级别来表示程序的,但是我们理解这两种语言的基础都是对象模型所提供的对运行程序的抽象描述。

本章在一个简单应用的背景下,引出并描述对象模型的本质特征。通过例子介绍UML 提供的这些概念的表示法,说明如何实现这些概念,解释设计语言和编程语言之间的密切联系。

2.1 对象模型

对象模型不是一个特定的UML 模型,而是一种考虑程序结构的一般方式。它由构成面向对象设计和编程活动的基础的概念框架组成。如同它的名字使人想起的那样,对象模型的基本性质是,计算是发生在对象 之内和对象 之间的。

各个对象负责维护系统数据的一部分,并负责实现系统整体功能的某些方面。当程序运行时,对象典型地由内存区域表示,该内存区域中就包含着该对象存储的数据。对象还支持方法或函数,以访问和更新对象所包含的数据。因此,对象结合了计算机程序的两个根本方面,即数据和处理, 在其他软件设计方法中这二者是分离的

然而,程序要比一组孤立的对象集合描述得更多。各个对象中存储的数据之间的关系必须要记录,而且程序的整体行为只有从多个不同对象的交互中才能显现出来。通过允许将对象连接到一起可以支持这些需求。典型地,这是通过使一个对象能够拥有对另一个对象的引用,或者更具体地讲,是知道其他对象的位置来实现的。

因而,对象模型将一个运行的程序视作是一个对象网络,或图(graph )。对象构成该图中的结点,连接对象的弧称为链接 (link )。每个对象包含程序数据的一个小子集,对象网络的结构则表示这些数据之间的关系。对象可以在运行时创建和销毁,对象之间的链接也可以改变。因此,对象网络的结构,或拓扑结构,是高度动态的,会随着程序的运行而改变。

对象之间的链接还可以作为对象交互的通信路径,使得对象能够通过互相发送消息( messages )进行交互。消息与函数调用类似:消息典型地是请求接收对象执行它的一个方法,而且可以附有用参数表示的消息的数据。通常,对象对一个消息的响应是向其它对象发送消息,这样,计算通过网络而展开,这个网络将包含响应一个初始消息而涉及到的多个对象。

描述一个运行程序的对象的图结构并跟踪各个消息的结果是有可能的:适合做这件事的工具是调试程序。但是,通过定义各个对象来编写程序通常是不可行的,而是要给出同一类的对象的类( class )的结构描述来定义对象能够持有的数据和方法的执行结果。因此,面向对象程序的源代码不是直接描述对象的图,而是描述组成这个图的这些对象的特性。

2.1.1 对象模型在设计中的作用

在设计中,对象模型的重要性在于它为UML 的设计表示法提供了语义基础。UML 中许多特征的含义可以通过将它们解释为对相互连接的、互通消息的对象的集合的说明来理解。

可以绘制UML 图(diagrams) 来表示对象特定运行时的配置。然而,更加常见的是绘制和源代码作用相同的图,从一般结构上来定义运行时会发生什么。这些图分成两大类。静态 图描述对象之间可能存在的关系 的种类,以及作为结果的对象网络 可以具有的可能的拓扑结构。动态 图描述可以在对象之间传递的消息 以及该消息对接收消息的对象的影响

对象模型的这种双重作用 使得将UML 设计表示法与实际的程序相关起来非常容易,这也解释了为什么UML 是适合于设计和文档化面向对象程序的语言。本章的其余部分将通过用一些基本的UML 表示法文档化一个简单程序的例子予以说明。

2.1.2 一个库存控制的例子

在制造业环境中,某些类别的复杂产品是由组成零件装配而成,常见的需求是记录所拥有的零件的库存以及这些零件的使用方式。本章我们将开发一个简单的程序来模拟不同种类的零件和它们的特性,以及用这些零件构造复杂组件的方式,通过这个例子来阐明对象模型。

这个程序必须管理描述系统所知的不同零件的信息。除了维护所使用的零件的不同类型信息,我们还设想对系统来说记录各个实际零件的信息也很重要,可能是为了质量保证和跟踪。

对这个例子来说,我们假定对每个零件我们感兴趣的是下列三项信息:

1. 零件的目录查找号(整数)

2. 零件的名字(字符串)

3. 单个零件的价格(浮点数值)

零件可以被装配成更复杂的结构,称为组件。一个组件可以包含多个零件,而且可以具有层次结构,也就是说,一个组件可以由许多子组件构成,每个子组件又由零件或可能它自己的更深一层的子组件构成。

维护零件、组件及它们的结构信息的程序应该能够用于许多目的,例如维护目录和库存信息,记录制造的组件的结构,支持对组件的各种操作,例如计算组件中零件的总价格,或者打印组件的所有零件的清单。本章我们将考虑一个简单的应用,即通过累加组件中包含的所有零件的价格,查出一个组件中的材料价格的查询功能。

 

2.2 类和对象

面向对象系统中的数据和功能分布在系统运行时存在的对象之中 。每个单独的对象维护部分系统数据并提供一组允许系统中的其他对象对这些数据进行某些操作的方法。面向对象设计的难题之一就是如何将系统的数据划分到一组对象中,这些对象将成功地交互以支持所要求的总体功能。

识别对象经常应用的一个经验规则是:用模型中的对象表示来自应用领域的现实世界中的对象。库存控制系统的主要任务之一是记住厂商库存中的所有物理零件。因此,很自然的起点就是考虑将这些零件中的每一个都表示为系统中的一个对象。

一般会有许多零件对象,每个对象描述一个不同的零件,因而保存了不同的数据,但是每个对象都具有相同的结构。表示同一种实体的一组对象的共有结构由类 来描述,该类的每个对象称为是该类的一个实例 (instance )。那么,作为库存管理系统设计的第一步,我们可以考虑定义一个“零件”类。

一旦确定了候选类,我们可以考虑该类的实例中应该放些什么数据。在零件类的情况中,一个自然的想法是每个对象应该保存系统必须保存的关于该零件的信息:它的名字、编号以及价格。这反映在下面的实现中。

 

UML 类的概念与C++ 和Java 这样的程序设计语言中的概念非常类似。一个UML 类定义了许多特征 (feature ):细分为属性 (attribute )和操作 (operation ),属性定义类的实例存储的数据,操作定义类的实例的行为。一般地说,属性相当于Java 类中的域,操作则相当于方法。

在UML 中,类由一个分为三栏的矩形图标表示,分别包含该类的名字、属性和操作。‘Part’ 类的UML 表示如图2.1

 

图 2.1 “ 零件”类的UML 表示

类图标最上面的部分包含类的名字,第二部分包含类的属性,第三部分是类的操作。在操作的特征标记(signature )中可以使用程序语言中的类型,用冒号把属性名、参数名或操作名与类型隔开。UML 也表示了类的各种特征的访问级别,用减号表示‘private (私有)’,加号表示‘public (公有)’。构造函数下面有下划线,以便和类的一般的实例方法相区分。

第8 章将给出UML 语法的详尽细节,但是在这里值得注意的是,图2.1 所示的许多细节都是可选的。其中,包含类名字的一栏是必需的:在特定的图中,如果没有要求,可以省略其他信息。

对象创建

类是在编译时定义的,而对象是在运行时作为类的实例创建的。执行下列语句的结果是创建一个新对象。它包括两个步骤:首先为对象分配一块内存区域,然后适当地初始化。一旦创建了新对象,将在变量myScrew 中保存它的一个引用。

 

UML 定义了描述单个对象及其所保存的数据的图形化表示法。上面一行代码创建的对象可以用UML 表示,如图2.2 所示。

 

2.2 一个Part 对象

对象由分为两栏的矩形表示。上面一栏包含对象的名字及其类的名字,都加有下划线。对象不一定要命名,但如果有和对象相关的变量,用变量的名字为对象命名有时会有用。当要知道对象的类时,对象的类名总要说明。通常的风格习惯是类的名字以大写字母开头,而对象的名字以小写字母开头。

数据作为属性的值保存在对象中。对象图标中下面的一栏包含有对象属性的名字和当前值。这一栏是可选的,如果在一个图中不必要显示对象的值时可以省略。

 

2.3 对象的特性

对象通常的特性描述表明对象是具有状态、行为和本体 的某个东西。下面将更详细地解释这个概念以及相关的封装的概念,另外还讲述了实现这些特征的类定义的各种术语。

2.3.1 状态

对象的第一个重要特征是它们充当数据的容器。在图2.2 中,对象的这个特性通过在表示对象的图标中包含数据来描绘。在纯面向对象系统中,系统维护的所有数据都保存在对象中:不存在其他模型中的全局数据或中央数据存储库的概念

包含在对象属性中的数据值通常称为对象的状态( state ) 。例如,图2.2 中所示的三个属性值构成了对象“myScrew” 的状态。由于这些数据值会随着系统的变化而改变,结果当然是对象的状态也可以改变。在面向对象的程序设计语言中,对象的状态由对象的类中所定义的域指定,而在UML 中由类的属性指定,例如图2.2 所示的三个属性值。

2.3.2 行为

每个对象除了保存数据之外还提供了一个由若干操作组成的接口。通常其中的一些操作将提供访问和更新对象中所保存的数据的功能,但其他操作将更通用,并实现系统全局功能的某些方面。

在编程语言中,对象的操作在它的类中作为一组方法来定义。对象定义的一组方法定义了该对象的接口 (interface )。例如,2.2 节中定义的零件类的接口包括一个构造函数和一些访问函数,以返回对象的域中所保存的数据。

在UML 中,操作不同于属性,操作没有出现在对象图标中。这是因为对象提供的完全是它的类所定义的操作。由于一个类可以有许多实例,每个都提供同样的操作,因此显示每个对象的操作会很多余。在这方面,对象的行为不同于它的属性,因为,通常同一个类的不同实例将保存不同的数据,因而具有不同的状态。

2.3.3 本体

对象定义的第三个方面是每个对象和其他所有对象都是可区别的,即使两个对象保存完全相同的数据,并在接口中提供完全相同的操作集合时也是如此。例如,下面几行代码创建两个状态相同的对象,但它们还是不同的对象。

 

对象模型假定为每个对象提供了一个唯一的本体 (identity ),作为区别于其他对象的标志。对象的本体是对象模型固有的一部分,不同于对象中存储的任何其他数据项

设计人员不需要定义一个特殊的数据来区分一个类的各个实例。但是,有时应用领域会包含对每个对象都不相同的真实的数据项,例如各种识别号码,这些数据项通常作为属性建模。然而,在没有这样的数据项的情况下,也没有必要只是为了区分对象而引入一个这样的数据项。

在面向对象的程序设计语言中,对象的本体一般由它在内存中的地址表示。由于不可能在同一个位置保存两个对象,所有对象都保证具有唯一的地址,因而任意两个对象的本体都是不同的。

2.3.4 对象名字

UML 允许为对象命名,对象名字不同于其所属类的名字。这些名字是模型内部的,允许在一个模型中的其他地方引用这个对象。这些名字不对应对象中存储的任何数据项,不过,可以将名字看作是为对象的本体提供了一个方便的别名。

对象的名字不同于刚好保存该对象的引用的变量名。在举例说明对象时,如在图2.2 中,使用保存对象引用的变量名字作为对象的名字通常比较方便。但是,可以有多个变量保存对同一个对象的引用,并且一个变量在不同的时候可以引用不同的对象,所以,这种惯例如果随意应用,可能很容易引起混淆。

2.3.5 封装

对象一般理解为封装 (encapsulate )了它们的数据。这意味着对象内保存的数据只能够通过属于该对象的操作来操纵,因而一个对象的操作不能直接访问在不同的对象中存储的数据。

在许多面向对象语言中,通过语言的访问控制机制来提供一种封装形式。例如,在2.2 节中的零件类的数据成员被声明为‘private’ ,意思是它们只能被同类对象的操作访问。注意,这种基于类的封装形式比基于对象形式的封装要弱,后者不允许对象访问任何其他对象的数据,即使是属于同一个类的对象的数据。

 

2.4 避免数据重复

尽管2.2 节中采用的对零件建模的简单直接的方法很有吸引力,但是在真正的系统中不可能令人满意。它的主要缺点是描述给定类型零件的数据是重复 的:数据保存在零件对象中,而且如果有两个或多个同类型的零件,该数据会在每个相关对象中重复。这样,至少存在着三个重大问题。

首先,它含有高度的冗余。系统可能记录一个特定类型的数千个零件,它们共有相同的查找号、描述和价格。如果在每个零件中都保存这些数据,将不必要地耗尽相当大量的存储。

其次,价格数据的重复尤其可能会导致维护问题。如果一个零件的价格改变了,每个受到影响的对象中的价格属性都需要更新。这样不仅低效,而且也难以保证在这种情况下每个相关对象都会被更新,并且不会错误地修改了表示不同种类零件的对象。

第三,需要永久保存零件的目录信息。但是,在某些情况下,一个特定类型的零件对象可能不存在,例如,假如零件还没有制造,就是如此。倘若这样,就没有地方保存目录信息。然而,不可能容许只有在相关零件存在时才能保存目录信息。

设计这个应用的一个更好的方法应该是将描述给定类型零件的共享信息保存在另外的对象中。这些“描述符”对象并不表示单独的零件,而是表示与描述一类零件的目录条目( catalogue entry ) 相关的信息。图2.3 非正式地举例说明了这种情况。

这个新设计要求,对于系统所知的每种不同类型的零件,都应该存在一个单一的目录条目对象,以保存该类型零件的名字(name )、查找号(number )和价格(cost )。零件对象不再保存任何数据。为了查找一个零件,必需访问描述该零件的目录条目对象。

这种方法解决了上列问题。数据只存储在一处,因而没有冗余。修改给定类型零件的数据很直接:如果一种零件的价格改变了,只需要更新一个属性,即相应的目录条目对象中的价格属性。最后,没有理由说为什么一个目录条目对象,即使没有零件对象与之相关联时,不能存在,因而解决了在创建任何零件之前如何能够保存零件信息的问题。

2.5 链接

现在,库存控制程序的设计包括了两个不同的类的对象。目录条目对象保存适用于给定类型的所有零件的信息,而每个零件对象则表示一个单个的实际零件。

 

2.3 目录条目描述的零件

然而,这些类之间存在一个重要的关系:为了获得对一个零件的完整描述,必需查看的不仅是零件对象,还要查看相关的描述零件的目录条目对象。

实际上,这意味着对每个零件对象,系统必须记录哪个目录条目对象在描述它。实现这种关系一般方法是,如下所示,每个对象包含一个到相关目录条目的引用。目录条目类现在包含着描述零件的数据,零件是用一个目录条目进行初始化并且保存对该目录条目的一个引用。

 

 

在创建一个零件时,必须提供适当的目录条目对象的一个引用。这样做的根本原因是:创建一个未知的或未指定类型的零件是没有意义的。下面的代码显示了如何用这些类创建零件对象。首先,必须创建一个目录条目对象,而后可以用它初始化所需的任意多个零件对象。

 

 

如同在2.2 节中解释的那样,一个类的域在UML 中通常是作为类的属性来建模。然而,如果一个域包含着对另一个对象的引用,例如上面的Part 类中的entry 域,这种方法就不合适。属性定义的是保存在对象内部的数据,但目录条目对象并不是保存在零件内部,而是单独的对象,能够独立于任何零件对象存在,并能够同时被多个零件对象引用。

在UML 中,一个对象保存另一个对象的引用的事实通过在这两个对象之间画一个链接 (link )来表示。链接表示为一个箭头,从保存引用的对象指向被引用的对象,而且在链接的箭头上可以标示保存引用的域的名字。因此,上面的代码所创建的对象可以用UML 建模,如图2.4 所示。

 

2.4 对象之间的链接

链接上的箭头表示只能在一个方向上遍历,或导航 。这意味着零件对象知道它所链接的目录条目对象的位置,并有权访问目录条目对象的公有接口。这并不隐含着目录条目对象对引用它的零件对象具有任何访问权限,它甚至不知道该零件对象。在另一个方向的访问只能通过在目录条目中保存对零件的引用来提供,从而使链接在两个方向都是可导航的。

 

对象图

对象图 (object diagram )是表示对象和对象之间的链接的图形表示。图2.4 是一个很简单的对象图的例子。对象图是以可见的形式表示2.1 节所讨论的对象的图结构的一种方式:它们给出了系统中的数据在给定时刻的一个“快照”。

在链接着的对象的结构中,信息是用两种不同的方法记录的。一些数据明确地作为属性保存在对象中,而另一些信息纯粹是在结构上依靠链接保存的。例如,零件属于给定类型的事实是通过零件对象和相应的目录条目之间的链接表示的:在系统中没有明确地记录零件的类型的数据项。

 

2.6 关联

正如同用类定义一组相似对象的共同结构一样,这些对象之间的链接的共同特性也可以通过相应的类之间的关系定义。在UML 中,类之间的数据关系称为关联 (association )。因而,链接是关联的实例,就如同对象是类的实例一样。

在库存控制的例子中,图2.4 中的链接必须是零件类和目录条目类之间的一个关联的实例。在UML 中用一条连接相关的类的线来表示关联;与上面的链接相对应的关联如图2.5 所示。注意,为了清晰起见,图中没有显示目录条目类的所有已知信息。

 

2.5 两个类之间的关联

这个关联用UML 建立了一个模型,它定义了在Part 类中的entry 域。因为entry 域保存了一个引用,所以关联表示为只在一个方向上可导航。类似于相应的链接,在关联的一端标记着域的名字。以这种方式放置在关联端点的标记称为角色名 (role name ):它的位置反映了“entry” 这个名字在零件类中用来引用所链接的目录条目对象的事实。

在关联的端点还标明了一个重数约束 (multiplicity constraint ),这个例子中是数字“1” 。重数约束表明一个给定的对象在任一时间能够和多少个实例链接。在这个例子中,约束是每个零件对象必须链接到恰好一个目录条目对象。但是,这个图没有规定多少个零件对象可以链接到一个目录条目对象。

重数约束给出了关于模型的很有价值的信息,但同样重要的是要知道它没有表达出来的东西。例如,对库存控制系统的一个合理的特性需求是:一个零件应该总是链接到同一个目录条目对象,因为建模的实际零件不能从一种类型变成另一类型。然而,这里显示的重数约束并没有强制这点:在任何给定时间应该只有一个链接的目录条目这个约束,并未含有在任何时候都是相同的目录条目的意思。

值得注意的是,上面给出的Java 的Part 类实际上没有实现图2.5 所示的重数。因为null 在Java 中是引用域的合法值,所以,如果将null 作为零件类的构造函数的参数,那么有未链接到任何目录条目的零件对象是可能的。这与一个零件应该总是链接到恰好一个目录条目对象的重数约束相矛盾。Part 类的一种更健壮的实现可以是在运行时对此进行检查,在试图用一个null 引用初始化时抛出一个异常。

类图

正如对象图显示对象和链接的集合一样,类图 (class diagrams )包含了类和关联。图2.5 是一个简单的类图的例子。

尽管对象图显示系统的对象的图的许多特定的状态,而类图则以一种更一般的方式指定了系统的任何合法状态都必须满足的特性。例如,如果图2.5 表示在任何给定时间,库存控制系统的对象图能够包含“Part” 和“CatalogueEntry” 类的实例,并且每个零件对象必须链接到恰好一个目录条目。那么,假如程序进入了一种状态,譬如说存在连接两个目录条目的链接,或者零件没有链接到目录条目,就会出现一个错误,而程序则处于一种非法状态。按照术语固有的外延,如果对象图满足类图中定义的各种约束,那么称对象图是类图的实例。

2.7 消息传递

本章早先的例子已经说明,在面向对象的程序中,数据是如何在系统中跨对象分布的。尽管一些数据作为属性值明确地保存,但是对象之间的链接也含有信息,它描述了对象之间持有的关系。

信息的分布意味着,为了完成任何有意义的功能,一般地,许多对象需要进行交互。例如,假设我们想要为零件类增加一个方法,使得我们能够检索单独一个零件的价格。但是,表示零件价格的数据值并没有保存在零件对象中,而是保存在零件引用的目录条目对象中。这意味着为了检索该数据,这个新方法必须调用目录条目类中的getCost 方法,如下面的实现所示。

 

现在,如果客户持有一个零件的引用并需要找到它的价格,可以如下简单地调用cost 方法。

 

UML 将方法调用表示为从一个对象发送到另一对象的消息 。当一个对象调用另一对象的方法时,可以看作是请求被调用的对象执行某些处理,这个请求作为一个消息建模。图2.6 显示了对应于上面的代码中调用s1.cost() 的消息。

 

2.6 发送一个消息

图2.6 中的client 对象只有对象名字而没有类名字。“cost” 可以由多个不同类的对象发送给一个零件,而消息发送者的类与理解消息以及零件对象对消息的响应无关。因此,在像图2.6 这样说明特定的交互的对象图中省略客户类比较方便。

客户代码持有对零件对象的一个引用,保存在变量s1 中,如前所述,这在图2.6 中表示为一个链接。这个引用还使得客户能够调用链接的对象的方法,然而,这意味着在UML 中,对象之间的链接也表示了消息的通信信道。在对象图中,消息用链接旁边带标记的箭头表示。在图2.6 中,显示了一个客户对象向零件对象发送消息请求得到零件的价格。消息本身则用常见的“函数调用”符号书写。

对象在接收到一个消息时,通常会以某种方式响应。在图2.6 中,预期的响应是零件对象向客户对象返回自己的价格。但是,为了查找价格,零件对象必须调用它所链接的目录条目对象中的getCost 方法。这可以用第二个消息表示,如图2.7 所示。

 

2.7 查找零件的价格

图2.7 还举例说明了表示消息返回值的UML 表示法。返回值写在消息名字的前面,并用赋值符号“:=” 分隔开。在不显示返回值时,或者没有返回值时,如图2.6 所示,可以简单地省略这个符号。

以上所示消息的语义是普通程序函数调用的语义。当一个对象给另一个对象发送消息时,程序中的控制流从发送者传递给接收消息的对象,发送消息的对象一直等到控制返回时才继续自己的处理。

2.8 多态性

除了维护单个零件的详细资料之外,库存控制程序还必须能够记录这些零件如何装配成组件。图2.8 所示的是一个包含一个strut (支杆)和两个screw (螺旋)的简单组件。注意,在这个图中省略了目录条目类中的无关属性。

 

2.8 一个简单组件

在图2.8 中,有关组件(Assembly )中包含哪些零件(Part )的信息是通过连接组件和零件对象的链接表示的。这些链接不是用角色名标示,而是用关联名 标示。关联名描述链接的对象之间具有的关系。关联名通常是经过选择的,像这里一样,使得可以从关联名以及所链接的类名构造出描述这种关系的语句。在这个例子中,合适的语句可以是“一个组件包含(contains )零件”。

组件类的实现必须提供一种方法,能保存对不定个数零件的引用。一种简单的支持方法是在类中包含一个数据结构,该数据结构能够保存该组件对所有零件的引用,如下所示。

 

图2.8 中的链接实例所属的关联在图2.9 中表示了出来。如链接那样,关联上标明了关联名,写在关联的中间。关联端点的“*” 符号是一个重数注文,含意是“0 个或多个”。在这个图中,它指定一个组件可以链接或包含零个或多个零件。

 

2.9 组件和零件之间的关联

图2.9 中的图在关联端点标示的角色名‘parts’ ,对应于Assembly 类中用于保存引用的域,从而文档化了上面代码中关联的实现。通常,为了让图的含意更清晰,可以使用所需要的任何名字和角色名的结合来标明关联和链接。

然而,将一个组件简单地作为一组零件建模是不够的。组件可以有层次结构,零件能够装配为子组件,子组件可以和其他子组件与零件装配在一起,形成更高层次的组件,可以达到任何需要的复杂度。图2.10 显示了一个简单的例子,它在图2.8 所示的结构中引入了一个子组件。

 

2.10 层次组件

为了实现层次结构,组件必须能够包含零件和其他组件。这意味着和图2.8 不同,2.8 中标示有‘Contains’ 的链接全都是把一个组件对象连接到一个零件对象,而在图2.10 中,标示着‘Contains’ 的链接还可以把一个组件对象连接到另一个组件。

如同许多程序设计语言一样,UML 也是强类型的语言。链接是关联的实例,因而由链接连接的对象必须是相应的关联端点的类的实例。在图2.8 中,这个要求是满足的:如图2.9 规定的那样,每个标示有‘Contains’ 的链接连接该组件类的一个实例到该零件类的一个实例。

但是,图2.10 中违反了这个条件,因为‘Contains’ 链接将顶层组件实例不是连接到一个零件对象,而是连接到了一个组件类的对象。如果我们想要建立层次组件的模型,在这些链接的“被包含的”一端,必须不能像图2.9 所指定的那样约束为只是‘Part’ 类,而是或‘Part’ 类,或‘Assembly’ 类。这是一个多态性 (polymorphism )的例子:多态性的意思是“许多形态”,暗示在某些情况下,通过同一类型的链接需要连接多个类的对象。

2.8.1 多态性的实现

UML 是强类型的,所以不允许链接连接任意类的对象。对于如图2.10 所示的多态链接,必需指定能够参与链接的类的范围,通常的实现方法是定义一个更一般的类,并说明我们希望链接的特殊类是这个一般类的特化。

在库存控制程序中,我们想要建立模型是,在此模型中,组件能由构件 (component )组成,而每个构件可以是一个子组件或是一个零件。这样就能够规定‘Contains’ 链接把组件对象连接到构件对象,而按照定义,构件对象或是零件对象,或是表示子组件的其他组件对象。

在面向对象语言中,是使用继承 (inheritance )机制来实现多态性。可以定义一个构件类,而将零件类和组件类定义为构件类的子类,如下所示。

 

和Assembly 类较早的实现不同,那里定义了将一个零件加入到组件中的方法,这里相应的方法是将一个构件加入到组件中。然而,在运行时,实际创建并加入到组件的对象将是零件类和组件类的实例,而不是构件类自身的实例。Java 中继承语义的含义是,可以在任何指定超类引用的地方使用子类的引用。在这里,这意味着零件和组件的引用都可以作为add 函数的参数来传递,因为这些类都是构件类的子类,而函数的参数指定为构件类。

组件类的实现可能甚至比这更具多态性,因为其中使用了Java 库的“Vector” 类来存储对构件的引用,而Vector 类可以保存任何类型对象的引用。对构件类的限制是由“add” 方法的参数类型强加的,它提供了客户向组件中加入构件的唯一方法。

 

2.8.2 UML 中的多态性

这个例子中,多态性的实现由两种不同机制相互作用而产生。第一,定义组件类使得它能够保存对多个构件对象的引用,第二,用继承定义子类,表示存在的不同类型的构件。于是程序设计语言规则就导致一个组件能够保存对不同类型构件的混合引用。

Java 的‘extends’ 关系在UML 中用类之间的特化 (specialization )关系表示:如果类E 是通过扩展类C 而定义的,那么就说E 是C 的特化。如果从超类比子类有更大的范围的关系的角度看,这种关系也被称为泛化 (generalization ):等价的描述可以说C 是E 的泛化。泛化,或者特化,在UML 类图中用一个将该关系中的子类连接到超类的箭头描绘。这些关系与关联在直观上的区别是箭头的形状。图2.11 显示了库存控制例子中的类之间的特化关系。

 

2.11 构件之间的泛化关系

和关联不同,泛化没有在对象图中出现的“实例”。尽管关联描述的是对象能够链接到一起的方式,泛化描述的则是一个类的对象能被另一类的对象替换 的情形。正因为这样,重数的概念不适用于泛化,并且一般也不标注泛化关系。

最后,考虑到图2.11 中的泛化,我们可以重新定义图2.9 中的关联。结果情况如图2.12 所示,该图还文档化了上面给出的Component 、Part 和Assembly 类的实现。

 

2.12 允许层次组件的模型

图2.12 表明组件能够包含零个或多个构件(关联),每个构件可以是一个零件或是一个组件(特化)。在后一种情况下,我们有了一个组件包含在另一个组件之中的情形,因此这个类图允许构造如图2.10 所示的层次对象结构。

 

2.8.3 抽象类

与零件和组件类不同,我们决不会期望创建构件类的实例。零件和组件对应于应用领域中的真实对象或结构,但是,构件类是一个概念的表示,即零件和组件可以认为是更一般的构件概念的特例。在模型中引入构件类的原因不是为了能够创建构件对象,而是为了指定零件和组件在某些情况下是可互换的。

像‘Component’ 这样的类,其引入主要是为了指定模型中其他类之间的关系,而不是为了支持新类型对象的创建,这样的类称为抽象类 (abstract classes )。如上面举例说明的,Java 允许将类声明为抽象的,而在UML 中,可以通过将类名字写成斜体表示,如图2.11 和2.12 所示。

 

2.9 动态绑定

如果传递给组件对象一个消息请求得到它的价格,那么组件对象满足这个请求的方法是向自己的构件请求得到它们的价格,然后返回所得到的价格的总和。自身也是组件的构件对象将向自己的构件发送类似的‘cost’ 消息;但是,若构件是简单零件时,就向它所链接的目录条目对象发送 ‘getCost’ 消息,如图2.7 所示。

 

2.13 层次中的消息传递

如果组件对象处于图2.10 中的层次的顶端时,向它发送一个‘cost’ 消息后将会产生的所有消息在图2.13 中给出。注意在面向对象程序中,单个请求非常容易引起系统中对象之间的一个复杂的交互网。

在这个交互中,组件对象通过向自己的所有构件发送同样的消息来计算出价格,即‘cost’ 。发送消息的对象不知道特定的构件是一个零件还是一个组件,实际上也不需要知道。它只是简单地发送消息,并依赖接收对象以一种恰当的方式解释这个消息。

当接收到一个‘cost’ 消息时,实际进行的处理将依赖于消息是发送给了组件对象还是零件对象。这种行为称为动态绑定 (dynamic binding ),或晚绑定 (late binding ):实质上,是消息的接收者,而不是发送者,来决定执行什么代码作为所发送消息的结果。在多态的情况下,消息接收者的类型可能直到运行时才可以知道,因而,响应消息所执行的代码只能在运行时选择。

在Java 中,获得这种行为很简单,通过在构件类中声明cost 函数,然后在零件类和组件类中重定义cost 函数,为各个类提供需要的功能,如下面所示的来自相关类的摘录。其他语言提供晚绑定有不同的方法:例如在C++ 中,就必须使用虚函数机制。

 

2.10 对象模型的适用性

本章已经讨论了对象模型的基本特征,并着重讨论了对象模型的概念与面向对象编程语言的概念之间的联系。但是,对象模型在软件开发生命周期从需求分析开始的所有阶段都要使用,因而研究对象模型对这些活动的适用性也很重要。

通常说,面向对象的观点是受到了我们平常看待世界的方式的启示。事实上我们的确是把世界作为是由对象组成来感知和认识的,这些对象具有各种特性、互相交互、以各种独特的方式行动,而对象模型则认为是这种一般意义上观点的反映。因此,有时甚至进一步声称,对象建模非常容易,软件系统中需要的对象可以简单地通过研究所建模的现实世界领域而发现。

在某些系统中,情况的确是这样,其中一些被建模的现实世界对象和一些软件对象之间存在着相当直接的对应,但是这不能类推而作为建立面向对象系统的一种非常有用的指导。设计软件的首要目标是产生满足用户需求的、易于维护的、易于修改和复用并且能够有效利用资源的系统。

但是,一般而言,认为简单地复制所感知的现实世界中的对象就能产生具有这些良好特性的软件是不合理的。例如,将零件直接表示为2.2 节的零件对象,2.4 节表明,这样会导致重大的效率问题和可维护性问题,至于它能否满足系统的功能需求,甚至也存在疑问。由于过分强调现实世界对象的特性而导致的拙劣设计的另一个典型例子将在14.5 节中给出。

此外,在对象模型中用于对象之间通信的消息传递机制,似乎也没有准确地表现现实世界中许多事件发生的方式。例如,考虑这样一种情形,两个人没有注意走到某个地方,互相偶然相遇。将这种情形想象为是其中一人向另一人发送了一个“相遇”消息,是与直观相违背的。两个人是平等的而且无意识地参与了一个事件的发生,这样似乎更恰当一些。由于这样的原因,一些面向对象的分析方法建议,根据对象和事件建立现实世界的模型,而只在设计过程的后期才引入相关的消息。

无疑地,存在一些案例,其中现实世界的对象,特别是代理,可以认为是给其他对象发送消息。然而,对象模型最有意义的长处不是来自它对现实世界建模的适宜性,而是来自具有面向对象结构的程序和软件系统更可能拥有许多人们想要的特性的事实,例如易于理解和维护。

理解面向对象最有益的是作为一种特别的方法,解决如何将软件系统中的数据和处理相关在一起。所有的软件系统都必须处理一组给定的数据,并提供操纵和处理这些数据的能力。在传统的过程式系统中,数据和处理数据的函数是分离的。系统的数据认为是存储在一个地方,而应用需要的功能则通过一些操作提供,这些操作能自由访问任何部分的数据,同时在本质上保持与数据的分离。每个操作都有从中央存储库中选取自己感兴趣的一部分数据的责任。

可以立即得出对这种结构的一种看法是,大多数操作将仅仅使用系统整个数据的一小部分,并且大多的数据块将只是由少数操作访问。面向对象方法试图做的是将数据存储库划分为许多独立的数据块,并将数据块和直接操纵该数据块的操作集成在一起。

这种方法与更传统的结构相比,能够提供许多重要的技术优势。然而,用容易理解的话来说,面向对象设计的益处似乎不是来自对象模型特别忠实于现实世界的结构,而是来自这些操作与它们所影响的数据都局部化在一起,而不是作为庞大而复杂的全局结构的一部分。

2.11 小结

· 面向对象建模语言是建立在抽象的对象模型 的基础之上的,对象模型将运行的系统看作是一个交互的对象的动态网络。该模型还提供了对面向对象程序运行时特性的一个抽象解释。

· 对象包含数据和一组操纵该数据的操作。每个对象和其他对象都是可区分的,不论其保存的数据或提供的操作相同与否。对象的这些特征称为对象的状态 、行为 和本体 。

· 类 描述了一组共享相同结构和特征的对象,这些对象称为是这个类的实例 。

· 对象一般阻止外部对象访问自己的数据,称为封装 。

· 对象图 显示运行时的一组对象以及对象之间的链接。对象可以被命名,并且可以显示它们的属性值。

· 对象通过发送消息 和其他对象合作。当对象收到一个消息时,它执行自己的一个操作。向不同的对象发送相同的消息可以引起执行不同的操作。

· 对象之间以消息形式进行的交互,包括参数和返回值,可以在对象图中表示。

· 类图 提供了对一组对象图中所显示的信息的一个抽象总结。它们显示与一般在系统源代码中找到的相同的信息。

· 在面向对象建模中经常使用的一种经验规则是以现实世界中发现的对象作为设计的基础。但是,以这种方式得到的设计的适宜性需要谨慎地评价。

 

2.12 习题

2.1 画出一个完整的类图,描述本章讲述的库存控制程序的最后的情况,包括在节选的代码中定义的所有属性和操作。

2.2 假设执行了下面一段代码:

 

 

(a ) 画图说明已经创建的对象、它们的数据成员以及它们之间的链接。

  1. 下面的代码创建一个组件对象并向其加入了上面创建的一些对象:

 

画图说明这段代码执行之后组件a 中包含的对象以及它们之间的链接。

  1. 下面代码的执行可以说明向组件a 发送了一个cost() 消息。

 

将执行这个函数期间会在对象之间发送的消息加入到你的图中。

 

2.3 图Ex2.3 中的对象图描绘了库存控制系统的不可能状态,根据图2.5 和2.12 中的类图,解释这是为什么。

 

Ex2.3 库存控制程序的非法状态

2.4 用一个图举例说明下列UML 的对象、链接和消息表示法的使用。

(a ) 类Window 的一个对象,不显示属性。

(b ) 类Rectangle 的一个对象,以及属性lengthwidth 。假设矩形类支持一个返回矩形对象的面积的操作。

(c ) Window 对象和Rectangle 对象之间的一个链接,模拟矩形定义了窗口的屏幕坐标的事实。

(d ) Window 对象向Rectangle 对象发送一个消息,请求得到它的面积。

画出一个类图显示具有本习题中提到的特性的Window 和Rectangle 类。

2.5 假定一个环境监测台装有三个传感器,即温度计、雨量计和湿度计。另外,还有一个输出设备,称为打印机,显示这三个传感器的读数。每5 分钟取一次读数,并转录到打印机。这个过程称为“获取检查点”。

(a ) 绘制一个对象图表明这些对象可能的配置,图中包含在每次获取检查点时系统中可能产生的消息。假设检查点由一个从定时器对象发送到监测台的消息启动。

(b ) 你的图中是否清楚地显示了消息发送的次序?如果没有,应该如何表明?

(c ) 画出概括监测台结构的类图。

2.6 一个工作站当前有三个用户登录,帐户分别是A 、B 和C 。这些用户运行了4 个进程,进程的ID 分别是1001 、1002 、1003 和1004 。用户A 正在运行进程1001 和1002 ,B 在运行1003 ,C 在运行1004 。

(a ) 绘制对象图,描述表示工作站、用户和进程的对象,以及表示在工作站上运行的进程和拥有进程的用户的关系的链接。

(b ) 考虑有一个操作,列出当前运行在工作站上的进程的信息。该操作可以报告所有当前进程的信息,或者在用适当的参数调用时,报告单个指定用户的进程的信息。讨论为了实现这个操作,需要在(a ) 部分所显示的对象之间传递什么消息。

2.7 对本章讨论的程序的另一种设计可以是取消零件类和目录条目类,而用不同的类来表示每种不同类型的零件。模型中可能包括例如“螺旋”、“支杆”和“螺栓”等类。每种零件的查找号、描述和价格将作为静态数据成员保存在相关的类中,单个零件只是这些类的实例。

(a ) 这种改变对程序的存储需求会产生什么影响?

(b ) 为这种新设计设计一个组件类。为了保证组件能够包含不同类型的零件,你需要作什么假设?

(c ) 画出这个新设计的类图。

(d ) 随着系统的进化,可能必须加入新类型的零件。解释这将如何完成:一是在本章提出的原始设计中,二是在本习题考虑的另一种设计中。

(e ) 根据这些考虑,你认为这两种设计中哪个更可取,是在什么情况下更可取?

2.8 假设为库存控制系统定义了一个新需求,维护库存的并且当前没有在组件中使用的每种零件的数量。确定这些数据应该保存在哪里,并在适当的对象图上画出消息,显示每当零件加入到组件时数量是如何递减的。

2.9 组件的 “零件剖析”是一个报告,它以某种适当的格式完整地列出了组件中的全部零件。扩充库存管理程序,以支持打印一个组件的零件剖析。如同图2.8 中那样,通过一个表示典型组件的包括有消息的对象图来阐明你的设计。

2.10 在2.5 节中提到,创建零件对象时没有将其链接到适当的目录条目对象是错误的。但是,2.5 节中给出的Part 类的构造函数并没有强制这个约束,因为它没有检查传给它的目录条目引用不是null ,如果是null ,那么将创建一个没有链接到任何目录条目的零件对象。扩充2.5 节定义的构造函数,以切合实际的方式处理这个问题。

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值