第10章 面向对象技术
面向对象(Object-Oriented, OO)方法是一种非常实用的软件开发方法,它一出现就受到软件技术人员的青睐,现在已经成为计算机科学研究的一个重要领域,并逐渐成为软件开发的一种主要方法。面向对象方法以客观世界中的对象为中心,其分析和设计思想符合人们的思维方式,分析和设计的结果与客观世界的实际比较接近,容易被人们所接受。在面向对象方法中,分析和设计的界线并不明显,它们采用相同的符号表示,能方便地从分析阶段平滑地过渡到设计阶段。此外,在现实生活中,用户的需求经常会发生变化,但客观世界的对象以及对象间的关系相对比较稳定,因此用面向对象方法分析和设计的结果也相对比较稳定。
10.1 面向对象的基本概念
Peter Coad和Edward Yourdon提出用下面的等式识别面向对象方法。
面向对象=对象(object)+分类(classification)+继承(inheritance)+通过消息的通信(communication with messages)
可以说,采用这4个概念开发的软件系统是面向对象的。
1.对象
在面向对象的系统中,对象是基本的运行时的实体,它既包括数据(属性),也包括作用于数据的操作(行为)。所以,一个对象把属性和行为封装为一个整体。封装是一种信息隐蔽技术,它的目的是使对象的使用者和生产者分离,使对象的定义和实现分开。从程序设计者来看,对象是一个程序模块;从用户来看,对象为他们提供了所希望的行为。在对象内的操作通常叫做方法。一个对象通常可由对象名、属性和操作三部分组成。
在现实世界中,每个实体都是对象,如学生、汽车、电视机和空调等都是现实世界中的对象。每个对象都有它的属性和操作,如电视机有颜色、音量、亮度、灰度、频道等属性,可以有切换频道、增大/减低音量等操作。电视机的属性值表示了电视机所处的状态,而这些属性只能通过其提供的操作来改变。电视机的各组成部分,如显像管、电路板和开关等都封装在电视机机箱中,人们不知道也不关心电视机是如何实现这些操作的。
2.消息
对象之间进行通信的一种构造叫做消息。当一个消息发送给某个对象时,包含要求接收对象去执行某些活动的信息。接收到信息的对象经过解释,然后予以响应。这种通信机制叫做消息传递。发送消息的对象不需要知道接收消息的对象如何对请求予以响应。
3.类
一个类定义了一组大体上相似的对象。一个类所包含的方法和数据描述一组对象的共同行为和属性。把一组对象的共同特征加以抽象并存储在一个类中的能力,是面向对象技术最重要的一点。是否建立了一个丰富的类库,是衡量一个面向对象程序设计语言成熟与否的重要标志。
类是在对象之上的抽象,对象是类的具体化,是类的实例(instance)。在分析和设计时,通常把注意力集中在类上,而不是具体的对象。也不必为每个对象逐个定义,只需对类作出定义,而对类的属性的不同赋值即可得到该类的对象实例。
有些类之间存在一般和特殊关系,即一些类是某个类的特殊情况,某个类是一些类的一般情况。这是一种is-a关系,即特殊类是一种一般类。例如“汽车”类、“轮船”类、“飞机”类都是一种“交通工具”类。特殊类是一般类的子类,一般类是特殊类的父类。同样,“汽车”类还可以有更特殊的类,如“轿车”类、“货车”类等。在这种关系下形成一种层次的关联。
通常把一个类和这个类的所有对象称为“类及对象”或对象类。
4.继承
继承是父类和子类之间共享数据和方法的机制。这是类之间的一种关系,在定义和实现一个类的时候,可以在一个已经存在的类的基础上来进行,把这个已经存在的类所定义的内容作为自己的内容,并加入若干新的内容。图10-1表示了父类A和它的子类B之间的继承关系。
图10-1 类的继承关系
一个父类可以有多个子类,这些子类都是父类的特例,父类描述了这些子类的公共属性和操作。一个子类可以继承它的父类(或祖先类)中的属性和操作,这些属性和操作在子类中不必定义,子类中还可以定义自己的属性和操作。
图10-1中的B只从一个父类A得到继承,叫做“单重继承”。如果一个子类有两个或更多个父类,则称为“多重继承”。
5.多态
在收到消息时,对象要予以响应。不同的对象收到同一消息可以产生完全不同的结果,这一现象叫做多态(polymorphism)。在使用多态的时候,用户可以发送一个通用的消息,而实现的细节则由接收对象自行决定。这样,同一消息就可以调用不同的方法。
多态的实现受到继承的支持,利用类的继承的层次关系,把具有通用功能的消息存放在高层次,而不同的实现这一功能的行为放在较低层次,在这些低层次上生成的对象能够给通用消息以不同的响应。
多态有几种不同的形式,Cardelli和Wegner把它分为4类,如下所示。其中,参数多态和包含多态称为通用的多态,过载多态和强制多态称为特定的多态。
参数多态是应用比较广泛的多态,被称为最纯的多态。包含多态在许多语言中都存在,最常见的例子就是子类型化,即一个类型是另一个类型的子类型。过载(overloading)多态是同一个名字在不同的上下文中所代表的含义。
6.动态绑定(dynamic binding)
绑定是一个把过程调用和响应调用所需要执行的代码加以结合的过程。在一般的程序设计语言中,绑定是在编译时进行的,叫做静态绑定。动态绑定则是在运行时进行的,因此,一个给定的过程调用和代码的结合直到调用发生时才进行。
动态绑定是和类的继承以及多态相联系的。在继承关系中,子类是父类的一个特例,所以父类对象可以出现的地方,子类对象也可以出现。因此在运行过程中,当一个对象发送消息请求服务时,要根据接收对象的具体情况将请求的操作与实现的方法进行连接,即动态绑定。
10.2 面向对象程序设计
面向对象程序设计(Object-Oriented Programming, OOP)的实质是选用一种面向对象程序设计语言(Object-Oriented Programming Language, OOPL),采用对象、类及其相关概念所进行的程序设计。
定义什么是面向对象、什么不是面向对象的程序设计的工作是困难的,这不依赖于程序设计语言(可用C++语言编写纯C语言程序),而依赖于程序设计风格。当然,好的OOPL必须至少支持:
(1)被封装的对象。
(2)类和实例概念。
(3)类间的继承性。
(4)多态。
10.2.1 面向对象的好处
1.面向对象是程序设计新范型
程序设计范型(Programming Paradigm)是人们在程序设计时所采用的基本方式模型。程序设计范型决定了程序设计时采用的思维方式、使用的工具,同时又有一定的应用范畴。
程序设计范型的发展经历了过程程序设计、模块化程序设计、函数程序设计、逻辑程序设计,发展到现在的面向对象程序设计范型。
面向对象程序设计范型是在上述的范型之上发展起来的,它的关键在于加入类和继承性。类和继承性的设置,进一步提高了抽象的程度。
2.面向对象的好处
Ian Graham在《Object-Oriented Methods Principles & Practice》一书中,将面向对象的好处总结如下。
(1)对象技术解决了产品质量和生产率之间的权衡。精心设计的面向对象系统能够作为那些基本上从可重用构件组装而成的系统的基础,从而创造了更高的生产率;重用以前项目中经过测试的那些已经存在的类会使系统具有更高的质量。
(2)面向对象程序设计,特别是继承机制,使得系统具有很高的灵活性和易扩充性。
(3)面向对象是一个能管理复杂性并增强伸缩性的工具。
(4)根据面向对象的观点,以现实世界对应物为基础,把某一领域分割成各种对象进行分析与设计,常常比自顶向下进行功能分解的分析及设计更自然合理。
(5)从概念模型化到分析、设计、编码可以实现无缝传递。
(6)通过封装进行的信息隐蔽有助于建立安全的系统。
10.2.2 面向对象程序设计语言
在程序设计语言中引入面向对象概念实际上不是最近几年才有的,其根源可以追溯到20世纪60年代后期,那时Kristen Nyagaard和Ole-Johan Dahl在挪威计算中心开发出了一种叫做Simual67的语言。这种语言在用于Simula的基础上,第一次引入了类、协同程序(coroutines)和子类的概念,非常类似于今天的面向对象程序设计语言。
20世纪70年代中期,在Xerox公司的Palo Alto研究中心(Xerox PARC)中产生了第一个完整、健全的面向对象程序设计语言——Smalltalk,以后又进行了改进和扩充,形成了Smalltalk-80。这种语言中的每一个元素都分别是一个对象,从语言本身的实现、程序设计环境,到所支持的程序设计风格,都是面向对象的。Simula 67和Smalltalk的开发,是目前面向对象程序设计和面向对象程序设计语言研究中大多数工作的起点。
到今天为止,已经有大约100多种面向对象的和基于对象的程序设计语言。本节以综述的形式介绍几种目前比较流行的或者独特的OOPLs,其中包括Smalltalk、Eiffel、C++和Java。
1.Smalltalk
Smalltalk语言起源于20世纪60年代末期,主要设计者Alan Kay还在犹他大学念研究生时,就认识到可以用Simula语言中的概念来充实他在图形方面的研究工作。Alan Kay到Xerox PARC工作后,将这些早期思想发展为基于图形的交互式个人工作站概念,并萌发了设计一种新语言支持这些概念的想法。为Smalltalk的开发作出重要工作的另外两个人是Adale Goldberg和Dan Ingans。
Smalltalk在Xerox PARC历经了多次重大修改,最终形成了Smalltalk-80,事实上,Smalltalk并不是一种单纯的程序设计语言,而是反映面向对象程序设计思想的程序设计环境。这个系统在系统本身的设计中强调了对象概念的归一性,引入了类、方法、实例等概念和术语,应用了单重继承和动态绑定,成为OOPLs发展过程中的一个引人注目的里程碑。
在Smalltalk-80中,除了对象之外没有其他形式的数据,对一个对象的唯一操作就是向它发消息。在这种语言中,连类也被看成是对象——类是元类的实例。因此,定义一个类就是向元类发一条消息,请求元类执行它的new来生成一个实例,也就是生成这个类,而消息中的参数,就是关于这个类的说明。
Smalltalk-80全面支持面向对象的概念,表10-1给出了它关于这些概念所采用的术语,可以看出二者是完全一致的,表明了这种语言对OOP的深刻影响。
表10-1 Smalltalk-80关于面向对象概念的术语
2.Eiffel
Eiffel语言是继Smalltalk-80之后的另一个“纯”OOPL。这种语言是由OOP领域中著名的专家Bertrand Meyer等人20世纪80年代后期在ISE公司(Interactive Software Engineering Inc.)开发的,它的主要特点是全面的静态类型化、有大量的开发工具、支持多继承。在众多的OOPLs中,Eiffel是较早(1988年)对多重继承提供支持的,它的一些实现策略(如同名冲突处理、异常处理等)已经对后来的OOPLs的设计和实现产生了影响。
Eiffel也全面支持面向对象的概念,表10-2给出了它关于这些概念所采用的术语。
表10-2 Eiffel关于面向对象概念的术语
3.C++
C++语言是一种面向对象的强类型化语言,由AT&T的Bell实验室于1980年推出,目前已被国外许多主要的计算机和软件生产企业选为替代C语言或与C语言并存的基本开发工具。
C++语言是C语言的一个向上兼容的扩充,而不是一种新语言。C++是一种支持多范型的程序设计语言,它既支持面向对象的程序设计,也支持面向过程的程序设计。它融合了Simula67的几种面向对象机制——类、继承、虚拟函数,借鉴了ALGOL68中的操作符过载、变量声明位置不受限制等特性,形成了一种“比Smalltalk更接近于机器、比C语言更接近于问题”的OOPL。
C++支持基本的面向对象概念:对象、类、方法、消息、子类和继承性。它同时支持静态类型和动态类型。表10-3给出了C++中关于这些概念所采用的术语。
表10-3 C++关于面向对象概念的术语
C++完全支持多继承,并且通过使用try/throw/catch模式提供了一个完整的异常处理机制。
C++不提供自动的无用存储单元收集。这必须通过程序员来实现,或者通过编程环境提供合适的代码库来予以支持。
4.Java
Java语言起源于Oak语言,Oak语言被设计成能运行在设备(如微波炉、摄影机等)的嵌入芯片上。
Java编译成伪代码(P码或者字节代码),这需要一个虚拟机来对其进行解释。Java的虚拟机在几乎每一种平台上都可以运行。这实质上使得开发是与机器无关的,并且提供了通用的可移植性。
Java把类的概念和接口(Interface)的概念区分开来,并试图通过只允许接口的多继承来克服多继承的危险。Java支持基本的面向对象概念,表10-4给出了Java中关于这些概念所采用的术语。
Java的异常处理机制与C++的try/throw/catch相类似,但更加严密。在Java中,通过声明轻型线程来处理并发性,这些线程通过副作用和同步协议进行通信。
Java Beans是组件,即类和其所需资源的集合,它们主要被设计用来提供定制的GUI小配件。
Java有自己的对象请求代理技术——RMI(远程方法调用),这使得应用能够跨越网络来调用执行在其他Java应用程序内的方法。从第2版起,Java也包含了一个兼容CORBA的、语言独立的对象请求代理。Java也包含了用于管理与关系数据库进行交互的类JDBC和用于基本图形的类。
表10-4 Java关于面向对象概念的术语
10.2.3 程序设计语言中的OOP机制
作为一种程序设计范型,OOP主要解决程序设计方面的问题,因而与程序设计语言的发展密切相关。特定的OOP概念一般是通过OOPLs中特定的语言机制来体现的。本节用C++语言从程序设计的角度进一步讨论这些概念。
OOP现在已经扩展到系统分析和软件设计的范畴,出现了面向对象分析和面向对象设计的概念,这两部分内容见本章的10.3节和10.4节。
1.类
通常,在介绍OOP的书籍或文章中总是先引入对象的概念,然后从对对象进行抽象的角度来引入类的概念。但是,当设计和实现一个面向对象的程序时,首先接触到的不是对象,而是类和类层次结构。
【例10.1】雇员类的定义。
类具有实例化功能,包括实例生成(由类的Constructor完成)和实例消除(由类的Destructor完成)。类的实例化功能决定了类及其实例具有下面的特征。
(1)同一个类的不同实例具有相同的数据结构,承受的是同一方法集合所定义的操作,因而具有规律相同的行为。
(2)同一个类的不同实例可以持有不同的值,因而可以具有不同的状态。
(3)实例的初始状态(初值)可以在实例化时确定。
2.继承和类层次结构
孤立的类只能描述实体集合的特征同一性,而客观世界中实体集合的划分通常还要考虑实体特征方面有关联的相似性。在OOP中使用继承机制解决这一问题。
在OOPLs中,继承一般通过定义类之间的关系来体现。
在一个面向对象系统中,子类与父类之间的继承关系构成了这个系统的类层次结构,可以用树(对应于单继承)或格(对应于多继承)这样的图来描述。
【例10.2】经理类的定义。
当执行一个子类的实例生成方法时,首先在类层次结构中从该子类沿继承路径上溯至它的一个基类,然后自顶向下地执行该子类所有父类的实例生成方法;最后执行该子类实例生成方法的函数体。当执行一个子类的实例消除方法时,顺序恰好与执行该子类实例生成方法相反:先执行该子类实例消除方法的函数体,再沿继承路径自底向上地执行该子类所有父类的实例消除方法。
与一般数据类型的实例化过程相比,类的实例化过程是一种实例的合成过程,而不仅仅是根据单个类型进行的空间分配、初始化和联编。
OOPLs中的继承机制体现了一条重要的面向对象程序设计原则:开发人员在构造程序时不必从零开始,而只需对差别进行程序设计。支持继承也是OOPLs与传统程序设计语言在语言机制方面最根本的区别。
3.对象、消息传递和方法
对象是类的实例。尽管对象的表示在形式上与一般数据类型十分相似,但是它们之间存在一种本质区别:对象之间通过消息传递方式进行通信。
消息传递源是一种与通信有关的概念,OOP使得对象具有交互能力的主要模型就是消息传递模型。对象被看成用传递消息的方式互相联系的通信实体,它们既可以接收,也可以拒绝外界发来的消息。一般情况下,对象接收它能够识别的消息,拒绝它不能识别的消息。对于一个对象而言,任何外部的代码都不能以任何不可预知或事先不允许的方式与这个对象进行交互。
发送一条消息至少应给出一个对象的名字和要发给这个对象的那条消息的名字。通常,消息的名字就是这个对象中外界可知的某个方法的名字。在消息中,经常还有一组参数(也就是那个方法所要求的参数),将外界的有关信息传给这个对象。
对于一个类来说,它关于方法界面的定义规定了实例的消息传递协议,而它本身则决定了消息传递的合法范围。由于类是先于对象构造而成的,所以一个类为它实例提供了可以预知的交互方式。例如,假设ml是类Manager的一个实例(或对象),当外界要求把这个对象所代表的那位经理的级别改变为2时,就应以下面的方式向这个对象发出一条消息:
4.对象自身引用
对象自身引用(self-reference)是OOPLs中的一种特有结构。这种结构在不同的OOPLs中有不同的名称,在C++和Java中称为this,在Smalltalk-80、Object-C和其他一些OOPLs中则称为self。
以C++语言为例,对于类c和方法c∷m,在c∷m方法体中出现的c的成员名n将被编译程序按this->n来对待。这里的this是一个类型为c*的指针(在Java语言中this是一个引用),它的值由语言中的消息传递机制提供。
对象自身引用的值和类型分别扮演了两种意义的角色:对象自身引用的值使得方法体中引用的成员名与特定的对象相关,对象自身引用的类型则决定了方法体被实际共享的范围。
对象自身引用机制使得在进行方法的设计和实现时并不需要考虑与对象联系的细节,而是从更高一级的抽象层次,也就是类的角度来设计同类型对象的行为特征,从而使得方法在一个类及其子类的范围内具有共性。在程序运行过程中,消息传递机制和对象自身引用将方法与特定的对象动态地联系在一起,使得不同的对象在执行同样的方法体时,可以因对象的状态不同而产生不同的行为,从而使得方法对具体的对象具有个性。
5.重置
重置(overriding)的基本思想是:通过一种动态绑定机制的支持,使得子类在继承父类界面定义的前提下,用适合于自己要求的实现去置换父类中的相应实现。
在OOPLs中,重置机制有相应的语法供开发人员选择使用。在C++语言中,通过虚拟函数(virtual function)的定义来进行重置的声明,通过虚拟函数跳转表(virtual functions jump tables, vtbl)结构来实现重置方法体的动态绑定。在Java语言中,通过抽象方法(abstract method)来进行重置的声明,通过方法查找(method lookup)实现重置方法体的动态绑定。
下面以C++语言为例,说明重置机制的使用。
【例10.3】使用重置修改类Employee和Manager。
6.类属类
类属是程序设计语言中普遍注重的一种参数多态机制,在OOPLs中也不例外。本节主要介绍与类有关的类属。
在C++语言中,类属有专门的术语——template。
【例10.4】类属类。
其中,<class T>中的class用来说明T是一个类型变元,但并不意味着T一定是一个类,也可以是基本类型。
类属类可以看成是类的模板。一个类属类是关于一组类的一个特性抽象,它强调的是这些类的成员特征中与具体类型无关的那些部分,而与具体类型相关的那些部分则用变元来表示。这就使得对类的集合也可以按照特性的相似性再次进行划分。类属类的一个重要作用,就是对类库的建设提供了强有力的支持。
重置和类属都是一种多态机制,多态的基本概念以及多态的分类见本章10.1节,对于其他利类的多态不在本节进行介绍。
7.无实例的类
在前几节中曾指出,类是对象的模板,对象是类的实例。那么是否每个类都至少有一个实例?如果在类之间没有定义继承关系,回答是肯定的。这是因为若存在没有实例的类,那么这样的类对程序的行为没有任何贡献,因而是冗余的。相反,如果存在继承关系,那么的确有可能在类层次结构的较高层次上看到始终没有实例的类。
要创建无实例的类,仍然需要语言的支持。在C++和Java语言中,抽象类就是这样的类。在C++中通过在类中定义纯虚拟函数来创建一个抽象类,在Java中通过在类中定义抽象方法来创建一个抽象类,或者直接将一个类声明为抽象类。
【例10.5】用C++语言创建抽象类。
【例10.6】用Java语言创建抽象类。
或者将整个类命名为抽象类,而不管这个类中是否包含抽象方法。
10.2.4 面向对象的程序
1.类库
类库是一种预先定义的程序库,可以由开发人员自己扩充。
类库以程序模块的形式,按照类层次结构把一组类的定义和实现组织在一起。通常,这一组类预先提供的是一些低层功能,如输入输出例程、图形操作原语和基本数据结构等,这些功能覆盖了传统例程库所提供的功能。
开发人员可以直接使用类库中的类,其方式与使用语言中的基本类型完全相同。开发人员可以扩充类库,办法是定义和实现类库中已有类的子类,再将这样的子类加入类库。
要想发挥面向对象的软件构造方式的优点,开发人员必须知道类库是怎样组织起来的。衡量一个开发人员的好坏,要看他是否知道如何来最好地发挥已有类库的优点,看他有没有能力将已有的类库与新的问题紧密地匹配起来,还要看他不得不另外编写的代码是不是最少。
大多数程序设计语言都有类库。最早引入类库的语言是Smalltalk,Smalltalk类库中基本的类层次结构片断如图10-2所示(为了清晰起见,图中略去了表示继承方向的箭头)。
图10-2 Smalltalk类库中基本的类层次结构
2.面向对象程序设计实例
【例10.7】C++语言本身不提供对数组下标越界的判断。为了解决这一问题,本例的程序中定义了相应的模板类,使得对于任意类型的二维数组,可以在访问数组元素的同时,对行下标和列下标进行越界判断,并给出相应的提示信息。
程序如下:
10.3 面向对象开发技术
众所周知,采用系统化的开发方法开发软件(特别是大规模软件)系统才可获得好的系统。什么是“好”系统呢?这个问题应从使用者(外部)和开发者(内部)两个角度来回答。从使用者观点出发,需要系统具有易学易用、界面友好、正确使用时能快速给出正确结果、效率高等优点,还要求系统安全可靠等;从系统开发者和管理者角度出发,要求系统易于修改和扩充、易于理解、易于测试和重用、易于与其他系统兼容和管理等。虽然不同的系统所强调的特性可能不同,但上述所要求的系统特性是基本特性。
相对而言,面向对象的开发技术更有利于开发具有上述特性的软件系统。本章简要介绍面向对象开发技术的基本概念和相关步骤。
10.3.1 面向对象分析
同其他分析方法一样,面向对象分析(Object-Oriented Analysis, OOA)的目的是为了获得对应用问题的理解。理解的目的是确定系统的功能、性能要求。面向对象分析法与功能/数据分析法之间的差别是前期的表述含义不同。功能/数据分析法分开考虑系统的功能要求和数据及其结构,面向对象分析方法是将数据和功能结合在一起作为一个综合对象来考虑。面向对象分析技术可以将系统的行为和信息间的关系表示为迭代构造特征。
面向对象分析包含5个活动:认定对象、组织对象、描述对象间的相互作用、定义对象的操作、定义对象的内部信息。
1.认定对象
在应用领域中,按自然存在的实体确立对象。在定义域中,首先将自然存在的“名词”作为一个对象,这通常是研究问题、定义域实体的良好开始。通过实体间的关系寻找对象常常没有问题,而困难在于寻找(选择)系统关心的实质性对象,实质性对象是系统稳定性的基础。例如在银行应用系统中,实质性对象应包含客户账务、清算等,而门卫值班表不是实质性对象,甚至可不包含在该系统中。
2.组织对象
分析对象间的关系,将相关对象抽象成类,其目的是为了简化关联对象,利用类的继承性建立具有继承性层次的类结构。抽象类时可从对象间的操作或一个对象是另一个对象的一部分来考虑,如房子由门和窗构成,门和窗是房子类的子类。由对象抽象类,通过相关类的继承构造类层次,所以说系统的行为和信息间的分析过程是一种迭代表征过程。
3.对象间的相互作用
描述出各对象在应用系统中的关系,如一个对象是另一个对象的一部分,一个对象与其他对象间的通信关系等。这样可以完整地描述每个对象的环境,由一个对象解释另一个对象,以及一个对象如何生成另一个对象,最后得到对象的界面描述。
4.基于对象的操作
当考虑对象的界面时,自然要考虑对象的操作。其操作有从对象直接标识的简单操作,如创建、增加和删除等;也有更复杂的操作,如将几个对象的信息连接起来。一般而言,避免对象太复杂比较好,当连接的对象太复杂时,可将其标识为新对象。当确定了对象的操作后,再定义对象的内部,对象内部定义包括其内部数据信息、信息存储方法、继承关系以及可能生成的实例数等属性。
分析阶段最重要的是理解问题域的概念,其结果将影响整个工作。经验表明,从应用定义域概念标识对象是非常合理的,完成上述工作后写出规范文档,文档确定每个对象的范围。
早期面向对象的目标之一是简化模型与问题域之间的语义差距。事实上,面向对象分析的基础是软件系统结构,这依赖于人类看待现实世界的方法。当人们理解求解问题的环境时,常采用对象、分类法和层次性这类术语。面向对象分析与功能/数据分析方法相比,面向对象的结果比较容易理解和管理。面向对象分析方法的另一个优点是便于修改,早期阶段的修改容易提高软件的可靠性。
10.3.2 面向对象设计
面向对象设计(Object-Oriented Design, OOD)的含义是设计分析模型和实现相应源代码,在目标代码环境中这种源代码可被执行。通常情况是,由概念模型生成的分析模型被装入到相应的执行环境中时,还需要修改。
同所有其他设计活动一样,要使系统的结构好且效率高,做好相互间的平衡是困难的。分析模型已经提供了概念结构,它将试图长期保存。另外,设计期必须充分考虑系统的稳定性,如目标环境所需的最大存储空间、可靠性和响应时间等,所有这些都会影响系统的结构。
对象标识期间的目标是分析对象,设计过程也是发现对象的过程,称之为再处理。因此,必须有从分析模型到设计模型到程序设计语言的线性转换规则。对象可以用预先开发的源代码实现,称这样的部分为构件(component)。由于这些构件是功能和数据的组装,因此,常常用于简化面向对象环境的产生。
如前所述,面向对象系统的开发需要面向对象的程序设计风格,这与OOPL无直接关系。面向对象是一种程序设计风格,而不只是一种具有构造继承性、封装性和多态的程序设计语言族的命名。
10.3.3 面向对象测试
就测试而言,用面向对象方法开发的系统测试与其他方法开发的系统测试没有什么不同,在所有开发系统中都是根据规范说明来验证系统设计的正确性。程序验证应尽可能早地开始。程序调试步骤是从最底层开始,从单元测试、综合测试到系统测试。单元测试是系统构件的分体测试。将测试好的系统构件接起来看它们之间相互作用的正确性称为综合测试。最后是整个系统的测试,包括软件系统所在相关环境的测试。通常综合测试是一种“主攻”活动,在系统开发期是非常关键的。这一阶段应随时连结已开发的每一部分,再看它们的实际工作,这种“主攻”活动在面向对象系统中是一种实质性的、渐渐增长的测试策略。
面向对象的系统由一些对象和它们相互间的通信组成,这些对象包括了它们的数据属性和操作(动作),比传统系统开发方法中独立工作的子程序大。对象的操作局限于特定数据,一个对象的所有操作自然由同一个设计者开发。这导致单元测试比传统系统的单元大,综合测试尽可能在早期阶段处理,因为通信是系统开发的实质。所有对象有预定义的界面,这也有利于综合测试。当综合测试继续到较高层次时,那么越来越多的对象就会被逐步连接起来。面向对象的综合测试是由底向上的测试。
一般来说,对面向对象软件的测试可分为下列4个层次进行。
(1)算法层。测试类中定义的每个方法,基本上相当于传统软件测试中的单元测试。
(2)类层。测试封装在同一个类中的所有方法与属性之间的相互作用。在面向对象软件中类是基本模块,因此可以认为这是面向对象测试中所特有的模块测试。
(3)模板层。测试一组协同工作的类之间的相互作用。大体上相当于传统软件测试中的集成测试,但是也有面向对象软件的特点(例如,对象之间通过发送消息相互作用)。
(4)系统层。把各个子系统组装成完整的面向对象软件系统,在组装过程中同时进行测试。
软件工程中传统的测试用例设计技术,如逻辑覆盖、等价类划分和边界值分析等方法,仍然可以作为测试类中每个方法的主要技术。面向对象测试的主要目标也是用尽可能低的测试成本和尽可能少的测试用例,发现尽可能多的错误。但是,面向对象程序中特有的封装、继承和多态等机制,也给面向对象测试带来一些新特点,增加了测试和调试的难度。
10.4 面向对象分析与设计方法
在实现一个系统之前应当十分重视对系统需求的理解和交流,这一点已经是公认的准则。结构化分析方法产生于20世纪60年代,基本思想是对一个系统的功能构件进行分解,将一种更易于管理、更加确定的方法引入系统分析阶段。面向对象分析强调的则是对一个系统中对象的特征和行为的定义。
目前,国际上已经出现了多种面向对象的方法,例如Peter Coad和Edward Yourdon的OOA和OOD方法、Booch的OOD方法、OMT(Object Modeling Technique,面向对象建模技术)方法及UML(Unified Modeling Language,统一建模语言)。本节将对这4种方法进行简要介绍。
10.4.1 Peter Coad和Edward Yourdon的OOA和OOD方法
1.OOA
OOA模型由下列5个层次和5个活动组成。
5个层次:主题层、对象类层、结构层、属性层和服务层。
5个活动:标识对象类、标识结构、定义主题、定义属性和定义服务。
在这种方法中定义了两种对象类之间的结构,一种称为分类结构,一种称为组装结构。分类结构就是一般与特殊关系,是一种is a关系。组装结构则反映了对象之间的整体与部分关系,例如“计算机”对象由“显示器”、“主机箱”、“键盘”和“鼠标”等对象组成,而“主机箱”对象又可由“主板”、CPU、“内存”和“风扇”等对象组成。它实际上是一种has a的关系。
主题提供了控制读者在一段时间内考虑和理解模型范围围的一种机制,在OOA中将与某一主题有关的对象用主题框围起来,其目的是减少读者理解系统的复杂程度。
OOA在定义属性的同时,还要识别实例连接。实例连接是一个实例对象与另一个实例对象的映射关系(或者说是一种简单的对应关系)。例如,一个公司有多个职员,一个职员只能在一家公司工作,那么“公司”类的实例与“职员”类的实例间就有1对多的实例连接关系。OOA在定义服务的同时要识别消息连接。当一个对象需要向另一个对象发送消息时,它们之间就存在消息连接。
需要强调的是,这5个活动不是必须顺序进行的,有些分析员喜欢先识别对象类,然后定义属性、服务,再识别结构和主题;有的则喜欢先识别对象类、结构和主题,再定义属性和服务。总之5个活动都完成了,OOA的模型就建立了。5个层次并不是构成软件系统的层次,而是一旦建立了模型,就可以在5个层次上进行表示和复审。
2.OOD
OOA中的5个层次和5个活动继续贯穿在OOD过程中。OOD模型由4个部分和4个活动组成,如图10-3所示。
图10-3 Coad & Yourdon的OOD模型
4个活动是设计问题域部件、设计人机交互部件、设计任务管理部件及设计数据管理部件。
(1)设计问题域部件。OOA的结果恰好就是OOD的问题域部件,分析的结果在OOD中可以被改动或增补,但基于问题域的总体组织框架是长时间稳定的。
(2)设计人机交互部件。人机交互部件在上述结果中加入人机交互的设计和交互的细节,包括窗口和输出报告的设计。可以用原型来帮助实际交互机制的开发和选择。
(3)设计任务管理部件。这部分主要是识别事件驱动任务,识别时钟驱动任务,识别优先任务和关键任务,识别协调者,审查每个任务并定义每个任务。
(4)设计数据管理部件。数据管理部件提供了在数据管理系统中存储和检索对象的基本结构,其目的是隔离数据管理方案(如普通文件、关系数据库和面向对象数据库等)对其他部分的影响。
10.4.2 Booch的OOD方法
Booch认为软件开发是一个螺旋上升的过程,在螺旋上升的每个周期中有以下步骤。
(1)标识类和对象。
(2)确定它们的含义。
(3)标识它们之间的关系。
(4)说明每一个类的界面和实现。
Booch的OOD模型如图10-4所示。
图10-4 Booch的OOD模型
除了类图、对象图、模块图和进程图外,Booch的OOD中还使用了两种动态描述图,一种是刻画特定类实例的状态转换图,另一种是描述对象间事件变化的时序图。
10.4.3 OMT方法
对象建模技术(Object Modeling Technique, OMT)是由J.Rumbaugh等人提出的。OMT定义了三种模型,它们是对象模型、动态模型和功能模型,OMT用这三种模型来描述系统。OMT方法有4个步骤:分析、系统设计、对象设计和实现。OMT方法的每一步都使用这三种模型,通过每一步对三种模型不断地精化和扩充。
1.对象模型、动态模型和功能模型
1)对象模型
对象模型描述系统中对象的静态结构、对象之间的关系、对象的属性、对象的操作。对象模型表示静态的、结构上的、系统的“数据”特征。对象模型为动态模型和功能模型提供了基本的框架。对象模型用包含对象和类的对象图来表示。
OMT的对象模型中除了对象、类、继承外,还有一些其他的概念,下面介绍几个主要的概念。
(1)链(link)和关联(association)。链表示实例对象间的物理或概念上的连接。例如在表10-5中,Joe Doe为Simplex公司工作,工资2000元。关联描述具有公共结构和公共语义的一组链,例如关联works-for描述了一组某人为某公司工作的链。实际上链是关联的一个实例。链可以有属性,称为链属性,链属性表示关联中链的性质。图10-5给出了链、关联和链属性的一个实例。图中的实心圆是关联的阶(也称为重数),阶指出一个类的多少个实例可以与所关联的类的一个实例相关。实心圆表示0或多个,空心圆表示0或1个,没有圆表示1个。
表10-5 人员登记表
图10-5 链、关联和链属性
(2)泛化(Generalization)。泛化是一个类与它的一个或多个细化种类之间的关系,即一般与特殊的关系。被细化的类称为父类,每个细化的种类称为子类,子类可以继承父类的性质。
(3)聚集(Aggregation)。聚集是一种整体与部分的关系,在这种关系中表示整体的对象与表示部分的对象关联。图10-6给出了泛化和聚集的实例。
图10-6 泛化和聚集
(4)模块(module)。模块是组合类、关联和泛化的一种逻辑结构。模块给出了某个主题的视图。
2)动态模型
动态模型描述与时间和操作顺序有关的系统特征——激发事件、事件序列、确定事件先后关系以及事件和状态的组织。动态模型表示瞬时的、行为上的、系统的“控制”特征。动态模型用状态图来表示。每张状态图显示了系统中一个类的所有对象所允许的状态和事件的顺序。
3)功能模型
功能模型描述与值的变换有关的系统特征——功能、映射、约束和函数依赖。功能模型用数据流图来表示。
对象模型、动态模型和功能模型之间具有下述关系。
(1)与功能模型的关系:对象模型展示了功能模型中的动作者、数据存储和流的结构,动态模型展示了执行加工的顺序。
(2)与对象模型的关系:功能模型展示了类上的操作和每个操作的变量,因此它也表示了类之间的“供应者—客户”关系;动态模型展示了每个对象的状态以及它接收事件和改变状态时所执行的操作。
(3)与动态模型的关系:功能模型展示了动态模型中未定义的不可分解的动作和活动的定义,对象模型展示了是谁改变了状态和承受了操作。
2.OMT的步骤
(1)分析。分析是OMT方法的第一步,其目的是建立可理解的现实世界模型。分析从问题陈述入手,通过与客户的不断交互以及对现实世界背景知识的了解,对能反映系统的三个本质特征(对象类及它们之间的关系、动态的控制流和受约束的数据的函数变换)进行分析,并构造出现实世界的模型。分析模型必须简洁、明确地抽象目标系统必须做的事情,而不是如何做这些事。模型中的对象应该是应用领域中的概念,而不是诸如数据结构之类的计算机实现中的概念。一个好的模型应该被应用领域专家而不是被程序员所理解和评价。分析模型不应包含任何与实现有关的考虑。
(2)系统设计。系统设计阶段确定整个系统的体系结构,形成求解问题和建立解答的高层次策略。系统设计人员必须形成的决策有:将系统分解成子系统,标识问题中固有的并发性,将子系统分配到处理器和任务,选择数据存储管理的手段,处理对全局资源的访问,选择软件中控制的实现,处理边界条件,设置折衷的优先级等。
(3)对象设计。在分析的基础上,对象设计阶段建立基于分析模型的设计模型,并考虑实现的细节。设计人员根据系统设计期间建立的策略把实现细节加入到设计模型中。对象设计的焦点是实现每个类的数据结构及所需的算法。分析模型中的对象类仍然是有意义的,但它们中都加入了数据结构和算法。
对象设计期间,设计者必须履行下列步骤:综合考虑三个模型以获得类的操作;设计实现操作的算法,优化数据访问路径,实现与外部交互的控制;调整类结构以增加继承,设计关联,确定对象表示,将类和关联包装到模块中。
(4)实现。实现阶段将对象设计阶段开发的对象类及其关系转换成特定的程序设计语言、数据库或硬件的实现。程序设计是开发周期中相对较小且机械的部分,因为在对象设计中已形成了所有重要的决策。目标语言在某种程度上能影响设计决策,但设计不应依赖于程序设计语言的细节。在实现期间,重要的是应该有一些好的软件工程准则作指导。
10.4.4 UML概述
统一建模语言是面向对象软件的标准化建模语言。由于其简单、统一,又能够表达软件设计中的动态和静态信息,目前已经成为可视化建模语言事实上的工业标准。
从企业信息系统到基于Web的分布式应用,甚至严格的实时嵌入式系统都适合用UML来建模。它是一种富有表达力的语言,可以描述开发所需要的各种视图,然后以此为基础装配系统。UML由三个要素构成:UML的基本构造块、支配这些构造块如何放置在一起的规则和运用与整个语言的一些公共机制。限于篇幅,下面仅对UML中的基本构造块进行讨论。
UML的词汇表包含三种构造块:事物、关系和图。事物是对模型中最具有代表性的成分的抽象;关系把事物结合在一起;图聚集了相关的事物。
1.事物
UML中有4种事物:结构事物、行为事物、分组事物和注释事物。
(1)结构事物(structural thing)。结构事物是UML模型中的名词。它们通常是模型的静态部分,描述概念或物理元素。结构事物包括类(class)、接口(interface)、协作(collaboration)、用例(use case)、主动类(active class)、构件(component)和节点(node)。
各种结构事物的图形化表示如图10-7所示。
图10-7 结构事物的图形表示
(2)行为事物(behavior thing)。行为事物是UML模型的动态部分。它们是模型中的动词,描述了跨越时间和空间的行为。共有两类主要的行为事物:交互(interaction)和状态机(state machine)。
交互由在特定语境中共同完成一定任务的一组对象之间交换的消息组成。一个对象群体的行为或单个操作的行为可以用一个交互来描述。交互涉及一些其他元素,包括消息、动作序列(由一个消息所引起的行为)和链(对象间的连接)。在图形上,把一个消息表示为一条有向直线,通常在表示消息的线段上总有操作名。如图10-8所示。
状态机描述了一个对象或一个交互在生命期内响应事件所经历的状态序列。单个类或一组类之间协作的行为可以用状态机来描述。一个状态机涉及到一些其他元素,包括状态、转换(从一个状态到另一个状态的流)、事件(触发转换的事物)和活动(对一个转换的响应)。在图形上,把状态表示为一个圆角矩形,通常在圆角矩形中含有状态的名称及其子状态,如图10-9所示。
图10-8 消息
图10-9 状态
(3)分组事物(grouping thing)。分组事物是UML模型的组织部分。它们是一些由模型分解成的“盒子”。在所有的分组事物中,最主要的分组事物是包(package)。包是把元素组织成组的机制,这种机制具有多种用途。结构事物、行为事物甚至其他分组事物都可以放进包内。包不像构件(仅在运行时存在),它纯粹是概念上的(即它仅在开发时存在)。包的图形化表示如图10-10所示。
(4)注释事物(annotational thing)。注释事物是UML模型的解释部分。这些注释事物用来描述、说明和标注模型的任何元素。注解(note)是一种主要的注释事物。注解是一个依附于一个元素或者一组元素之上,对它进行约束或解释的简单符号。注解的图形化表示如图10-11所示。
图10-10 包
图10-11 注解
2.关系
UML中有4种关系:依赖、关联、泛化和实现。
(1)依赖(dependency)。依赖是两个事物间的语义关系,其中一个事物(独立事物)发生变化会影响另一个事物(依赖事物)的语义。在图形上,把一个依赖画成一条可能有方向的虚线,如图10-12所示。
(2)关联(association)。关联是一种结构关系,它描述了一组链,链是对象之间的连接。聚集(aggregation)是一种特殊类型的关联,它描述了整体和部分间的结构关系。关联和聚集的图形化表示如图10-13和图10-14所示。
图10-12 依赖
图10-13 关联
图10-14 聚集
在关联上可以标注重复度(multiplicity)和角色(role)。
(3)泛化(generalization)。泛化是一种特殊/一般关系,特殊元素(子元素)的对象可替代一般元素(父元素)的对象。用这种方法,子元素共享了父元素的结构和行为。在图形上,把一个泛化关系画成一条带有空心箭头的实线,它指向父元素,如图10-15所示。
(4)实现(realization)。实现是类元之间的语义关系,其中一个类元指定了由另一个类元保证执行的契约。在两种地方要遇到实现关系:一种是在接口和实现它们的类或构件之间;另一种是在用例和实现它们的协作之间。在图形上,把一个实现关系画成一条带有空心箭头的虚线,如图10-16所示。
图10-15 泛化
图10-16 实现
这4种关系是UML模型中可以包含的基本关系事物。它们也有变体,例如,依赖的变体有精化、跟踪、包含和延伸。
3.UML中的图
图(diagram)是一组元素的图形表示,大多数情况下把图画成顶点(代表事物)和弧(代表关系)的连通图。为了对系统进行可视化,可以从不同的角度画图,这样图是对系统的投影。
UML2.0提供了13种图,分别是类图、对象图、用例图、序列图、通信图、状态图、活动图、构件图、部署图、组合结构图、包图、交互概览图和时序图。
1)类图
类图(class diagram)展现了一组对象、接口、协作和它们之间的关系。在面向对象系统的建模中所建立的最常见的图就是类图。类图给出系统的静态设计视图。包含主动类的类图给出了系统的静态进程视图。
类图中通常包括下述内容(如图10-17所示)。
图10-17 UML类图
(1)类。
(2)接口。
(3)协作。
(4)依赖、泛化和关联关系。
类图中也可以包含注解和约束。类图还可以含有包或子系统,二者都用于把模型元素聚集成更大的组块。
类图用于对系统的静态设计视图建模。这种视图主要支持系统的功能需求,即系统要提供给最终用户的服务。当对系统的静态设计视图建模时,通常以下述三种方式之一使用类图。
(1)对系统的词汇建模。对系统的词汇建模涉及到做出这样的决定:哪些抽象是考虑中的系统的一部分,哪些抽象处于系统边界之外。用类图详细描述这些抽象和它们的职责。
(2)对简单的协作建模。协作是一些共同工作的类、接口和其他元素的群体,该群体提供的一些合作行为强于所有这些元素的行为之和。例如,当对分布式系统的事务语义建模时,不能仅仅盯着一个单独的类来推断要发生什么,而要有相互协作的一组类来实现这些语义。用类图对这组类以及它们之间的关系进行可视化和详述。
(3)对逻辑数据库模式建模。将模式看作为数据库的概念设计的蓝图。在很多领域中,要在关系数据库或面向对象数据库中存储永久信息,可以用类图对这些数据库的模式建模。
2)对象图
对象图(object diagram)展现了一组对象以及它们之间的关系。对象图描述了在类图中所建立的事物的实例的静态快照。对象图一般包括对象和链。
和类图一样,对象图给出系统的静态设计视图或静态进程视图,但它们是从真实的或原型案例的角度建立的。这种视图主要支持系统的功能需求,即系统应该提供给最终用户的服务。利用对象图可以对静态数据结构建模。
当对系统的静态设计视图或静态进程视图建模时,主要是使用对象图对对象结构进行建模。对对象结构建模涉及到在给定时刻抓取系统中对象的快照。对象图表示了交互图表示的动态场景的一个静态画面。可以使用对象图可视化、详述、构造和文档化系统中存在的实例以及它们之间的相互关系。
3)用例图
用例图(use case diagram)展现了一组用例、参与者(Actor)以及它们之间的关系。
用例图通常包括如下内容(如图10-18所示)。
图10-18 UML用例图
(1)用例。
(2)参与者。
(3)扩展关系、包含关系。
用例图用于对系统的静态用例视图进行建模。这个视图主要支持系统的行为,即该系统在它的周边环境的语境中所提供的外部可见服务。
当对系统的静态用例视图建模时,可以用下列两种方式来使用用例图。
(1)对系统的语境建模。对一个系统的语境进行建模,包括围绕整个系统画一条线,并声明有哪些参与者位于系统之外并与系统进行交互。在这里,用例图说明了参与者以及他们所扮演的角色的含义。
(2)对系统的需求建模。对一个系统的需求进行建模,包括说明这个系统应该做什么(从系统外部的一个视点出发),而不考虑系统应该怎样做。在这里,用例图说明了系统想要的行为。通过这种方式,用例图使我们能够把整个系统看作一个黑盒子。可以观察到系统外部有什么,系统怎样与哪些外部事物相互作用,但却看不到系统内部是如何工作的。
4)交互图
序列图、通信图、交互概览图和时序图均被称为交互图,它们用于对系统的动态方面进行建模。一张交互图显示的是一个交互,由一组对象和它们之间的关系组成,包含它们之间可能传递的消息。顺序图是强调消息时间顺序的交互图;通信图则是强调接收和发送消息的对象的结构组织的交互图。
交互图用于对一个系统的动态方面建模。在多数情况下,它包括对类、接口、构件和节点的具体的或原型化的实例以及它们之间传递的消息进行建模,所有这些都位于一个表达行为的脚本的语境中。交互图可以单独使用,来可视化、详述、构造和文档化一个特定的对象群体的动态方面,也可以用来对一个用例的特定的控制流进行建模。
交互图一般包含对象、链和消息。
(1)序列图。序列图(sequence diagram)是场景(scenario)的图形化表示,描述了以时间顺序组织的对象之间的交互活动。如图10-19所示,形成序列图时,首先把参加交互的对象放在图的上方,沿x轴方向排列。通常把发起交互的对象放在左边,下级对象依次放在右边。然后,把这些对象发送和接收的消息沿Y轴方向按时间顺序从上到下放置。这样,就提供了控制流随时间推移的清晰的可视化轨迹。
序列图有两个不同于通信图的特征。
① 序列图有对象生命线。对象生命线是一条垂直的虚线,表示一个对象在一段时间内存在。在交互图中出现的大多数对象存在于整个交互过程中,所以这些对象全都排列在图的顶部,其生命线从图的顶部画到图的底部。但对象也可以在交互过程中创建,它们的生命线从接收到构造型为create的消息时开始。对象也可以在交互过程中撤销,它们的生命线在接收到构造型为destroy的消息时结束(并且给出一个大X的标记表明生命的结束)。
图10-19 UML序列图
② 序列图有控制焦点。控制焦点是一个瘦高的矩形,表示一个对象执行一个动作所经历的时间段,既可以是直接执行,也可以是通过下级过程执行。矩形的顶部表示动作的开始,底部表示动作的结束(可以由一个返回消息来标记)。还可以通过将另一个控制焦点放在它的父控制焦点的右边来显示(由循环、自身操作调用或从另一个对象的回调所引起的)控制焦点的嵌套(其嵌套深度可以任意)。如果想特别精确地表示控制焦点在哪里,也可以在对象的方法被实际执行(并且控制还没传给另一个对象)期间,将那段矩形区域阴影化。
(2)通信图。通信图(communication diagram)强调收发消息的对象的结构组织,在早期的版本中也被称作协作图。通信图强调参加交互的对象的组织。产生一张通信图,首先要将参加交互的对象作为图的顶点,然后把连接这些对象的链表示为图的弧,最后用对象发送和接收的消息来修饰这些链。这就提供了在协作对象的结构组织的语境中观察控制流的一个清晰的可视化轨迹。
通信图有两个不同于序列图的特性。
① 通信图有路径。为了指出一个对象如何与另一个对象链接,可以在链的末端附上一个路径构造型(如构造型《local》,表示指定对象对发送者而言是局部的)。通常只需要显式地表示以下几种链的路径:local(局部)、parameter(参数)、global(全局)以及self(自身),但不必表示association(关联)。
② 通信图有顺序号。为表示一个消息的时间顺序,可以给消息加一个数字前缀(从1号消息开始),在控制流中,每个新消息的顺序号单调增加(如2,3等)。为了显示嵌套,可使用带小数点的号码(1表示第一个消息;1.1表示嵌套在消息1中的第一个消息,1.2表示嵌套在消息1中的第二个消息,等等)。嵌套可为任意深度。还要注意的是,沿同一个链可以显示许多消息(可能发自不同的方向),并且每个消息都有唯一的一个顺序号。
序列图和通信图是同构的,它们之间可以相互转换。
交互概览图是UML2.0新增的交互图之一,它描述交互(特别是关注控制流),但是抽象掉了消息和生命线。它使用活动图的表示法。纯粹的交互概览图中所有的活动都是交互发生。另一种新增的、特别适合实时和嵌入式系统建模的交互图称为时序图,时序图关注沿着线性时间轴、生命线内部和生命线之间的条件改变。它描述对象状态随着时间改变的情况,很像示波器,适合分析周期和非周期性任务。
5)状态图
状态图(state diagram)展现了一个状态机,它由状态、转换、事件和活动组成。状态图关注系统的动态视图,它对于接口、类和协作的行为建模尤为重要,强调对象行为的事件顺序。
状态图通常包括简单状态和组合状态、转换(事件和动作)。如图10-20所示。
图10-20 UML状态图
可以用状态图对系统的动态方面建模。这些动态方面可以包括出现在系统体系结构的任何视图中的任何一种对象的按事件排序的行为,这些对象包括类(各主动类)、接口、构件和节点。
当对系统、类或用例的动态方面建模时,通常是对反应型对象建模。
一个反应型或事件驱动的对象是这样一个对象,其行为通常是由对来自语境外部的事件做出反应来刻画的。反应型对象在接收到一个事件之前通常处于空闲状态。当它接收到一个事件时,它的反应常常依赖于以前的事件。在这个对象对事件做出反应后,它就又变成闲状态,等待下一个事件。对于这种对象,将着眼于对象的稳定状态、能够触发从状态到状态的转换的事件,以及当每个状态改变时所发生的动作。
6)活动图
活动图(activity diagram)是一种特殊的状态图,它展现了在系统内从一个活动到另一个活动的流程。活动图专注于系统的动态视图,它对于系统的功能建模特别重要,并强调对象间的控制流程。
活动图一般包括活动状态和动作状态、转换和对象。
用活动图建模的控制流中,会发生一些事情。可能要对一个设置属性值或返回一些值的表达式求值;也可能要调用对象上的操作,发送一个消息给对象,甚至创建或销毁对象,这些可执行的原子计算被称作动作状态,因为它们是该系统的状态,每个原子计算都代表一个动作的执行。动作状态不能被分解。动作状态是原子的,也就是说事件可以发生,但动作状态的工作不能被中断。最后,动作状态的工作所占用的执行时间一般被看作是可忽略的。
活动状态能够进一步被分解,它们的活动由其他的活动图表示。活动状态不是原子的,它们可以被中断。并且,一般来说,还要考虑到它需要花费一段时间来完成。可以把一个动作状态看作一个活动状态的特例。类似地,可以把一个活动状态看作一个组合,它的控制流由其他的活动状态和动作状态组成。
活动图可以表示分支和汇合。
当对一个系统的动态方面建模时,通常有两种使用活动图的方式。
(1)对工作流建模。此时所关注的是与系统进行协作的参与者所观察到的活动。工作流常常位于软件系统的边缘,用于可视化、详述、构造和文档化开发系统所涉及的业务过程。在活动图的这种用法中,对对象流的建模是特别重要的。
(2)对操作建模。此时是把活动图作为流程图使用,对一个计算的细节部分建模。在活动图的这种用法中,对分文、分叉和汇合状态的建模是特别重要的。用于这种方式的活动图语境包括该操作的参数和它的局部对象。
7)构件图
构件图(component diagram)展现了一组构件之间的组织和依赖。构件图专注于系统的静态实现视图。它与类图相关,通常把构件映射为一个或多个类、接口或协作。
8)部署图
部署图(deployment diagram)展现了运行处理节点以及其中构件的配置。部署图给出了体系结构的静态实施视图。它与构件图相关,通常一个节点包含一个或多个构件。
10.5 设计模式
10.5.1 设计模式的要素
“每一个模式描述了一个在我们周围不断重复发生的问题,以及该问题的解决方案的核心。这样,你就能一次又一次地使用该方案而不必做重复劳动”。设计模式的核心在于提供了相关问题的解决方案。
设计模式一般有如下4个要素。
(1)模式名称(pattern name)。一个助记名,它用一两个词来描述模式的问题、解决方案和效果。命名一个新的模式增加了设计词汇。设计模式允许在较高的抽象层次上进行设计。基于一个模式词汇表,就可以讨论模式并在编写文档时使用它们。模式名可以帮助人们思考,便于人们与其他人交流设计思想及设计结果。找到恰当的模式名也是设计模式工作的难点之一。
(2)问题(problem)。描述了应该在何时使用模式。它解释了设计问题和问题存在的前因后果,可能描述了特定的设计问题,如怎样用对象表示算法等;也可能描述了导致不灵活设计的类或对象结构。有时候,问题部分会包括使用模式必须满足的一系列先决条件。
(3)解决方案(solution)。描述了设计的组成成分,它们之间的相互关系及各自的职责和协作方式。因为模式就像一个模板,可应用于多种不同场合,所以解决方案并不描述一个特定而具体的设计或实现,而是提供设计问题的抽象描述和怎样用一个具有一般意义的元素组合(类或对象组合)来解决这个问题。
(4)效果(consequences)。描述了模式应用的效果及使用模式应权衡的问题。尽管描述设计决策时,并不总提到模式效果,但它们对于评价设计选择和理解使用模式的代价及好处具有重要意义。软件效果大多关注对时间和空间的衡量,它们也表述了语言和实现问题。因为复用是面向对象设计的要素之一,所以模式效果包括它对系统的灵活性、扩充性或可移植性的影响,显式地列出这些效果对理解和评价这些模式很有帮助。
设计模式确定了所包含的类和实例,它们的角色、协作方式以及职责分配。每一个设计模式都集中于一个特定的面向对象设计问题或设计要点,描述了什么时候使用它,在另一些设计约束条件下是否还能使用,以及使用的效果和如何取舍。按照设计模式的目的可以分为三大类,如下表所示。
创建型模式与对象的创建有关;结构型模式处理类或对象的组合;行为型模式对类或对象怎样交互和怎样分配职责进行描述。
10.5.2 创建型设计模式
创建型模式抽象了实例化过程,它们帮助一个系统独立于如何创建、组合和表示它的那些对象。一个类创建型模式使用继承改变被实例化的类,而一个对象创建型模式将实例化委托给另一个对象。
随着系统演化得越来越依赖于对象复合而不是类继承,创建型模式变得更为重要。当这种情况发生时,重心从对一组固定行为的硬编码(hard-coding)转移为定义一个较小的基本行为集,这些行为可以被组合成任意数目的更复杂的行为。这样创建有特定行为的对象要求的不仅仅是实例化一个类。
在这些模式中有两个不断出现的主旋律。第一,它们都将关于该系统使用哪些具体的类的信息封装起来。第二,它们隐藏了这些类的实例是如何被创建和放在一起的。整个系统关于这些对象所知道的是由抽象类所定义的接口。因此,创建型模式在什么被创建,谁创建它,它是怎样被创建的,以及何时创建这些方面给予了很大的灵活性。它们允许用结构和功能差别很大的“产品”对象配置一个系统。配置可以是静态的(即在编译时指定),也可以是动态的(在运行时)。
【例10.8】 Singleton模式。
通常情况下,用户可以对应用系统进行配置,并将配置信息保存在配置文件中。应用系统在启动时首先将配置文件加载到内存中,这些内存配置信息应该有且仅有一份。应用单身模式(Singleton)以保证Configure类只能有一个实例。这样,Configure类的使用者无法定义该类的多个实例,否则会产生编译错误。
程序如下:
10.5.3 结构型设计模式
结构型模式涉及到如何组合类和对象以获得更大的结构。结构型类模式采用继承机制来组合接口或实现。一个简单的例子是采用多重继承方法将两个以上的类组合成一个类,结果这个类包含了所有父类的性质。这一模式尤其有助于多个独立开发的类库协同工作。其中一个例子是类形式的Adapter模式。一般来说,适配器使得一个接口与其他接口兼容,从而给出了多个不同接口的统一抽象。为此,类适配器对一个adaptee类进行私有继承。这样,适配器就可以用adaptee的接口表示它的接口。
结构型对象模式不是对接口和实现进行组合,而是描述了如何对一些对象进行组合,从而实现新功能的一些方法。因为可以在运行时刻改变对象组合关系,所以对象组合方式具有更大的灵活性,而这种机制用静态类组合是不可能实现的。
Composite模式是结构型对象模式的一个实例。它描述了如何构造一个类层次式结构,这一结构由两种类型的对象所对应的类构成。其中的组合对象使得用户可以组合基元对象以及其他的组合对象,从而形成任意复杂的结构。在Proxy模式中,proxy对象作为其他对象的一个方便的替代或占位符。它的使用可以有多种形式,例如可以在局部空间中代表一个远程地址空间中的对象,也可以表示一个要求被加载的较大的对象,还可以用来保护对敏感对象的访问。Proxy模式还提供了对对象的一些特有性质的一定程度上的间接访问,从而可以限制、增强或修改这些性质。Flyweight模式为了共享对象定义了一个结构。至少有两个原因要求对象共享:效率和一致性。Flyweight的对象共享机制主要强调对象的空间效率。使用很多对象的应用必须考虑每一个对象的开销。使用对象共享而不是进行对象复制,可以节省大量的空间资源。但是,仅当这些对象没有定义与上下文相关的状态时,它们才可以被共享。Flyweight的对象没有这样的状态。任何执行任务时需要的其他一些信息仅当需要时才传递过去。由于不存在与上下文相关的状态,因此Flyweight对象可以被自由地共享。
如果说Flyweight模式说明了如何生成很多较小的对象,那么Facade模式则描述了如何用单个对象表示整个子系统。模式中的facade用来表示一组对象,facade的职责是将消息转发给它所表示的对象。Bridge模式将对象的抽象和其实现分离,从而可以独立地改变它们。
Decorator模式描述了如何动态地为对象添加职责。Decorator模式是一种结构型模式,这一模式采用递归方式组合对象,从而允许添加任意多的对象职责。例如,一个包含用户界面组件的Decorator对象可以将边框或阴影这样的装饰添加到该组件中,或者它可以将窗口滚动和缩放这样的功能添加到组件中。可以将一个Decorator对象嵌套在另外一个对象中,就可以很简单地增加两个装饰,添加其他的装饰也是如此。因此,每个Decorator对象必须与其组件的接口兼容并且保证将消息传递给它。Decorator模式在转发一条信息之前或之后都可以完成它的工作(例如绘制组件的边框)。许多结构型模式在某种程度上具有相关性。
10.5.4 行为设计模式
行为模式涉及到算法和对象间职责的分配。行为模式不仅描述对象或类的模式,还描述它们之间的通信模式。这些模式刻画了在运行时难以跟踪的复杂的控制流。它们将用户的注意力从控制流转移到对象间的联系方式上来。
行为类模式使用继承机制在类间分派行为。本章包括两个这样的模式,其中Templa teMethod较为简单和常用。模板方法是一个算法的抽象定义,它逐步地定义该算法,每一步调用一个抽象操作或一个原语操作,子类定义抽象操作以具体实现该算法。另一种行为类模式是Interpreter,它将一个文法表示为一个类层次,并实现一个解释器作为这些类的实例上的一个操作。
行为对象模式使用对象复合而不是继承。一些行为对象模式描述了一组对等的对象怎样相互协作以完成其中任一个对象都无法单独完成的任务。这里一个重要的问题是对等的对象。
如何互相了解对方。对等对象可以保持显式的对对方的引用,但那会增加它们的耦合度。在极端情况下,每一个对象都要了解所有其他的对象。Mediator在对等对象间引入一个mediator对象以避免这种情况的出现。mediator提供了松耦合所需的间接性。
Chain of Responsibility提供更松的耦合。它让用户通过一条候选对象链隐式地向一个对象发送请求。根据运行时刻情况任一候选者都可以响应相应的请求。候选者的数目是任意的,可以在运行时刻决定哪些候选者参与到链中。
Observer模式定义并保持对象间的依赖关系。典型的Observer的例子是Smalltalk中的模型/视图/控制器,其中一旦模型的状态发生变化,模型的所有视图都会得到通知。
其他的行为对象模式常将行为封装在一个对象中并将请求指派给它。Strategy模式将算法封装在对象中,这样可以方便地指定和改变一个对象所使用的算法。Command模式将请求封装在对象中,这样它就可作为参数来传递,也可以被存储在历史列表里,或者以其他方式使用。State模式封装一个对象的状态,使得当这个对象的状态对象变化时,该对象可改变它的行为。Visitor封装分布于多个类之间的行为,而Iterator则抽象了访问和遍历一个集合中的对象的方式。
【例10.9】Observer模式。
在一公文处理系统中,开发者定义了一个公文类OfficeDoc,其中定义了公文具有的属性和处理公文的相应方法。当公文的内容或状态发生变化时,关注此OfficeDoc类对象的相应的DocExplorer对象都要更新其自身的状态。一个OfficeDoc对象能够关联一组DocExplorer对象。当OfficeDoc对象的内容或状态发生变化时,所有与之相关联的DocExplorer对象都将得到通知,这种应用被称为观察者模式。
程序如下: