第九章 面向对象实现
面向对象实现主要包括两项工作:
1.把面向对象设计结果,翻译成用某种
程序语言书写的面向对象程序;
2.测试并调试面向对象的程序。
面向对象编程
面向对象测试
第一节 面向对象编程
一、非面向对象的语言与面向对象的语
言
面向对象设计的结果,既可以用面向对
象语言,也可以用非面向对象语言实现。使
用非面向对象语言编写面向对象程序,必须
由程序员自己把面向对象概念映射到目标程
序中。
例如,C语言本身并不直接支持类或对象
的概念,程序员只能利用结构( struct)定
义变量,由于不能直接在结构中定义函数,
因此必须利用指针间接定义相应的函数。另
外,所有非面向对象语言都不支持继承的实
现,使用这类语言编程时,要么完全回避继
承的概念,要么在声明特殊化类时,把对一
般化类的引用嵌套在它里面。
面向对象语言充分支持面向对象概念的
实现。从问题域到面向对象分析模型,从面
向对象模型到面向对象设计模型,最后从面
向对象设计模型到面向对象编程都具有一致
的表示方法。一致的表示方法既有利于在软
件开发过程中始终使用统一的概念,也有利
于维护人员理解软件的各种配臵成分。
二、面向对象语言选择
80年代以来,面向对象语言像雨后春笋
一样大量涌现,形成了两大类面向对象语言,
一类是纯面向对象语言,如 Smalltalk和
Eiffel等语言,着重支持面向对象方法研究
和快速原型的实现;另一类是混合型面向对
象语言,也就是在过程语言的基础上增加面
向对象机制,如 C++等语言,它的目标是提高
运行速度和使传统程序员容易接受面向对象
思想。
下面介绍几种典型的面向对象语言和选
择面向对象语言时应着重考察的一些技术特
点。下表是各种面向对象语言中使用的术语
对比。
面向对象
概念
Smalltalk-80
术语
C++
术语
Objective-C
术语
Eiffel
术语
对象 对象 对象 对象 对象
类 类 类 对象工厂 对象工厂
方法 方法 成员函数 方法 例程
属性 实例变量 数据成员 实例变量 属性
消息 消息 函数调用 消息表达式 例程施用
子类 子类 派生类 子类 后代
继承性 继承性 派生 继承性 继承性
(一)几种典型的面向对象语言
1.Smalltalk Smalltalk起源可追溯到
20世纪 60年代后期,由美国的 Xerox公司 Palo
Alto研究中心( PARC)开发。 Smalltalk语言
在 Xerox PARC经过了多次重大修改,最终形
成了 Smalltalk-80版本。 Smalltalk-80全面
支持面向对象的概念,从上表可以看出二者
基本一致,表明了这种语言对 OOP的深刻影响。
除了概念上的影响作用外,Smalltalk-
80对 OOP的其它主要贡献是构成其开发环境的
两个要素:窗口化的程序设计工具和类库。
Smalltalk-80实际上与开发环境不可分离。
这个环境同样是用类和对象实现的,提供了
一组对象管理功能,具有多窗口、图形化的
用户界面和一组程序设计工具。
在这些工具的支持下,程序中的类、消
息和方法的实现都可以在不同的窗口中联机
地设计、实现、浏览和调试。 Smalltalk-80
是最早引入类库的语言。 Smalltalk-80的类
库实际上就是语言的核,连语言的(伪)编
译程序、排错程序、编辑程序,也是基于类
库中的类实现的。用这种语言进行程序设计,
首先要了解并会使用它的类库。
Smalltalk-80的缺点是不支持强类型化,
是一种弱类型化语言,程序中不作变量的类
型说明,系统也不进行类型检查,支持完全
的动态联编机制(在发送消息前,无须知道
接受消息的对象当时属于哪个类,只有在运
行时才进行识别)。 Smalltalk-80的另一个
缺点时执行效率较低。
2.C++ C++语言是 C语言的一个超集,由
AT&T的 Bell实验室于 1986年推出,C++基于 C
语言的特点,既易于为广大的 C语言程序员所
接受,又可以充分利用长期积累的大量 C语言
例程和应用,已被国外许多主要的计算机和
软件生产企业选为替代 C语言或与 C语言并存
的基本开发工具。
C++支持基本的面向对象概念。 C++中的
类可以由用户自定义,用户自定义类型与系
统内在类型在程序中的地位与用法完全相同。
因此,C++中的类是用户对类型系统的扩充,
受到编译系统同样的类型检查处理。 C++中的
对象生成方法与申述一个变量的方式也完全
相同。因此,C++也体现了结构化程序设计的
基本风格。另外,C++的运行速度明显高于
Smalltalk-80。
3.Objective-C Objective-C语言也是 C
语言的一个超集。它是 B,Cox为 Stepstone公
司开发的另一种基于 C语言的 OOPL。它是将
Smalltalk结构加入 C语言后于 1983年形成的,
也完全支持面向对象的基本概念。
在 Objective-C中,每一个类的定义由两
个文件构成,即接口文件和实现文件。
Objective-C比起 C++来,受 Smalltalk的影响
要深得多。它的一个主要优点也来自这里,
即有比较丰富的类库。这种语言的主要弱点
是:它是一种弱类型化系统。
4.Eiffel Eiffel语言由美国
Interactive Software Engineering公司
Bertrand Meyer开发,1986年推出 1,0版本,
1991年 7月推出完整的 3,0版本。 Eiffel是完
全根据面向对象程序设计思想设计出来的纯
面向对象语言,该语言推出后备受程序设计
理论界推崇和欢迎。然而由于实现效率与开
发环境等原因,Eiffel语言的实际应用与开
发远不及 C++语言广泛。
5.Java Java是一种适合于分布式计算
的新型面向对象程序设计语言,由美国 Sun
Microsystem公司于 1990年开始设计,是目前
推广最快的程序设计语言。 Java语言可以看
作是 C++的派生语言,它从 C++语言中继承了
大量的语言成分,但抛弃了 C++中冗余的、容
易引起问题的功能(如头文件、编译指令、
指针、结构、隐式类型转换、操作符重载
等),增加了多线程、异常处理、网络程序
设计等方面的支持。 Java包含以下新特性:
(1)平台无关性。 Java编译程序将
Java源程序编译为字节码,字节码十分类似
于机器指令,但又不是为某个特定的机器定
义,因此一般不能在某个具体的平台上执行,
而需要由 Java运行系统中的解释程序执行。
此外,Java语言为了做到结构中立,还制订
了完全统一的语言文本,如基本数据类型不
随机器字长变化,整数类型总是 32位,长整
数类型总是 64位等。
( 2)支持语言级多线程:永久对象与并
发性是 C++语言尚未涉及的问题。 Java程序中
的多线程就是对并发处理的支持,它采用的
同步机制是管程和临界区保护。
( 3)垃圾自动收集机制:内存管理是
C++程序员比较麻烦且容易出错的工作。 Java
提供了垃圾自动收集机制,程序员不再需要
关心内存管理的问题,Java系统自动收回不
再使用的内存。
( 4)稳定性与安全性,Java语言引入了
内存保护机制,取消了指针操作,从而消除
了破坏内存单元有用数据的可能性。一旦产
生错误,可使用类似 C++语言的异常处理机制
来管理。一个 Java程序运行其间的内存分配
及布局由 Java运行系统决定而不是由编译程
序决定,同时采用字节码验证功能,可确保
程序的安全性。
( 5)动态特性,Java语言的类可在运行
期间动态装载,使得 Java可在分布式环境中
动态地维护应用程序,并保持类库之间的一
致性。
Java将面向对象、平台无关性、稳定与安
全性、多线程等特性集于一身,为用户提供
了一个良好的程序设计环境,特别适合于
Internet的应用开发,与当前迅速发展的
Internet紧密结合是 Java语言成功的关键所
在。
(二)考察面向对象语言的技术特点
在种类繁多的面向对象语言中选择哪一
种呢?在实际选择时考虑以下的技术特点:
1.类与对象占有内存的管理机制 所有
面向对象语言都允许用户动态创建对象,这
就意味着系统必须处理内存管理问题,如果
不及时释放不再需要的对象所占有的内存,
动态存储分配就有可能耗尽内存。有两种管
理内存的方法,一种是由语言的运行机制自
动管理内存,即自动回收垃圾的机制,如
Java;另一种是由程序员编写释放内存的代
码。
自动管理内存不仅方便而且安全,但是
必须采用先进的垃圾收集算法才能减少开销。
某些面向对象语言(如 C++)允许程序员定义
析构函数( destructor),每当一个对象超
出范围或被显式删除时,就自动调用析构函
数。这种机制使得程序员能够方便地构造和
唤醒释放内存的操作,却又不是垃圾收集机
制。
2.实现组合结构的机制 一般说来,可
以用指针或独立的关联对象实现组合结构。
大多数现有的面向对象语言并不显式支持独
立的关联对象,在这种情况下,通过增加内
部指针可以方便地实现组合。如图 (a)中是
,银行储蓄系统, 中, 分行, 与, 储蓄所,
之间的组合结构。可以通过在, 储蓄所, 类
中增加一个指针类型的属性,使某个, 储蓄
所, 实例通过指针能找到它所属的某个, 分
行, 实例(见图 (b))。
储蓄
所
分
行11
+
储蓄
所
分行
号
分
行
(a) (b)
3.实现归纳结构的机制 面向对象语言
一般通过继承实现归纳结构,有的语言只提
供单继承机制,有的有提供多继承机制。
4.实现属性和服务的机制 对于属性实
现机制应该着重考虑以下几个方面:是否提
供实例连接的机制;是否有属性的可见性控
制;对属性值是否有约束机制。对于服务应
该着重考虑以下几个方面:是否有支持消息
连接的机制;是否有控制服务可见性的机制;
是否具有动态联编的机制。
动态联编是实现面向对象程序运行多态
性的基础。所谓多态性是指一个服务名在不
同情况下对应不同的方法(方法指服务的实
现)。它分为静态和动态两种。静态多态性
是指定义在一个类中的同名服务,但参数表
(参数类型及个数)不同,在编译时就可根
据参数确定该服务所对应的方法,称为静态
联编。动态多态性是指一个类继承层次中不
同类中的服务,它们不仅服务名相同,参数
表也相同,必须在运行中根据对象所属的类
才能确定它对应的方法,称为动态联编
动态联编可以使软件人员在发送消息时
具有较大的灵活性,在发送消息前,无须知
道接受该消息的对象属于哪个类。
5.类型检查 程序设计语言可以按照编
译时进行类型检查的严格程度来分类。如果
语言仅要求每个变量或属性隶属于一个对象,
则是弱类型的;如果语法规定每个变量或属
性必须准确地属于某个特定的类,则这样的
语言是强类型的。面向对象语言在这方面差
异很大,例如,Smalltalk是一种无类型语言
(所有对象都是未制定类的对象); C++和
Eiffel是强类型语言。强类型语言有利于在
编译时发现程序错误,增加了优化的可能性,
有助于提高软件的可靠性和运行效率;弱类
型语言主要用于快速开发原型。
6.类库和开发环境 大多数的面向对象
语言都提供一个实用的类库,类库中往往包
含实现通用数据结构(例如动态数组、表、
队列、栈、树等)的类,更完整的类库还提
供独立于具体设备的接口类(例如输入、输
出流),以及用于实现窗口系统的用户界面
类。存在类库,许多软构件就不必由程序员
重头编写了,这为实现软件重用带来很大方
便。
考虑类库时,不仅应该考虑是否提供了
类库,还应该考虑类库中提供了那些有价值
的类。为便于积累可重用的类和重用已有的
类,要考虑开发环境中除了提供编辑程序、
编译程序或解释程序、浏览工具、调试器等
最基本的软件工具,是否提供使用方便的类
库编辑工具和浏览工具。
7.效率 考虑到软件的性能时,语言的
执行效率是非常重要的方面。解释型的面向
对象语言的执行效率不如编译型的语言。如
果类库中提供了更高效的算法和更好的数据
结构(例如程序员已经无须编写实现哈希表
或平衡树算法的代码了,类库中已经提供了
这类数据结构,而且算法先进、代码精巧可
靠),那么重用这类代码不仅提高开发效率,
优化之后,有时能得到比使用非面向对象语
言运行更快的代码。
8.持久保存对象 有时希望对象中的数
据能长期保存,例如, 储户, 的信息。有的
语言如 Eiffel语言在类库中增加对象存储功
能,可以从, 可存储的类, 中派生出需要长
期保存的对象,该对象自然继承了对象存储
管理功能;有的语言如 Smalltalk把对象当前
的执行状态完整地保存在磁盘上,提供了能
访问磁盘对象的输入输出操作;有的语言如
C++没有提供直接存储对象的机制,用户必须
自己管理对象的输入、输出,或者购买面向
对象的数据库管理机制。
9.参数化类 在实际应用程序中,常常
看到这样一些软件元素,从它们的逻辑功能
看,彼此是相同的,所不同的主要是处理的
对象类型不同。例如,对于一个向量(一维
数组)类来说,不论是整型向量、浮点型向
量还是其它任何类型的向量,针对它的数据
元素所进行的基本操作都是相同的,但不同
向量的数据元素的类型不同。如果程序语言
提高一种能抽象出这类共性的机制,则对减
少冗余和提高可重用性是大有好处的。
除了上述的因素,还应该考虑的其它因
素有:在若干年以后,哪种面向对象语言将
占主导地位,开发人员对语言的熟悉程度,
对用户学习语言所能提供的培训服务等。
总之,选择一种合适的面向对象语言,
要综合考虑各方面的因素。
三、面向对象概念的编程实现
我们已经介绍了面向对象的基本概念,
面向对象语言完全支持这些概念。这里,以
C++为例,介绍面向对象语言如何实现这些概
念。
1.类及实例
C++中,用关键字 class定义类,类在语
法上类似于结构( struct),但 struct一般
只包含数据的定义,而 class中包含了数据
(属性)和使用该数据的函数(服务)的定
义。下图类, person”,在 C++中的定义方式,
类中包含两个部分:
(1)类的说明 类说明包括两个部分:
1)公共接口的规格说明 用 public标识
该部分,描述该类的对象与外部的接口的规
格说明。其中有一个与类同名的是对象构造
函数,用来说明如何创建对象。一旦定义了
类,便可如图所示建立该类的对象。
2)私有部分说明 用 private标识该部
分,描述该类的对象内部使用的数据(属性)
和函数的规格说明。
(2)类的实现 类中包括的函数的具体实
现。
//类的说明, 包括两个部分:公共接口说明和私有部分说
明
class person{
public,//公共接口的规格说明
person(char s,int t,int w)//对象构造函数
void answer_tall( )//回答身高
void answer_weight( )//回答体重
private,//私有部分说明
char sex;//性别
int tall;//身高
int weight;//体重
}
//类的实现
person:,person(char s,int t,int w)//对象构造函数
{sex=s;tall=t;weight=w;}
void person:,answer_tall( )//回答身高
{cout<<”tall:”<<tall<<endl;}
void person:,answer_weight( )//回答体重
{ cout<<”weight:”<<weight<<endl;}
};
//声明对象, 张三,,, 李四,,, 王
五,
person ZHANG(‘m’,170,75);
person LI(‘f’,165,55);
person WANG(‘m’,175,90);
2.消息
C++中类的 public中描述了属于该类的实
例所能接收的消息及消息格式。例如询问
ZHANG的身高,在程序中可用如下的语句实现:
ZHANG.answer_tall( );
ZHANG接到消息就执行它的函数
,answer_tall”来响应。如果其它对象向
ZHANG发送一条消息, what’s your name”,
ZHANG不会采取任何行动,因为他不具有该操
作,因而不理解这消息。
通常完整的消息由下述三个部分组成:
接收消息的对象、消息选择符(也称为消息
名)、零个或多个变元。例如,Mycircle是
一个半径 4cm、圆心位于( 100,200)的
Circle类的对象,Show是 Circle类中定义的
一个操作。当要求 Mycircle以绿颜色在屏幕
上显示时,在 C++语言中应该向它发下列消息:
Mycircle.Show(GREEN);
其中,Mycircle是接收消息的对象名字,
Show是消息名,圆括号内的 GREEN是消息的变
元。当 Mycircle接收到这个消息后,将执行
在 Circle类中所定义的 Show操作。
3.封装
C++将属性( C++中称为, 数据成员, )、
操作( C++中称为, 成员函数, )封装在
class中,其它用户不必知道对象属性和方法
的实际细节,只需利用 class中的 public部分
提供的消息格式来访问该对象。
4.继承性
C++允许在既有类的基础上定义新的类,
而不需把既有类的内容重新书写一遍。既有
类称为基类或父类,在它基础上建立的类称
为派生类或子类。例第八章中的派生类
,teacher”,,student”类在既有类, person”
的基础上的说明如下:
//派生类的说明
class teacher,public person{
public:
void teach_what( );
private:
char course[10];
};
class student,public person{
public:
void which_grade( );
private:
int grade;
};
第二节 面向对象测试
一, 测试策略
面向对象软件测试的目标与结构化软件
测试的目标相同,都是为了找出软件开发中
的错误,提高软件的质量。结构化软件的测
试策略是从组成系统的最小单元 —— 模块开
始进行测试,然后逐步集成进行小系统测试、
系统测试,最后在用户的参与下进行验收测
试。
面向对象软件的测试策略也是从组成系
统的最小单元开始,对象是面向对象软件中
的最小单元,所以首先进行对象测试,然后
对组成子系统的各个对象之间的协同关系进
行测试,最后是整个系统的测试。
面向对象软件的测试策略与上述策略基
本相同,但也有许多新特点。
1.面向对象的单元测试
类(对象)是组成面向对象软件系统的
最小单元,所以首先进行类测试。类是属性
与服务的封装体,它与传统的一个入口一个
出口的模块测试完全不同。类的测试包含两
个步骤:单个服务的实现体 —— 方法的测试、
各个方法之间协作的测试。
(1)单个方法的测试 对象靠消息的接收
执行相应的方法,传统针对模块的设计测试
用例的技术例如逻辑覆盖、等价划分、边界
值分析和错误推测等仍然可以使用。方法测
试中有两个方面要加以注意:
首先,方法执行的结果并不一定返回调
用者,有的可能是改变被测对象的某个属性。
由于对象的封装性,被测对象的属性状
态对外界是不能直接可见的,需要采取其它
的办法:例如在类中增加方法,该方法的功
能是显示该对象当前的属性状态,然后在被
测方法执行前后分别发送消息获得被测对象
的属性状态进行比较。
其次,除了类中自己定义的方法,还可
能存在从基类继承来的方法,这些方法虽然
在基类中已经测试过,在派生类往往需要再
次测试。例如类 CL2是类 CL1的派生类,类 CL3
是类 CL2的派生类。其中
类 CL1:包含方法 f1,f2,但方法 f1中使
用了 f2;
类 CL2:重载方法 f1,继承方法 f2,但方
法 f1中仍使用了 f2;
类 CL3:继承方法 f1,重载方法 f2。
类 CL3中方法 f1尽管是从基类 CL2中不变
继承来的,但 f1使用了 f2,该方法在 CL2实例
背景下被调用时,和该方法在 CL3实例背景下
被调用时,实际所执行的 f2代码是不一样的。
所以在测试派生类 CL3时,继承来的方法 f1还
应该重新测试。
(2)方法间协作的测试 面向对象中,在
保证单个方法功能正确的基础上,还应该测
试方法之间的协作关系。方法被封装在对象
类中,对象彼此间通过发送消息启动相应的
方法,但是,对象并没有明显地规定用什么
次序启动它的操作才是合法的。这时,对象
就像一个有多个入口的模块,因此,必须测
试方法的不同次序组合的情况。
2.面向对象的集成测试
因为在面向对象的软件中不存在层次的
控制结构,传统的自顶向下和自底向上的集
成策略就没有意义了。面向对象软件的集成
测试有两种不同的策略。
①是基于线程的测试( thread-based
testing),这种策略把响应系统的一个输入
或一个事件所需要的一组类集成起来。分别
集成并测试每个线程,同时应用回归测试以
保证没有产生副作用。
② 基于使用的测试 (use-based
testing),这种测试首先测试几乎不使用服
务器类的那些类(称为独立类),把独立类
都测试完之后,接下来测试使用独立类的下
一个层次的类(成为依赖类)。对依赖类的
测试一个层次一个层次地持续进行下去,直
至把整个软件系统构造完为止。
集群测试( cluster testing)是面向对
象软件测试的一个步骤。在这个测试步骤中,
用精心设计的测试用例检查一群相互协作的
类(通过研究对象模型可以确定协作类),
这些测试用例力图发现协作错误。
3,面向对象的确认测试
在确认测试层次,不再考虑类之间相互
连接的细节。和传统的确认测试一样,面向
对象软件的确认测试也集中检查用户可见的
动作和用户可识别的输出。为了导出确认测
试用例,测试人员应该认真研究动态模型和
描述系统行为的脚本,以确定最可能发现用
户交互需求错误的情景。
二、测试用例的设计
1.测试类的测试用例设计
与传统的单元测试的测试用例设计的关
注点(传统单元测试关注算法细节)不同,
由于类中单个方法一般由两三个语句构成,
测试比较简单,所以类测试用例的设计关注
于设计适当的操作序列以检查类中方法的协
作。主要的方法有:随机测试、划分测试和
基于故障的测试等三种。
(1)随机测试 以银行应用系统为例,
简要说明这种测试方法。
该系统的 account(帐户)类有如下操作:
open(打开),setup(建立),deposit
(存款),withdraw(取款),balance(余
额),summarize(清单),creditLimit
(透支限额),close(关闭)。
上列每个操作都可以应用于 account类的
实例,但是,该系统的性质也对操作的应用
施加了一些限制,例如,必须在应用其它操
作之前先打开帐户,在完成了全部操作之后
才能关闭帐户。即使有这些限制,可做的操
作也有许多种排列方法。
一个 account类实例的最小行为历史包
括下列操作:
open﹒ setup﹒ deposit﹒ withdraw﹒ close
这就是对 account类的最小测试序列。
但是,在下面的序列中可能发生许多其它行
为:
open﹒ setup﹒ deposit﹒ [ deposit | withdraw | balance |
summarize | creditLimit ]﹒ withdraw﹒ close
从上列序列可以随机地产生一系列不同
的操作序列,例如:
测试用例 # r1:
open﹒ setup﹒ deposit﹒ deposit﹒ bala
nce ﹒ summarize﹒ withdraw﹒ close
测试用例 # r2:
open﹒ setup﹒ deposit﹒ withdraw﹒ dep
osit﹒ balance﹒ creditLimit﹒ withdraw﹒ c
lose
执行上述这些及另外一些随机产生的测
试用例,可以测试类实例的不同生存历史。
( 2) 划分测试 与测试传统时采用
等价划分方法类似,划分测试方法可以
减少测试类时所需要的测试用例的数量。
介绍划分的方法有如下几种:
1) 基于状态的划分 这种方法根据
类操作改变状态的能力来划分操作。例
如 account类中状态操作有,deposit和
withdraw,非状态操作有 balance、
summarize和 creditLimit。
可以设计出如下的测试用例:
测试用例 # p1:
open﹒ setup﹒ deposit﹒ deposit﹒ with
draw ﹒ withdraw﹒ close
测试用例 # p2:
open﹒ setup﹒ deposit﹒ summarize﹒
creditLimit﹒ withdraw﹒ close
在测试用例 # p1改变类的状态,测试用
例 # p2不改变。
2)基于属性的划分 这种方法根据类
操作使用的属性来划分类操作。例如 account
类中的根据属性 balance的使用可划分为三个
类别:
使用 balance的操作
修改 balance的操作
不使用也不修改 balance的操作。
然后为每个类别设计测试序列。
3) 基于功能的划分 这种方法根据类
操作所完成的功能来划分类操作。例如,可
以把 account类中的操作分为:
初始化操作( open,setup)
计算操作( deposit,withdraw)
查询操作( balance,summarize、
creditLimit)
终止操作( close)。
然后为每个类别设计测试序列。
( 3)基于故障的测试 基于故障的测试
与传统的错误推测法类似,首先推测软件中
可能有的错误,然后设计出最可能发现这些
错误的测试用例。
2.集成测试的测试用例设计
在集成测试阶段,必须对类间协作进
行测试,测试类协作也可以使用随机测试和
划分测试方法,以及基于情景的测试和行为
测试来完成。
生成多个类的随机测试用例,Kirani和
Tsai建议使用下列步骤:
1)对每个客户类,使用类操作符列表来
生成一系列随机测试序列。这些操作符向服
务器类实例发送消息。
2)对所生成的每个消息,确定协作类和
在服务器对象中的对应操作符。
3)对服务器对象中的每个操作符(已经
被来自客户对象的消息调用),确定传递的
消息。
4)对每个消息,确定下一层被调用的操
作符,并把这些操作符结合进测试序列中。
面向对象实现主要包括两项工作:
1.把面向对象设计结果,翻译成用某种
程序语言书写的面向对象程序;
2.测试并调试面向对象的程序。
面向对象编程
面向对象测试
第一节 面向对象编程
一、非面向对象的语言与面向对象的语
言
面向对象设计的结果,既可以用面向对
象语言,也可以用非面向对象语言实现。使
用非面向对象语言编写面向对象程序,必须
由程序员自己把面向对象概念映射到目标程
序中。
例如,C语言本身并不直接支持类或对象
的概念,程序员只能利用结构( struct)定
义变量,由于不能直接在结构中定义函数,
因此必须利用指针间接定义相应的函数。另
外,所有非面向对象语言都不支持继承的实
现,使用这类语言编程时,要么完全回避继
承的概念,要么在声明特殊化类时,把对一
般化类的引用嵌套在它里面。
面向对象语言充分支持面向对象概念的
实现。从问题域到面向对象分析模型,从面
向对象模型到面向对象设计模型,最后从面
向对象设计模型到面向对象编程都具有一致
的表示方法。一致的表示方法既有利于在软
件开发过程中始终使用统一的概念,也有利
于维护人员理解软件的各种配臵成分。
二、面向对象语言选择
80年代以来,面向对象语言像雨后春笋
一样大量涌现,形成了两大类面向对象语言,
一类是纯面向对象语言,如 Smalltalk和
Eiffel等语言,着重支持面向对象方法研究
和快速原型的实现;另一类是混合型面向对
象语言,也就是在过程语言的基础上增加面
向对象机制,如 C++等语言,它的目标是提高
运行速度和使传统程序员容易接受面向对象
思想。
下面介绍几种典型的面向对象语言和选
择面向对象语言时应着重考察的一些技术特
点。下表是各种面向对象语言中使用的术语
对比。
面向对象
概念
Smalltalk-80
术语
C++
术语
Objective-C
术语
Eiffel
术语
对象 对象 对象 对象 对象
类 类 类 对象工厂 对象工厂
方法 方法 成员函数 方法 例程
属性 实例变量 数据成员 实例变量 属性
消息 消息 函数调用 消息表达式 例程施用
子类 子类 派生类 子类 后代
继承性 继承性 派生 继承性 继承性
(一)几种典型的面向对象语言
1.Smalltalk Smalltalk起源可追溯到
20世纪 60年代后期,由美国的 Xerox公司 Palo
Alto研究中心( PARC)开发。 Smalltalk语言
在 Xerox PARC经过了多次重大修改,最终形
成了 Smalltalk-80版本。 Smalltalk-80全面
支持面向对象的概念,从上表可以看出二者
基本一致,表明了这种语言对 OOP的深刻影响。
除了概念上的影响作用外,Smalltalk-
80对 OOP的其它主要贡献是构成其开发环境的
两个要素:窗口化的程序设计工具和类库。
Smalltalk-80实际上与开发环境不可分离。
这个环境同样是用类和对象实现的,提供了
一组对象管理功能,具有多窗口、图形化的
用户界面和一组程序设计工具。
在这些工具的支持下,程序中的类、消
息和方法的实现都可以在不同的窗口中联机
地设计、实现、浏览和调试。 Smalltalk-80
是最早引入类库的语言。 Smalltalk-80的类
库实际上就是语言的核,连语言的(伪)编
译程序、排错程序、编辑程序,也是基于类
库中的类实现的。用这种语言进行程序设计,
首先要了解并会使用它的类库。
Smalltalk-80的缺点是不支持强类型化,
是一种弱类型化语言,程序中不作变量的类
型说明,系统也不进行类型检查,支持完全
的动态联编机制(在发送消息前,无须知道
接受消息的对象当时属于哪个类,只有在运
行时才进行识别)。 Smalltalk-80的另一个
缺点时执行效率较低。
2.C++ C++语言是 C语言的一个超集,由
AT&T的 Bell实验室于 1986年推出,C++基于 C
语言的特点,既易于为广大的 C语言程序员所
接受,又可以充分利用长期积累的大量 C语言
例程和应用,已被国外许多主要的计算机和
软件生产企业选为替代 C语言或与 C语言并存
的基本开发工具。
C++支持基本的面向对象概念。 C++中的
类可以由用户自定义,用户自定义类型与系
统内在类型在程序中的地位与用法完全相同。
因此,C++中的类是用户对类型系统的扩充,
受到编译系统同样的类型检查处理。 C++中的
对象生成方法与申述一个变量的方式也完全
相同。因此,C++也体现了结构化程序设计的
基本风格。另外,C++的运行速度明显高于
Smalltalk-80。
3.Objective-C Objective-C语言也是 C
语言的一个超集。它是 B,Cox为 Stepstone公
司开发的另一种基于 C语言的 OOPL。它是将
Smalltalk结构加入 C语言后于 1983年形成的,
也完全支持面向对象的基本概念。
在 Objective-C中,每一个类的定义由两
个文件构成,即接口文件和实现文件。
Objective-C比起 C++来,受 Smalltalk的影响
要深得多。它的一个主要优点也来自这里,
即有比较丰富的类库。这种语言的主要弱点
是:它是一种弱类型化系统。
4.Eiffel Eiffel语言由美国
Interactive Software Engineering公司
Bertrand Meyer开发,1986年推出 1,0版本,
1991年 7月推出完整的 3,0版本。 Eiffel是完
全根据面向对象程序设计思想设计出来的纯
面向对象语言,该语言推出后备受程序设计
理论界推崇和欢迎。然而由于实现效率与开
发环境等原因,Eiffel语言的实际应用与开
发远不及 C++语言广泛。
5.Java Java是一种适合于分布式计算
的新型面向对象程序设计语言,由美国 Sun
Microsystem公司于 1990年开始设计,是目前
推广最快的程序设计语言。 Java语言可以看
作是 C++的派生语言,它从 C++语言中继承了
大量的语言成分,但抛弃了 C++中冗余的、容
易引起问题的功能(如头文件、编译指令、
指针、结构、隐式类型转换、操作符重载
等),增加了多线程、异常处理、网络程序
设计等方面的支持。 Java包含以下新特性:
(1)平台无关性。 Java编译程序将
Java源程序编译为字节码,字节码十分类似
于机器指令,但又不是为某个特定的机器定
义,因此一般不能在某个具体的平台上执行,
而需要由 Java运行系统中的解释程序执行。
此外,Java语言为了做到结构中立,还制订
了完全统一的语言文本,如基本数据类型不
随机器字长变化,整数类型总是 32位,长整
数类型总是 64位等。
( 2)支持语言级多线程:永久对象与并
发性是 C++语言尚未涉及的问题。 Java程序中
的多线程就是对并发处理的支持,它采用的
同步机制是管程和临界区保护。
( 3)垃圾自动收集机制:内存管理是
C++程序员比较麻烦且容易出错的工作。 Java
提供了垃圾自动收集机制,程序员不再需要
关心内存管理的问题,Java系统自动收回不
再使用的内存。
( 4)稳定性与安全性,Java语言引入了
内存保护机制,取消了指针操作,从而消除
了破坏内存单元有用数据的可能性。一旦产
生错误,可使用类似 C++语言的异常处理机制
来管理。一个 Java程序运行其间的内存分配
及布局由 Java运行系统决定而不是由编译程
序决定,同时采用字节码验证功能,可确保
程序的安全性。
( 5)动态特性,Java语言的类可在运行
期间动态装载,使得 Java可在分布式环境中
动态地维护应用程序,并保持类库之间的一
致性。
Java将面向对象、平台无关性、稳定与安
全性、多线程等特性集于一身,为用户提供
了一个良好的程序设计环境,特别适合于
Internet的应用开发,与当前迅速发展的
Internet紧密结合是 Java语言成功的关键所
在。
(二)考察面向对象语言的技术特点
在种类繁多的面向对象语言中选择哪一
种呢?在实际选择时考虑以下的技术特点:
1.类与对象占有内存的管理机制 所有
面向对象语言都允许用户动态创建对象,这
就意味着系统必须处理内存管理问题,如果
不及时释放不再需要的对象所占有的内存,
动态存储分配就有可能耗尽内存。有两种管
理内存的方法,一种是由语言的运行机制自
动管理内存,即自动回收垃圾的机制,如
Java;另一种是由程序员编写释放内存的代
码。
自动管理内存不仅方便而且安全,但是
必须采用先进的垃圾收集算法才能减少开销。
某些面向对象语言(如 C++)允许程序员定义
析构函数( destructor),每当一个对象超
出范围或被显式删除时,就自动调用析构函
数。这种机制使得程序员能够方便地构造和
唤醒释放内存的操作,却又不是垃圾收集机
制。
2.实现组合结构的机制 一般说来,可
以用指针或独立的关联对象实现组合结构。
大多数现有的面向对象语言并不显式支持独
立的关联对象,在这种情况下,通过增加内
部指针可以方便地实现组合。如图 (a)中是
,银行储蓄系统, 中, 分行, 与, 储蓄所,
之间的组合结构。可以通过在, 储蓄所, 类
中增加一个指针类型的属性,使某个, 储蓄
所, 实例通过指针能找到它所属的某个, 分
行, 实例(见图 (b))。
储蓄
所
分
行11
+
储蓄
所
分行
号
分
行
(a) (b)
3.实现归纳结构的机制 面向对象语言
一般通过继承实现归纳结构,有的语言只提
供单继承机制,有的有提供多继承机制。
4.实现属性和服务的机制 对于属性实
现机制应该着重考虑以下几个方面:是否提
供实例连接的机制;是否有属性的可见性控
制;对属性值是否有约束机制。对于服务应
该着重考虑以下几个方面:是否有支持消息
连接的机制;是否有控制服务可见性的机制;
是否具有动态联编的机制。
动态联编是实现面向对象程序运行多态
性的基础。所谓多态性是指一个服务名在不
同情况下对应不同的方法(方法指服务的实
现)。它分为静态和动态两种。静态多态性
是指定义在一个类中的同名服务,但参数表
(参数类型及个数)不同,在编译时就可根
据参数确定该服务所对应的方法,称为静态
联编。动态多态性是指一个类继承层次中不
同类中的服务,它们不仅服务名相同,参数
表也相同,必须在运行中根据对象所属的类
才能确定它对应的方法,称为动态联编
动态联编可以使软件人员在发送消息时
具有较大的灵活性,在发送消息前,无须知
道接受该消息的对象属于哪个类。
5.类型检查 程序设计语言可以按照编
译时进行类型检查的严格程度来分类。如果
语言仅要求每个变量或属性隶属于一个对象,
则是弱类型的;如果语法规定每个变量或属
性必须准确地属于某个特定的类,则这样的
语言是强类型的。面向对象语言在这方面差
异很大,例如,Smalltalk是一种无类型语言
(所有对象都是未制定类的对象); C++和
Eiffel是强类型语言。强类型语言有利于在
编译时发现程序错误,增加了优化的可能性,
有助于提高软件的可靠性和运行效率;弱类
型语言主要用于快速开发原型。
6.类库和开发环境 大多数的面向对象
语言都提供一个实用的类库,类库中往往包
含实现通用数据结构(例如动态数组、表、
队列、栈、树等)的类,更完整的类库还提
供独立于具体设备的接口类(例如输入、输
出流),以及用于实现窗口系统的用户界面
类。存在类库,许多软构件就不必由程序员
重头编写了,这为实现软件重用带来很大方
便。
考虑类库时,不仅应该考虑是否提供了
类库,还应该考虑类库中提供了那些有价值
的类。为便于积累可重用的类和重用已有的
类,要考虑开发环境中除了提供编辑程序、
编译程序或解释程序、浏览工具、调试器等
最基本的软件工具,是否提供使用方便的类
库编辑工具和浏览工具。
7.效率 考虑到软件的性能时,语言的
执行效率是非常重要的方面。解释型的面向
对象语言的执行效率不如编译型的语言。如
果类库中提供了更高效的算法和更好的数据
结构(例如程序员已经无须编写实现哈希表
或平衡树算法的代码了,类库中已经提供了
这类数据结构,而且算法先进、代码精巧可
靠),那么重用这类代码不仅提高开发效率,
优化之后,有时能得到比使用非面向对象语
言运行更快的代码。
8.持久保存对象 有时希望对象中的数
据能长期保存,例如, 储户, 的信息。有的
语言如 Eiffel语言在类库中增加对象存储功
能,可以从, 可存储的类, 中派生出需要长
期保存的对象,该对象自然继承了对象存储
管理功能;有的语言如 Smalltalk把对象当前
的执行状态完整地保存在磁盘上,提供了能
访问磁盘对象的输入输出操作;有的语言如
C++没有提供直接存储对象的机制,用户必须
自己管理对象的输入、输出,或者购买面向
对象的数据库管理机制。
9.参数化类 在实际应用程序中,常常
看到这样一些软件元素,从它们的逻辑功能
看,彼此是相同的,所不同的主要是处理的
对象类型不同。例如,对于一个向量(一维
数组)类来说,不论是整型向量、浮点型向
量还是其它任何类型的向量,针对它的数据
元素所进行的基本操作都是相同的,但不同
向量的数据元素的类型不同。如果程序语言
提高一种能抽象出这类共性的机制,则对减
少冗余和提高可重用性是大有好处的。
除了上述的因素,还应该考虑的其它因
素有:在若干年以后,哪种面向对象语言将
占主导地位,开发人员对语言的熟悉程度,
对用户学习语言所能提供的培训服务等。
总之,选择一种合适的面向对象语言,
要综合考虑各方面的因素。
三、面向对象概念的编程实现
我们已经介绍了面向对象的基本概念,
面向对象语言完全支持这些概念。这里,以
C++为例,介绍面向对象语言如何实现这些概
念。
1.类及实例
C++中,用关键字 class定义类,类在语
法上类似于结构( struct),但 struct一般
只包含数据的定义,而 class中包含了数据
(属性)和使用该数据的函数(服务)的定
义。下图类, person”,在 C++中的定义方式,
类中包含两个部分:
(1)类的说明 类说明包括两个部分:
1)公共接口的规格说明 用 public标识
该部分,描述该类的对象与外部的接口的规
格说明。其中有一个与类同名的是对象构造
函数,用来说明如何创建对象。一旦定义了
类,便可如图所示建立该类的对象。
2)私有部分说明 用 private标识该部
分,描述该类的对象内部使用的数据(属性)
和函数的规格说明。
(2)类的实现 类中包括的函数的具体实
现。
//类的说明, 包括两个部分:公共接口说明和私有部分说
明
class person{
public,//公共接口的规格说明
person(char s,int t,int w)//对象构造函数
void answer_tall( )//回答身高
void answer_weight( )//回答体重
private,//私有部分说明
char sex;//性别
int tall;//身高
int weight;//体重
}
//类的实现
person:,person(char s,int t,int w)//对象构造函数
{sex=s;tall=t;weight=w;}
void person:,answer_tall( )//回答身高
{cout<<”tall:”<<tall<<endl;}
void person:,answer_weight( )//回答体重
{ cout<<”weight:”<<weight<<endl;}
};
//声明对象, 张三,,, 李四,,, 王
五,
person ZHANG(‘m’,170,75);
person LI(‘f’,165,55);
person WANG(‘m’,175,90);
2.消息
C++中类的 public中描述了属于该类的实
例所能接收的消息及消息格式。例如询问
ZHANG的身高,在程序中可用如下的语句实现:
ZHANG.answer_tall( );
ZHANG接到消息就执行它的函数
,answer_tall”来响应。如果其它对象向
ZHANG发送一条消息, what’s your name”,
ZHANG不会采取任何行动,因为他不具有该操
作,因而不理解这消息。
通常完整的消息由下述三个部分组成:
接收消息的对象、消息选择符(也称为消息
名)、零个或多个变元。例如,Mycircle是
一个半径 4cm、圆心位于( 100,200)的
Circle类的对象,Show是 Circle类中定义的
一个操作。当要求 Mycircle以绿颜色在屏幕
上显示时,在 C++语言中应该向它发下列消息:
Mycircle.Show(GREEN);
其中,Mycircle是接收消息的对象名字,
Show是消息名,圆括号内的 GREEN是消息的变
元。当 Mycircle接收到这个消息后,将执行
在 Circle类中所定义的 Show操作。
3.封装
C++将属性( C++中称为, 数据成员, )、
操作( C++中称为, 成员函数, )封装在
class中,其它用户不必知道对象属性和方法
的实际细节,只需利用 class中的 public部分
提供的消息格式来访问该对象。
4.继承性
C++允许在既有类的基础上定义新的类,
而不需把既有类的内容重新书写一遍。既有
类称为基类或父类,在它基础上建立的类称
为派生类或子类。例第八章中的派生类
,teacher”,,student”类在既有类, person”
的基础上的说明如下:
//派生类的说明
class teacher,public person{
public:
void teach_what( );
private:
char course[10];
};
class student,public person{
public:
void which_grade( );
private:
int grade;
};
第二节 面向对象测试
一, 测试策略
面向对象软件测试的目标与结构化软件
测试的目标相同,都是为了找出软件开发中
的错误,提高软件的质量。结构化软件的测
试策略是从组成系统的最小单元 —— 模块开
始进行测试,然后逐步集成进行小系统测试、
系统测试,最后在用户的参与下进行验收测
试。
面向对象软件的测试策略也是从组成系
统的最小单元开始,对象是面向对象软件中
的最小单元,所以首先进行对象测试,然后
对组成子系统的各个对象之间的协同关系进
行测试,最后是整个系统的测试。
面向对象软件的测试策略与上述策略基
本相同,但也有许多新特点。
1.面向对象的单元测试
类(对象)是组成面向对象软件系统的
最小单元,所以首先进行类测试。类是属性
与服务的封装体,它与传统的一个入口一个
出口的模块测试完全不同。类的测试包含两
个步骤:单个服务的实现体 —— 方法的测试、
各个方法之间协作的测试。
(1)单个方法的测试 对象靠消息的接收
执行相应的方法,传统针对模块的设计测试
用例的技术例如逻辑覆盖、等价划分、边界
值分析和错误推测等仍然可以使用。方法测
试中有两个方面要加以注意:
首先,方法执行的结果并不一定返回调
用者,有的可能是改变被测对象的某个属性。
由于对象的封装性,被测对象的属性状
态对外界是不能直接可见的,需要采取其它
的办法:例如在类中增加方法,该方法的功
能是显示该对象当前的属性状态,然后在被
测方法执行前后分别发送消息获得被测对象
的属性状态进行比较。
其次,除了类中自己定义的方法,还可
能存在从基类继承来的方法,这些方法虽然
在基类中已经测试过,在派生类往往需要再
次测试。例如类 CL2是类 CL1的派生类,类 CL3
是类 CL2的派生类。其中
类 CL1:包含方法 f1,f2,但方法 f1中使
用了 f2;
类 CL2:重载方法 f1,继承方法 f2,但方
法 f1中仍使用了 f2;
类 CL3:继承方法 f1,重载方法 f2。
类 CL3中方法 f1尽管是从基类 CL2中不变
继承来的,但 f1使用了 f2,该方法在 CL2实例
背景下被调用时,和该方法在 CL3实例背景下
被调用时,实际所执行的 f2代码是不一样的。
所以在测试派生类 CL3时,继承来的方法 f1还
应该重新测试。
(2)方法间协作的测试 面向对象中,在
保证单个方法功能正确的基础上,还应该测
试方法之间的协作关系。方法被封装在对象
类中,对象彼此间通过发送消息启动相应的
方法,但是,对象并没有明显地规定用什么
次序启动它的操作才是合法的。这时,对象
就像一个有多个入口的模块,因此,必须测
试方法的不同次序组合的情况。
2.面向对象的集成测试
因为在面向对象的软件中不存在层次的
控制结构,传统的自顶向下和自底向上的集
成策略就没有意义了。面向对象软件的集成
测试有两种不同的策略。
①是基于线程的测试( thread-based
testing),这种策略把响应系统的一个输入
或一个事件所需要的一组类集成起来。分别
集成并测试每个线程,同时应用回归测试以
保证没有产生副作用。
② 基于使用的测试 (use-based
testing),这种测试首先测试几乎不使用服
务器类的那些类(称为独立类),把独立类
都测试完之后,接下来测试使用独立类的下
一个层次的类(成为依赖类)。对依赖类的
测试一个层次一个层次地持续进行下去,直
至把整个软件系统构造完为止。
集群测试( cluster testing)是面向对
象软件测试的一个步骤。在这个测试步骤中,
用精心设计的测试用例检查一群相互协作的
类(通过研究对象模型可以确定协作类),
这些测试用例力图发现协作错误。
3,面向对象的确认测试
在确认测试层次,不再考虑类之间相互
连接的细节。和传统的确认测试一样,面向
对象软件的确认测试也集中检查用户可见的
动作和用户可识别的输出。为了导出确认测
试用例,测试人员应该认真研究动态模型和
描述系统行为的脚本,以确定最可能发现用
户交互需求错误的情景。
二、测试用例的设计
1.测试类的测试用例设计
与传统的单元测试的测试用例设计的关
注点(传统单元测试关注算法细节)不同,
由于类中单个方法一般由两三个语句构成,
测试比较简单,所以类测试用例的设计关注
于设计适当的操作序列以检查类中方法的协
作。主要的方法有:随机测试、划分测试和
基于故障的测试等三种。
(1)随机测试 以银行应用系统为例,
简要说明这种测试方法。
该系统的 account(帐户)类有如下操作:
open(打开),setup(建立),deposit
(存款),withdraw(取款),balance(余
额),summarize(清单),creditLimit
(透支限额),close(关闭)。
上列每个操作都可以应用于 account类的
实例,但是,该系统的性质也对操作的应用
施加了一些限制,例如,必须在应用其它操
作之前先打开帐户,在完成了全部操作之后
才能关闭帐户。即使有这些限制,可做的操
作也有许多种排列方法。
一个 account类实例的最小行为历史包
括下列操作:
open﹒ setup﹒ deposit﹒ withdraw﹒ close
这就是对 account类的最小测试序列。
但是,在下面的序列中可能发生许多其它行
为:
open﹒ setup﹒ deposit﹒ [ deposit | withdraw | balance |
summarize | creditLimit ]﹒ withdraw﹒ close
从上列序列可以随机地产生一系列不同
的操作序列,例如:
测试用例 # r1:
open﹒ setup﹒ deposit﹒ deposit﹒ bala
nce ﹒ summarize﹒ withdraw﹒ close
测试用例 # r2:
open﹒ setup﹒ deposit﹒ withdraw﹒ dep
osit﹒ balance﹒ creditLimit﹒ withdraw﹒ c
lose
执行上述这些及另外一些随机产生的测
试用例,可以测试类实例的不同生存历史。
( 2) 划分测试 与测试传统时采用
等价划分方法类似,划分测试方法可以
减少测试类时所需要的测试用例的数量。
介绍划分的方法有如下几种:
1) 基于状态的划分 这种方法根据
类操作改变状态的能力来划分操作。例
如 account类中状态操作有,deposit和
withdraw,非状态操作有 balance、
summarize和 creditLimit。
可以设计出如下的测试用例:
测试用例 # p1:
open﹒ setup﹒ deposit﹒ deposit﹒ with
draw ﹒ withdraw﹒ close
测试用例 # p2:
open﹒ setup﹒ deposit﹒ summarize﹒
creditLimit﹒ withdraw﹒ close
在测试用例 # p1改变类的状态,测试用
例 # p2不改变。
2)基于属性的划分 这种方法根据类
操作使用的属性来划分类操作。例如 account
类中的根据属性 balance的使用可划分为三个
类别:
使用 balance的操作
修改 balance的操作
不使用也不修改 balance的操作。
然后为每个类别设计测试序列。
3) 基于功能的划分 这种方法根据类
操作所完成的功能来划分类操作。例如,可
以把 account类中的操作分为:
初始化操作( open,setup)
计算操作( deposit,withdraw)
查询操作( balance,summarize、
creditLimit)
终止操作( close)。
然后为每个类别设计测试序列。
( 3)基于故障的测试 基于故障的测试
与传统的错误推测法类似,首先推测软件中
可能有的错误,然后设计出最可能发现这些
错误的测试用例。
2.集成测试的测试用例设计
在集成测试阶段,必须对类间协作进
行测试,测试类协作也可以使用随机测试和
划分测试方法,以及基于情景的测试和行为
测试来完成。
生成多个类的随机测试用例,Kirani和
Tsai建议使用下列步骤:
1)对每个客户类,使用类操作符列表来
生成一系列随机测试序列。这些操作符向服
务器类实例发送消息。
2)对所生成的每个消息,确定协作类和
在服务器对象中的对应操作符。
3)对服务器对象中的每个操作符(已经
被来自客户对象的消息调用),确定传递的
消息。
4)对每个消息,确定下一层被调用的操
作符,并把这些操作符结合进测试序列中。