第10章 面向对象设计实例
10.1 过程抽象和数据抽象
抽象是形成概念的必要手段,它是从许多事物中舍弃个别的、非本质性的特征,抽取共同及本质性的特征的过程。
对于分析而言,抽象原则具有两方面的意义:
(1)尽管问题域中的事物很复杂,但分析员并不需要了解和描述其全部特征,只需要分析研究与系统目标有关的事物及其本质特征。对于那些与系统目标无关的特征和许多具体的细节,即使有所了解,也应该舍弃。
(2)通过舍弃个体事物在细节上的差异,抽取其共同特征可以得到一批事物的抽象概念。
过程抽象是指任何一个完成确定功能的操作序列,其使用者都可以把它看做一个单一的实体,尽管实际上它可能是由一系列更低级的操作完成的。运用过程抽象,软件开发者可以把一个复杂的功能分解为一些子功能,过程抽象对于在对象范围内组织对象的成员函数是很有用的。
数据抽象是指根据施加于数据之上的操作来定义数据类型,并限定数据的值只能由这些操作来修改和观察。
10.2 发现对象并建立对象层
软件开发者将被开发的整个业务范围称作“问题域”,可以按如下步骤考虑发现对象并建立对象层。
1.将问题域和系统责任作为出发点
2.正确运用抽象原则
在OOA中正确地运用抽象原则,首先要舍弃那些与系统责任无关的事物,只注意与系统责任有关的事物。其次,对于与系统责任有关的事物,也不是把他们的任何特征都在相应的对象中表达出来,而是要舍弃那些与系统责任无关的特征。判断事物是否与系统责任有关的关键问题,一是该事物是否为系统提供了一些有用的信息(是否需要系统为它保存和管理某些信息);二是它是否向系统提供了某些服务(是否需要系统描述它的某些行为)。
3.寻找候选对象的基本方法
寻找候选对象的基本方法的主要策略是从问题域、系统边界和系统责任三方面找出可能有的候选对象。
4.审查和筛选对象
5.异常情况的检查和调整
一般认为下述情况都算异常情况,则需要进行调整。
(1)类的数据成员或成员函数不适合该类的全部对象。
(2)不同类的数据成员或成员函数相同或相似。
(3)对同一事物的重复描述。
10.3 定义数据成员和成员函数
为了发现对象的数据成员,首先应考虑借鉴以往的OOA结果,看看相同或相似的问题域是否有已开发的OOA模型,尽可能复用其中同类对象数据成员的定义。然后重点研究当前问题域和系统责任,针对本系统应该设置的每一类对象,按照问题域的实际情况,以系统责任为目标进行正确地抽象,从而找出每一类对象应有的数据成员。
1.寻找数据成员的一般方法
2.审查与筛选数据成员
对于初步发现的数据成员,要进行审查和筛选。即对每个数据成员提出以下问题:
(1)这个数据成员是否体现了以系统责任为目标的抽象。
(2)这个数据成员是否描述了这个对象本身的特征。
(3)该属性是否符合人们日常的思维习惯。
(4)这个数据成员是不是可以通过继承得到。
(5)是否可以从其他数据成员直接导出的数据成员。
3.定义成员函数
使用中要特别注意区分成员函数、非成员函数和友元函数三者。
10.4 如何发现基类和派生类结构
1.学习当前领域的分类学知识
分析员应该学习一些与当前问题域有关的分类学知识,,因为问题域现行的分类方法(如果有),往往比较正确地反映了事物的特征、类别以及各种概念的一般性与特殊性。
2.按照常识考虑事物的分类
从不同的角度考虑问题域中事物的分类,可以形成一些建立基类与派生类结构的初步设想,从而启发自己发现一些确实需要的基类与派生类结构。
3.构建基类与派生类
按照基类与派生类结构的两种定义,可引导我们从两种不同的思路去发现基类与派生类结构。一种思路是把每一个类看做是一个对象集合,分析这些集合之间的包含关系。另一种思路是看一个类是不是具有另一个类的全部特征。这包括两种情况:一是建立这些类时已经计划让某个类继承另一个类的全部成员,此时应建立基类与派生类结构来落实这种继承;另一种情况是起初只是孤立地建立每一个类,现在发现一个类中定义的成员(数据成员和成员函数)全部在另一个类中重新出现了,此时应该考虑建立基类与派生类结构,把后者作为前者的派生类,以简化定义。
4.考察类的成员
对系统中的每个类,从以下两个方面考察它们的成员;一是看一个类的成员是否适合这个类的全部对象。如果某些数据成员和成员函数只能适合该类的一部分对象,说明应该从这个类中划分出一些派生类,建立基类与派生类关系。
另一方面检查是否有两个(或更多的)类含有一些共同的数据成员和成员函数。如果有,则考虑若把这些共同的数据成员和成员函数提取出来后能否构成一个在概念上是包含原来那些类的基类,组成一个基类与派生类结构。
还要对发现的结构进行审查、调整和简化,处理异常情况,才能建立合适的结构。
10.5 接口继承与实现继承
现在假设设计一个可以供其他类继承的基类,派生类使用公有继承方式。公有继承实际上是由两个不同部分组成的,即函数接口的继承和函数实现的继承。可概括成如下两点:
(1)继承的总是成员函数的接口。对于基类是正确的任何事情,对于它的派生类必须也是正确的。
(2)声明纯虚函数的目的是使派生类仅仅继承函数接口,而纯虚函数的实现则由派生类去完成。
(3)声明虚函数的目的是使派生类既能继承基类对此虚函数的实现,又能继承虚函数提供的接口。
(4)声明实函数的目的是使派生类既能继承基类对此实函数的实现,又能继承实函数提供的接口。
1.纯虚函数
纯虚函数最显著的两个特征是:
(1)它们必须由继承它的非抽象类重新说明
(2)它们在抽象类中没有定义
2.虚函数
虚函数的情况不同于纯虚函数,派生类继承虚函数的接口,虚函数一般只提供一个可供派生类选择的实现。之所以声明虚函数,目的在于使派生类既继承函数接口,也继承虚函数的实现。
3.实函数
一个实成员函数指定它自己在派生类中保持不变。因此,声明实函数的目的是使派生类既继承这个函数的实现,也继承其函数接口。
4.避免重新定义继承的实函数
虽然在派生类中可以重定义基类的同名实函数,但是从使用安全角度来看,为了提高程序质量,在实际应用中应避免这样做。
10.6 设计实例