第12章 面向对象实现
一、面向对象实现概述
1.主要任务
(1)把面向对象设计结果翻译成用某种程序语言书写的面向对象程序。
(2)测试并调试面向对象的程序。
2.面向对象程序质量的影响因素
(1)面向对象设计的质量;
(2)采用的程序语言的特点;
(3)程序设计风格。
3.保证软件可靠性的方法
保证软件可靠性的主要措施是软件测试。面向对象测试的目标是用尽可能低的测试成本发现尽可能多的软件错误。
二、程序设计语言
1.面向对象语言的优点
(1)面向对象设计结果的表示方式
①面向对象语言
编译程序可以自动把面向对象概念映射到目标程序中。
②非面向对象语言
必须由程序员自己把面向对象概念映射到目标程序中。
(2)优点
从面向对象观点看来,能够更完整、更准确地表达问题域语义的面向对象语言的语法是非常重要的,会带来以下3个重要优点:
①一致的表示方法
面向对象开发基于不随时间变化的、一致的表示方法。既有利于在软件开发过程中始终使用统一的概念,也有利于维护人员理解软件的各种配置成分。
②可重用性
既可重用面向对象分析结果,也可重用相应的面向对象设计和面向对象程序设计结果。
③可维护性
程序显式地表达问题域语义,对维护人员理解待维护的软件有很大帮助。在选择编程语言时,应该考虑的首要因素是哪个语言能最恰当地表达问题域语义。
2.面向对象语言的技术特点
纯面向对象语言着重支持面向对象方法研究和快速原型的实现,而混合型面向对象语言的目标则是提高运行速度和使传统程序员容易接受面向对象思想。成熟的面向对象语言通常都提供丰富的类库和强有力的开发环境。在选择面向对象语言时应该着重考虑以下几种技术特点。
(1)支持类与对象概念的机制
①内容
面向对象语言允许用户动态创建对象,并且可以用指针引用动态创建的对象。需要及时释放不再需要的对象所占用的内存。
②管理内存的方法
a.由语言的运行机制自动管理内存,即提供自动回收“垃圾”机制;
b.由程序员编写释放内存的代码。
注意:自动管理内存虽然方便且安全,但是必须采用先进的垃圾收集算法才能减少开销。
(2)实现聚集结构的机制
①使用指针;
②使用独立的关联对象。
注意:大多数现有的面向对象语言并不显式支持独立的关联对象,在这种情况下,增加内部指针可以方便地实现关联。
(3)实现泛化结构的机制
①实现继承的机制;
②解决名字冲突的机制,即处理在多个基类中可能出现的重名问题。
(4)实现属性和服务的机制
①实现属性的机制
a.支持实例连接的机制;
b.属性的可见性控制;
c.对属性值的约束。
②实现服务的机制
a.支持消息连接(表达对象交互关系)的机制;
b.控制服务可见性的机制;
c.动态联编(在发送消息前,无须知道接受消息的对象属于哪个类)。
(5)类型检查
①分类
a.弱类型
语言仅要求每个变量或属性隶属于一个对象。
b.强类型
语法规定每个变量或属性必须准确地属于某个特定的类。
②强类型语言优点
a.有利于在编译时发现程序错误,提高软件的可靠性。
b.增加了优化的可能性,提高软件的运行效率。
③适用性
使用强类型编译型语言开发软件产品,使用弱类型解释型语言快速开发原型。
(6)类库
①类型
大多数面向对象语言都提供一个实用的类库。某些语言本身并没有规定提供什么样的类库,而是由实现这种语言的编译系统自行提供类库,主要可分为以下几类:
a.包含实现通用数据结构的类,即包容类;
b.实现各种关联的类;
c.独立于具体设备的接口类;
d.用于实现窗口系统的用户界面类。
②重要性
存在类库,许多软构件就不必由程序员从头编写了,为实现软件重用带来很大方便。
(7)效率
提高面向对象语言效率的方法为:
①使用拥有完整类库的面向对象语言。
②优化查找继承树查找过程,从而实现高效率查找。
(8)持久保存对象
①原因
a.为实现在不同程序之间传递数据,需要保存数据;
b.为恢复被中断了的程序的运行,首先需要保存数据。
②方法
a.在类库中增加对象存储管理功能;
b.使程序设计语言语法与对象存储管理语法无缝集成。
(9)参数化类
①定义
参数化类是使用一个或多个类型去参数化一个类的机制,如果程序语言提供一种能抽象出这类共性的机制,则对减少冗余和提高可重用性是大有好处的。
②方法
a.定义一个参数化的类模板;
b.把数据类型作为参数传递进来。
(10)开发环境
软件工具和软件开发环境对软件生产率有很大影响,因此面向对象语言所提供的软件工具或开发环境应该至少包括编辑程序,编译程序或解释程序,浏览工具和调试器等。具体如下:
①编辑程序;
②编译程序或解释程序(最重要);
③浏览工具;
④调试器。
其中,编译程序或解释程序是最基本、最重要的软件工具。编译与解释的差别主要是速度和效率不同,编译程序快且效率高,解释程序慢且效率低但易于调试。
3.选择面向对象语言的标准
(1)将来能否占主导地位
(2)可重用性
应该优先选用能够最完整、最准确地表达问题域语义的面向对象语言。
(3)类库和开发环境
①在类库中,应该考虑是否提供了类库、类库中提供了哪些有价值的类。
②在开发环境中,除了基本软件工具外,还应该提供类库编辑工具和浏览工具。
注意:语言、开发环境和类库这3个因素综合起来,共同决定了可重用性。
(4)其他因素
①对用户学习面向对象分析、设计和编码技术所能提供的培训服务;
②在使用这个面向对象语言期间能提供的技术支持;
③能提供给开发人员使用的开发工具、开发平台、发行平台;
④对机器性能和内存的需求;
⑤集成已有软件的容易程度等。
三、程序设计风格
1.概念
(1)良好的程序设计风格的重要性
①能明显减少维护或扩充的开销。
②有助于在新项目中重用已有的程序代码。
(2)良好的面向对象程序设计风格的内容
①传统的程序设计风格准则。
②为适应面向对象方法所特有的概念而必须遵循的一些新准则。
2.提高可重用性
(1)代码重用
①内部重用
即本项目内的代码重用,主要是找出设计中相同或相似的部分,然后利用继承机制共享它们。
②外部重用
即新项目重用旧项目的代码,需要有长远眼光,反复考虑,精心设计。
(2)主要准则
有助于实现内部重用和外部重用的程序设计准则如下:
①提高方法的内聚
一个方法应该只完成单个功能。如果某个方法涉及两个或多个不相关的功能,则应该把它分解成几个更小的方法。
②减小方法的规模
应该减小方法的规模。如果某个方法规模过大,则应该把它分解成几个更小的方法。
③保持方法的一致性
保持方法的一致性,有助于实现代码重用,功能相似的方法应该有一致的名字、参数特征、返回值类型、使用条件及出错条件等。
④把策略与实现分开
a.方法分类
第一,策略方法。
策略方法应该检查系统运行状态,并处理出错情况,它们并不直接完成计算或实现复杂的算法。其紧密依赖于具体应用。
第二,实现方法。
实现方法仅仅针对具体数据完成特定处理,用于实现复杂的算法。在执行过程中发现错误,它们只返回执行状态而不对错误采取行动。其相对独立于具体应用。
b.与实现分开
把算法的核心部分放在一个单独的具体实现方法中。为此需要从策略方法中提取出具体参数,作为调用实现方法的变元。
⑤全面覆盖
a.输入条件的各种组合都可能出现,应针对所有组合写出方法。
b.一个方法对空值、极限值及界外值等异常情况也应能够做出有意义的响应。
⑥尽量不使用全局信息
应该尽量降低方法与外界的耦合程度,不使用全局信息是降低耦合度的一项主要措施。
⑦利用继承机制
在面向对象程序中,使用继承机制是实现共享和提高重用程度的主要途径。
a.调用子过程
图12-1 通过调用公用方法实现代码重用
把公共的代码分离出来,构成一个被其他方法调用的公用方法。可以在基类中定义这个公用方法,供派生类中的方法调用,如图12-1所示。
b.分解因子
图12-2 通过因子分解实现代码重用
提高相似类代码可重用性的一个有效途径,是从不同类的相似方法中分解出不同的代码,把余下的代码作为公用方法中的公共代码,把分解出的因子作为名字相同算法不同的方法,放在不同类中定义,并被这个公用方法调用,如图12-2所示。
c.使用委托
继承关系的存在意味着子类“即是”父类,因此,父类的所有方法和属性都应该适用于子类,仅当确实存在一般—特殊关系时,使用继承才是恰当的,当逻辑上不存在一般—特殊关系时,为重用已有的代码,可以利用委托机制。
d.把代码封装在类中
重用通过其他方法编写的、解决同一类应用问题的程序代码的一个比较安全的途径是把被重用的代码封装在类中。
3.提高可扩充性
提高可扩充行的主要准则为:
(1)封装实现策略
应该把类的实现策略封装起来,对外只提供公有的接口,否则将降低今后修改数据结构或算法的自由度。
(2)不要用一个方法遍历多条关联链
一个方法应该只包含对象模型中的有限内容。违反这条准则将导致方法过分复杂,既不易理解,也不易修改扩充。
(3)避免使用多分支语句
可以利用DΟ_CASE语句测试对象的内部状态,而不要用来根据对象类型选择应有的行为,否则在增添新类时将不得不修改原有的代码。
(4)精心确定公有方法
公有方法是向公众公布的接口。对这类方法的修改往往会涉及许多其他类,因此,修改公有方法的代价通常都比较高。为提高可修改性,降低维护成本,必须精心选择和定义公有方法私有方法是仅在类内使用的方法,通常利用私有方法来实现公有方法。删除、增加或修改私有方法所涉及的面要窄得多,因此代价也比较低。
4.提高健壮性
(1)健壮性
健壮性是在硬件故障、输入的数据无效或操作错误等意外环境下,系统能做出适当响应的程度。程序员在编写实现方法时,需要在健壮性与效率之间做出适当的折衷。
(2)提高健壮性的主要准则
①预防用户的操作错误
软件系统必须具有处理用户操作错误的能力。当用户在输入数据时发生错误,不应该引起程序运行中断,更不应该造成“死机”。
②检查参数的合法性
对公有方法,尤其应该着重检查其参数的合法性,因为用户在使用公有方法时可能违反参数的约束条件。
③不要预先确定限制条件
在设计阶段,往往很难准确地预测出应用系统中使用的数据结构的最大容量需求。因此,不应该预先设定限制条件。
④先测试后优化
为在效率与健壮性之间做出合理的折衷,应该在为提高效率而进行优化之前,先测试程序的性能。
四、测试策略
1.经典的测试策略
测试软件的经典策略是,从“小型测试”开始,逐步过渡到“大型测试”。用软件测试的专业术语描述,可以分为以下三步:
(1)单元测试;
(2)集成测试;
(3)确认测试、系统测试。
2.面向对象测试策略
(1)面向对象的单元测试
最小的可测试单元是封装起来的类和对象。一个类可以包含一组不同的操作,而一个特定的操作也可能存在于一组不同的类中。测试面向对象软件时,不能再孤立地测试单个操作,而应该把操作作为类的一部分来测试。在测试面向对象的软件时,传统的单元测试方法是不适用的,不能再在“真空”中(即孤立地)测试单个操作。
(2)面向对象的集成测试
①策略
因为在面向对象的软件中不存在层次的控制结构,传统的自顶向下或自底向上的继承策略是没有意义的。面向对象软件的集成测试主要有下述两种不同的策略:
a.基于线程的测试
把响应系统的一个输入或一个事件所需要的那些类集成起来。分别集成并测试每个线程,同时应用回归测试以保证没有产生副作用。
b.基于使用的测试
不使用服务器类的独立类,把独立类都测试完之后,再测试使用独立类的下一个层次的类(称为依赖类)。对依赖类的测试一层一层次地测试,直至把整个软件系统构造完为止。
②集群测试
集群测试是面向对象软件集成测试的一个步骤。在这个测试步骤中,用精心设计的测试用例检查一群相互协作的类,这些测试用例力图发现协作错误。
(3)面向对象的确认测试
在确认测试或系统测试层次,不再考虑类之间相互连接的细节。面向对象软件的确认测试也集中检查用户可见的动作和用户可识别的输出。为了导出确认测试用例,测试人员应该认真研究动态模型和描述系统行为的脚本,以确定最可能发现用户交互需求错误的情景。
五、设计测试用例
1.测试类的方法
(1)随机测试
通过执行一些随机产生的测试用例,来对类和对象进行测试的过程。
(2)划分测试
①优点
采用划分测试方法可以减少测试类时所需要的测试用例的数量。
②流程
a.把输入和输出分类;
b.设计测试用例以测试划分出的每个类别。
③方法
a.基于状态的划分:根据类操作改变类状态的能力来划分类操作。
b.基于属性的划分:根据类操作使用的属性来划分类操作。
c.基于功能的划分:根据类操作所完成的功能来划分类操作。
(3)基于故障的测试
首先推测软件中可能有的错误,然后设计出最可能发现这些错误的测试用例。
2.集成测试方法
(1)多类测试
和测试单个类相似,测试类协作可以使用随机测试方法和划分测试方法,以及基于情景的测试和行为测试来实现。
①随机测试
a.对每个客户类,使用类操作符列表来生成一系列随机测试序列。
b.对所生成的每个消息,确定协作类和在服务器对象中的对应操作符。
c.对服务器对象中的每个操作符,确定传递的消息。
d.对每个消息,确定下一层被调用的操作符,并把这些操作符结合进测试序列中。
②划分测试
a.应该扩充测试序列以包括那些通过发送给协作类的消息而被调用的操作。
b.根据与特定类的接口来划分类操作。
(2)从动态模型导出测试用例
类的状态图可以帮助人们导出测试该类的动态行为的测试用例。通过导出大量的测试用例,保证该类的所有行为都被适当地测试了。在类的行为导致与一个或多个类协作的情况下,应该使用多个状态图去跟踪系统的行为流。