面向对象的设计.docx
《面向对象的设计.docx》由会员分享,可在线阅读,更多相关《面向对象的设计.docx(21页珍藏版)》请在冰点文库上搜索。
面向对象的设计
面向对象的设计概述
OOA和OOD之间有密切的衔接关系,从OOA到OOD是一个逐渐扩充模型的过程。
分析处理以问题为中心,可以不考虑任何与特定计算机有关的问题,而OOD则把我们带进了面向计算机的“实地”开发活动中去。
通常,OOD分为两个阶段,即高层设计和低层设计。
高层设计建立应用的体系结构。
低层设计集中于类的详细设计。
(1)高层设计
高层设计阶段开发软件的体系结构,构造软件的总体模型。
在这个阶段,标识在计算机环境中进行问题解决工作所需要的概念,并增加了一批需要的类。
这些类包括那些可使应用软件与系统的外部世界交互的类。
此阶段的输出是适合应用软件要求的类、类间的关系、应用的子系统视图规格说明。
通常,利用面向对象设计得到的系统框架如图6.11所示。
①高层设计模型
一个典型的高层设计模型即客户-服务器模型,它构造起应用软件的总体模型,这个模型导出的体系结构既可在过程性系统中使用,又可在面向对象的系统中使用。
客户-服务器模型的想法是让系统的一个部分(服务器子系统)提供一组服务给系统的另一个部分(客户子系统)。
请求服务的对象都归于客户子系统,而接受请求提供服务的部分就是服务器。
图6.11OOD设计导出的体系结构
②高层设计的规则
最小化各构件间的通信:
在子系统的各个高层构件之间的通信量应当达到最小。
一个用户界面应当能够自行处理交互、错误改正和硬件控制,而不需打扰主应用。
隐藏复杂性:
子系统应当把那些成组的类打包,形成高度的内聚。
逻辑功能分组:
虽然输入和输出设备可能相互间不通信,但逻辑上把它们归组到一个处理输入/输出的子系统中。
这样比较容易识别并定位问题论域中的事件。
类与通过概念封装的子系统十分类似。
事实上,每个子系统都可以被当做一个类来实现,这个类聚集它的构件,提供了一组操作。
类和子系统的结构是正交的,一个单个类的实例可能是不止一个子系统的一部分。
高层设计阶段增加了一批必要的类,主要包括了那些可使应用软件与系统的外部世界交互的类。
这些交互则包括与其它软件系统(如数据库管理系统、鼠标和键盘)的界面,与使用来进行数据收集或者负责控制的硬件设备的界面等。
高层设计可以表征为标识和定义模块的过程。
但这种模块可以是单个的类,还可以是由一些类组合成的子系统。
定义过程是职责驱动的。
高层设计和类设计这两个阶段是相对封闭的。
在这种情况下,应用软件中的每一个事物都是一个对象,包括应用软件自身在内!
根据这个思想,这两个阶段又是连接的。
应用软件的设计是大类的设计,这种类设计考察应用软件所期望的每一个行为,并利用这些行为形成应用类的界面。
(2)类设计的目标和方针
类设计的第一步是标识应用所需的概念。
应用分析过程包括了对问题论域所需的类的模型化;但在最终实现应用时不只有这些类,还需要追加一些类。
类设计的主要目标如下:
①单一概念的模型:
在分析与高层设计阶段,常常需要使用多个类来表示一个“概念”。
一般人们在使用面向对象方法开发软件时,常常把一个概念进行分解,用一组类来表示这个概念。
当然,也可以只用一个独立的类来表示一个概念。
②可复用的构件:
我们希望所开发的构件可以在未来的应用中使用。
因此,需要一些附加特性。
例如,在相关的类的集合中界面的标准化
③可靠的构件:
应用软件必须是可靠的(健壮的和正确定义的)软件。
而这种可靠性与它的构件有关。
每个构件必须经过充分的测试。
但由于成本关系,往往测试不够完备。
然而,如果我们要建立可复用的类,则通过测试确保构件的可靠性是绝对必要的。
④可集成的构件:
我们希望把类的实例用到其它类的开发和应用中,这要求类的界面应当尽可能小,一个类所需要的数据和操作都定义在类定义中。
因此,类的设计应当尽量减少命名冲突。
面向对象语言的消息语法可通过鉴别带有实例名的操作名来减少可能的命名冲突。
类结构提供的封装使得把概念集成到应用的工作变得很容易。
封装特性保证了把一个概念的所有细节都组合在一个界面下,而信息隐蔽则保证了实现级的名字将不会其它类的名字互相干扰。
我们讨论的方针是类的模块设计的方针,还要给出类设计质量的度量。
①信息隐蔽:
软件设计通过信息隐蔽可增强抽象,并可保护类的存储表示不被抽象数据类型实例的用户直接存取。
对其表示的唯一存取途径只能是界面。
②消息限制:
类的设计者应当为类的命令设计一个明确的界面,该类实例的用户应当只使用界面提供的操作。
③狭窄界面:
不是所有的操作都是公共的。
只有对其它类的实例是必要的操作才放到界面上,其它操作应是隐蔽实现的部分。
④强内聚:
模块内部各个部分之间应有较强的关系
⑤弱耦合:
一个单独模块应尽量不依赖于其它模块。
如果在类A的实例中建立了类B的实例,或者如果类A的操作需要类B的实例做为参数,或者如果类A是类B的一个派生类,则称类A“依赖于”类B。
一个类应当尽可能少地依赖于其它类。
耦合程度部分依赖于所使用的分解方法。
类A之所以依赖于类B,是因为类A要求类B提供服务。
这个依赖性可通过复制类A中的类B的功能来消除。
但代码的复制减少了系统的灵活性并增加了维护的困难。
继承结构损害了弱耦合的概念。
因为在建立一般化-特殊化关系的时候,继承引入了依赖。
⑥显式信息传递:
除了依赖于最少的类外,还应该明确在这些类之间的信息流。
在类之间全局变量的共享隐含了信息的传递,并且是一种依赖形式。
因此,两个类之间的交互应当仅涉及显式信息传递。
显式信息传递是通过参数表来完成的。
⑦派生类当做派生类型:
继承结构的使用是面向对象开发方法的一大特色。
每个派生类应该当做基类的特殊化来开发,而基类所具有的公共界面成为派生类的共有界面的一个子集。
C++允许设计者选择类的基类是共有的或私有的。
如果基类是共有的,则其共有界面将成为新的派生类的共有界面部分,这表明基类的行为成为派生类的行为部分。
这类似于类型与派生类型之间的关系。
如果基类是私有的,它的行为将不是继承类的公共行为部分而是实现部分。
它的提出是为了提供实现新类的服务。
⑧抽象类:
某些语言提供了一个类,用它做为继承结构的开始点,所有用户定义的类都直接或间接以这个类为基类。
Smalltalk提供了一个类Object做为所有类的继承树的根,而C++则支持多重继承结构。
每一种结构都包含了一组类,它们是(或应该是)某种概念的特殊化。
这个概念应抽象地由结构的根类来表示。
因此,每个继承结构的根类应当是目标概念的一个抽象模型。
这个抽象模型生成一个类,它不用于产生实例。
它定义了一个最小的共有界面,许多派生类可以加到这个界面上以给出概念的一个特定视图。
(3)通过复用设计类
利用既存类来设计类,有4种方式:
选择,分解,配置和演变。
这是面向对象技术的一个重要优点。
许多类的设计都是基于既存类的复用。
1选择:
设计类最简单的方法是从既存构件中简单地选择合乎需要的构件。
这就是开发软件库的目的。
一个OO开发环境应提供常用构件库,大多数语言环境都带有一个原始构件库(如整数、实数和字符),它是基础层。
任一基本构件库(如“基本数据结构”构件)都应建立在这些原始层上。
这些都是些一般的可复用的类。
这个层还包括一组提供其它应用论域服务的一般类,如窗口系统和图形图元。
表6.1显示了建立在这些层上面的特定域的库。
最低层的论域库包括了应用论域的基础概念并支持广泛的应用开发。
特定项目和特定组的库包括一些论域库,它包含为相应层所定义的信息。
表6.1一个面向对象构件库的层次
特定组的构件─一个小组为他们自己组内所有成员使用而开发
特定项目的构件─一个小组为某一个项目而开发
特定问题论域的构件─购自某一个特定论域的软件销售商
一般构件─购自专门提供构件的销售商
特定语言原操作─购自一个编译器的销售商
②分解:
最初标识的“类”常常是几个概念的组合。
在设计时,可能会发现所标识的操作落在分散的几个概念中,或者会发现,数据属性被分开放到模型中拆散概念形成的几个组内。
这样我们必须把一个类分成几个类,希望新标识的类容易实现,或者它们已经存在。
③配置:
在设计类时,可能会要求由既存类的实例提供类的某些特性。
通过把相应类的实例声明为新类的属性来配置新类。
例如,一种仿真服务器可能要求使用一个计时器来跟踪服务时间。
设计者不必开发在这个行为中所需的数据和操作,而是应当找到计时器类,并在服务器类的定义中声明它。
图6.13建立子类
④演变:
要开发的新类可能与一个既存类非常类似,但不完全相同。
此时,不适宜采用“选择”操作,但可以从一个既存类演变成一个新类,可以利用继承机制来表示一般化-特殊化的关系。
特殊化处理有三种可能的方式。
由既存类建立子类:
现要建立一个新类“起重车”。
它的许多属性和服务都在既存类“汽车”中。
关系如图6.13所示。
新类是既存类的特殊情形。
这时直接让“起重车”类作为“汽车”类的子类即可。
②建立继承层次由既存类建立新类:
现要增加一个新类“拖拉机”。
它的属性与服务有的与“汽车”类相同,有的与“汽车”类不同。
关系如图6.14所示。
这时,调整继承结构。
建立一个新的一般的“车辆”类,把“拖拉机”与“汽车”类的共性放到“车辆”类中,“拖拉机”与“汽车”类都成为“车辆”类的子类。
“车辆”是抽象类,相关操作到子类“汽车”类去找。
车辆
拖拉机
汽车
拖拉机类
图6.14调整继承结构
汽车类
③建立既存类的父类:
另一种情形是想在既存类的基础上加入新类,使得新类成为既存类的一般类。
例如,已经存在“三角形”类,“四边形”类,想加入一个“多边形”类,并使之成为“三角形”和“四边形”类的一般类。
继承结构如图6.15所示。
从这个“多边形”类又可派生出新的类,如“六边形”类。
多边形
多
边
形
类
六边形
四边形
三角形
图6.15建立一般类
后两种涉及既存类的修改。
在这两种情况下,既存类中定义的操作或数据被移到新类中。
如果遵循信息隐蔽和数据抽象的原理,这种移动应不影响已有的使用这些类的应用。
类的界面保持一致,虽然某些操作是通过继承而不是通过类的定义伸到这个类的。
(4)类设计方法
通常,类中的实例具有相同的属性和操作,应当建立一个机制来表示类中实例的数据表示、操作定义和引用过程。
这时,类的设计是由数据模型化、功能定义和ADT定义混合而成的。
类是某些概念的一个数据模型,类的属性就是模型中的数据域,类的操作就是数据模型允许的操作。
要明确规定它们两个谁先确定是不可能的,两个处理是互补的。
类的标识有主动和被动之分。
被动类是数据为中心的,它们是根据系统的其它对象发送来的消息而修改其封装数据的;主动类则提供许多系统必须履行的基本操作。
与被动类的实例(被动对象)一样,主动类的实例(主动对象)接收消息,但这些对象是负责发送追加消息和控制某些应用部分的。
在窗口环境,一个窗口是一个被动对象,它基于发送给窗口的消息来显示某些内容。
窗口管理器是一个主动对象,它担负着各种在它控制的窗口上的操作。
在被动类与主动类的设计之间不存在明显的差别。
在设计主动类时,需要优先确定数据模型,稍后再确定操作;在设计被动类时,把类提供的服务翻译成操作。
在标识了服务之后再设计为支持服务所需要的数据。
许多类都是这两个极端的混合。
类中对象的组成包括了私有数据结构、共享界面操作和私有操作。
而消息则通过界面,执行控制和过程性命令。
因此,要分别讨论它们的实现。
类的设计描述包括两部分:
①协议描述:
协议描述定义了每个类可以接收的消息,建立一个类的界面。
协议描述由一组消息及对每个消息的相应注释组成。
②实现描述:
实现描述说明了每个操作的实现细节,这些操作应包含在类的消息中。
实现描述由以下信息构成:
类名和对一个类引用的规格说明
私有数据结构的规格说明,包括数据项和其类型的指示
每个操作的过程描述
实现描述必须包含充足的信息,以提供在协议描述中所描述的所有消息的适当处理。
由一个类所提供服务的用户必须熟悉执行服务的协议,即定义“什么”被描述;而服务的提供者(对象类本身)必须关心:
服务如何提供给用户,即实现细节的封装问题。
7.Coad与Yourdon面向对象设计方法
OOD模型类似于构造蓝图,以最完整的形式全面地定义了如何用特定的实现技术建立起一个目标系统。
在OOA模型和OOD模型中使用了共同的表示法。
这有助于从分析到设计的转换,并有助于在当前的设计和实现中维护OOA模型。
与OOA模型一样,OOD模型也有5层结构,又被划分成了4个组成部分:
问题论域、用户界面、任务管理和数据管理。
这些组成部分把实现技术隐藏起来,使之与系统的基本问题论域行为分离开来。
这种策略能够帮助提高产品的可复用性,有助于产品的升级换代。
在OOA中,我们实际上只涉及到问题论域部分,其它三个部分是在OOD中加进来的。
由于问题论域部分包括与我们所面对的应用问题直接有关的所有类和对象。
由于识别和定义这些类和对象的工作在OOA中已经开始,这里只是对它们做进一步的细化。
在其它的三个部分中,将识别和定义新的类和对象。
这些类和对象形成问题论域部分与用户、与外部系统和专用设备,以及与磁盘文件和数据库管理系统的界面。
Coad与Yourdon强调这三部分的作用主要是保证系统基本功能的相对独立,以加强软件的可复用性。
假如外部的通信系统更新了,相应的通信协议也应有所变化。
在这种情况下,我们只需修改任务管理部分中的某些类和对象,而不必对其它几个部分做任何修改。
(1)问题论域部分(PDC)的设计
完整的未经改动的OOA模型将成为初始的OOD模型的PDC部分。
然后根据实现技术及实现方面的限制,对初始PDC部分的模型中的某些类与对象、结构、属性、操作进行组合与分解。
但保留在OOA模型中所捕获到的基本的系统行为。
如果使用可复用的类,那么它也要引入到PDC中。
另外根据OOD的附加原则,增加必要的类、属性和关系。
①复用设计
根据问题解决的需要,把从类库或其它来源得到的既存类增加到问题解决方案中去。
既存类可以是用面向对象程序语言编写出来的,也可以是用其它语言编写出来的可用程序。
要求标明既存类中不需要的属性和操作,把无用的部分维持到最小限度。
并且增加从既存类到应用类之间的一般化―特殊化的关系。
进一步地,把应用中因继承既存类而成为多余的属性和操作标出。
还要修改应用类的结构和连接,必要时把它们变成可复用的既存类。
②把应用论域相关的类关联起来
在设计时,从类库中引进一个根类,做为包容类,把所有与应用论域有关的类关联到一起,建立类的层次。
把同一应用论域的一些类集合起来,存于类库中。
③加入一般化类以建立类间协议
有时,某些特殊类要求一组类似的服务。
在这种情况下,应加入一个一般化的类,定义为所有这些特殊类共用的一组服务名,这些服务都是虚函数。
在特殊类中定义其实现。
④调整继承支持级别
在OOA阶段建立的对象模型中可能包括有多继承关系,但实现时使用的程序设计语言可能只有单继承,甚至没有继承机制,这样就需变更PDC中类的层次结构。
图6.27(a)是多继承模式。
1
1,m
(a)多继承(b)通过实例连接分解多继承(c)平铺为单继承
图6.27多继承改为单继承
针对单继承语言的调整:
利用单继承语言,可使用两种方法把多继承结构转变为单继承结构。
一是把特殊类的对象看做是一个一般类对象所扮演的角色,通过实例连接把多继承的层次结构转换为单继承的层次结构,如图6.27(b)所示;一是把多继承的层次结构平铺,成为单继承的层次结构,如图6.27(c)所示。
在这种情况下,有些属性或操作在同层的特殊类中会重复出现。
针对无继承语言的调整:
当使用无继承的程序设计语言时,必须把具有继承关系的类层次结构平铺开来,成为一组类和对象。
一般可利用命名惯例,把这些类或对象关联起来。
⑤修改设计以提高性能
提高执行效率和速度是系统设计的主要指标之一。
有时,必须改变问题论域的结构以提高效率。
如果类之间经常需要传送大量消息,可合并相关的类,使得通信成为对象内的通信,而不是对象间的通信;或者使用全局数据作用域,打破封装的原则,以减少消息传递引起的速度损失。
增加某些属性到原来的类中,或增加低层的类,以保存暂时结果,避免每次都要重复计算造成速度损失。
为提高性能,在对OOA模型进行大规模的改动之前,应考虑下面一些问题:
如果没有性能准则,不要去人为地建立。
当软件运行在一个CPU速度很快的计算机上,并是单机的人机交互时,大多数的时钟周期都用在了等待用户输入上。
不要认为象C++之类的OOPL就一定效率不高。
有一些事实表明,非OOPL的紧凑代码的效率比OOPL的效率高近10倍,但在大多数情况下,OOPL的效率损失约为10%。
而且用非OOPL编程会令程序员非常疲劳,容易出错。
提高一个现存系统的工作效率比重新设计一个高效的系统要容易。
一开始应当建立一个原始的简单的设计,实现和调试不会太困难。
如果对设计有性能要求,只需加入少量的工作就可以了。
通常系统80%的开销都集中在20%的代码段上。
与其为了尽量处处节省系统开销而破坏完善的系统结构,还不如找出系统开销最集中的地方,只对那部分做优化。
预测软件开销集中在什么地方是困难的,进行优化最有效的方法是在系统运行时使用性能监测工具对系统进行观测。
一些像继承、动态绑定、消息传递等处理虽然看起来简单,但需要大量的系统开销。
有的代码看起来复杂,但效率不见得低。
在代码复杂性与运行的低效之间没有相关性。
提高性能最好的方法是采用最出色的解决方案,而不是拼命地去节省几个微秒、几个字节。
这个结论在面向对象技术出现是这样,在面向对象论域仍然是这样。
⑥加入较低层的构件
在做面向对象分析时,分析员往往专注于较高层的类和对象,避免考虑太多较低层的实现细节。
但在做面向对象设计时,设计师在找出高层的类和对象时,必须考虑到底需要用到哪些较低层的类和对象。
(2)用户界面部分的设计
通常在OOA阶段给出了所需的属性和操作,在设计阶段必须根据需求把交互的细节加入到用户界面的设计中,包括有效的人机交互所必需的实际显示和输入。
如Windows、Pane、Selector等。
用户界面部分设计主要由以下几个方面组成。
①用户分类
进行用户分类的目的是明确使用对象,针对不同的使用对象设计不同的用户界面,以适合不同用户的需要。
分类的原则有:
按技能层次分类:
外行/初学者/熟练者/专家。
按组织层次分类:
行政人员/管理人员/专业技术人员/其它办事员。
按职能分类:
顾客/职员
②描述人及其任务的场景
对以上定义的每一类用户,列出对以下问题做出的考虑:
什么人、目的、特点、成功的关键因素、熟练程度以及任务场景。
③设计命令层
研究现行的人机交互活动的内容和准则。
这些准则可以是非正式的,如“输入时眼睛不易疲劳”,也可以是正式规定的;
建立一个初始的命令层:
可以有多种形式,如一系列MenuScreens、或一个MenuBar、或一系列Icons。
细化命令层:
这时,要考虑以下几个问题。
排列命令层次。
把使用最频繁的操作放在前面;按照用户工作步骤排列。
通过逐步分解,找到整体-部分模式,帮助在命令层中对操作进行分块。
对菜单宽度与深度进行比较,把深度尽量限制在三层之内。
减少操作步骤:
在完成必须任务的前提下,把点取、拖动和键盘操作减到最少。
④设计详细的交互
用户界面设计有若干原则,其中包括:
一致性:
采用一致的术语、一致的步骤和一致的活动。
操作步骤少:
减少敲键和鼠标点取的次数,减少完成某件事所需的下拉菜单的距离。
不要“哑播放”:
即每当用户等待系统完成一个活动时,要给出一些反馈信息,说明工作正在进展,以及进展的程度。
Undo:
在操作出现错误时,恢复或部分恢复原来的状态。
减少人脑的记忆负担:
不应在一个窗口使用在另一个窗口中记忆或写下的信息;需要人按特定次序记忆的东西应当组织得容易记忆。
学习的时间和效果:
提供联机的帮助信息。
趣味性:
在外观和感受上,尽量采取图形界面,符合人类习惯,有一定吸引力。
⑤继续做原型
用户界面原型是用户界面设计的重要工作。
人需要对提交的人机交互活动进行体验、实地操作,并精炼成一致的模式。
使用快速原型工具或应用构造器,对各种命令方式,如菜单、弹出、填充以及快捷命令,做出一些可供选择的原型,让用户使用,收集用户的反映,通过修改、演示的迭代,使界面越来越有效。
⑥设计HIC(人机交互)类
设计HIC类,首先从组织窗口和构件的用户界面界面的设计开始。
窗口需要进一步细化,通常包括类窗口、条件窗口、检查窗口、文档窗口、画图窗口、过滤器窗口、模型控制窗口、运行策略窗口、模板窗口等。
每个类包括窗口的菜单条、下拉菜单、弹出菜单的定义。
还要定义用于创建菜单、加亮选择项、引用相应的响应的操作。
每个类还负责窗口的实际显示。
所有有关物理对话的处理都封装在类的内部。
必要时,还要增加在窗口中画图形图符的类、在窗口中选择项目的类、字体控制类、支持剪切和粘贴的类等。
与机器有关的操作实现应隐蔽在这些类中。
⑦根据图形用户界面进行设计
图形用户界面区分为字型、坐标系统和事件。
图形用户界面的字型是字体、字号、样式和颜色的组合。
坐标系统主要因素有原点(基准点)、显示分辨率、显示维数等。
事件则是图形用户界面程序的核心,操作将对事件做出响应,事件的工作方式有两种:
直接方式和排队方式。
所谓直接方式,是指每个窗口中的项目有它自己的事件处理程序,一旦事件发生,则系统自动执行相应的事件处理程序。
所谓排队方式,是指当事件发生时系统把它排到队列中,每个事件可用一些子程序信息来激发。
(3)任务管理部分的设计
所谓任务,是进程的别称,是执行一系列活动的一段程序。
当系统中有许多并发行为时,需要依照各个行为的协调和通信关系,划分各种任务,以简化并发行为的设计和编码。
而任务管理主要包括任务的选择和调整,它的工作有以下几种。
①识别事件驱动任务
一些负责与硬件设备通信的任务是事件驱动的,也就是说,这种任务可由事件来激发,而事件常常是当数据到来时发出一个信号。
②识别时钟驱动任务
以固定的时间间隔激发这种事件,以执行某些处理。
某些人机界面、子系统、任务、处理机或与其它系统需要周期性的通信。
③识别优先任务和关键任务
根据处理的优先级别来安排各个任务。
在系统中,有些操作具有高优先级,因此必须在很强的时间限制内完成;有些操作具有较低的优先级,可进行时间要求较低的处理(如后台处理)。
通常需要有一个附加的任务,把各个任务分离开来。
所谓关键任务是对系统的成败起关键作用的处理。
必须使用附加的任务来分离这种任务,并对其安全性仔细进行设计、编程和测试。
④识别协调者
当有三个或更多的任务时,应当增加一个附加任务,起协调者的作用。
它的行为可以用状态转换矩阵来描述。
这种任务仅用于协调任务。
⑤评审各个任务
必须对各个任务进行评审,确保它能满足选择任务的工程标准──事件驱动、时钟驱动、优先级/关键任务或协调者。
⑥定义各个任务
定义任务的工作主要包括:
它是什么任务、如何协调工作及如何通信。
它是什么任务:
为任务命名,并简要说明这个任务。
如何协调工作:
定义各个任务如何协调工作。
指出它是事件驱动还是时钟驱动。
对于事件驱动的任务,描述激发该任务的事件;对于时钟驱动的任务,指明激发之前所经过的时间间隔,同时指出是一次性