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