第五章_总体设计
第五章_总体设计
总体设计/概要设计基本目的:“系统如何实现”
- 划分组成系统的物理元素
- 设计软件的结构,确定模块构成和之间的联系
1. 设计过程由两个主要阶段组成(9个步骤)
系统设计阶段,确定系统的具体实现方案
设想供选择的方案
根据需求分析阶段得出的数据流图考虑各种可能的实现方案,力求从中选出最佳方案。
选取合理的方案
从前一步得到的一系列供选择的方案中选取若干个合理的方案。对每个合理的方案分析员都应该准备下列4份资料:
- 系统流程图;
- 组成系统的物理元素清单;
- 成本/效益分析;
- 实现这个系统的进度计划。
推荐最佳方案
分析员应该综合对比各种合理方案的利弊,推荐一个最佳的方案, 并且为推荐的方案制定详细地实现计划。
结构设计阶段,确定软件结构
功能分解
首先进行结构设计,然后进行过程设计。
- 结构设计确定程序由哪些模块组成, 以及这些模块之间的关系;$\quad$过程设计 确定每个模块的处理过程。
- 结构设计是总体设计阶段的任务,$\quad$过程设计是详细设计阶段的任务。
设计软件的结构
- 通常程序中的一个模块完成一个适当的子功能。应该把模块组织成良好的层次系统。软件结构可以用层次图或结构图来描绘。
- 如果数据流图已经细化到适当的层次,则可以直接从数据流图映射出软件结构,这就是面向数据流的设计方法。
设计数据库
对于需要使用数据库的那些应用系统,软件工程师应该在需求分析阶段所确定的系统数据需求的基础上,进一步设计数据库。
制定测试计划
在软件开发的早期阶段考虑测试问题,能促使软件设计人员在设计时注意提高软件的可测试性。
书写文档
应该用正式的稳定记录总体设计的结果,在这个阶段应该完成的文档通常有下述几种:
- 系统说明;
- 用户手册;
- 测试计划;
- 详细的实现计划;
- 数据库设计结果;
审查和复审
最后应该对总体设计的结果进行严格的技术审查和管理复审。
2. 设计原理:模块化、抽象、逐步求精、信息隐藏和局部化,和模块独立(耦合和内聚)
1. 模块化
- 模块:由边界元素限定的相邻程序元素的序列,还有一个总体标识符代表
- 模块化:把程序划分成独立命名且可独立访问的模块,每个模块完成一个子功能,把这些模块集成起来构成一个整体,可以完成指定的功能满足用户的需求。
为什么要模块化?
- 使一个复杂的大型程序能被人的智力所管理,软件应该具备的唯一属性。
- 如果一个大型程序仅由一个模块组成,它将很难被人所理解。
决定模块效率化的因素:
- 模块的数量;
- 模块之间的接口;
评价一种设计方法定义模块能力的五条标准:
- 模块可分解性
- 模块可组装性
- 模块可理解性
- 模块连续性
- 模块保护性
模块化的作用:
- 采用模块化原理可以使软件结构清晰,不仅容易设计也容易阅读和理解。
- 模块化使软件容易测试和调试, 因而有助于提高软件的可靠性。
- 模块化能够提高软件的可修改性。
- 模块化也有助于软件开发工程的组织管理。
2. 抽象
>
- 抽象: 现实世界中一定事物、状态或过程之间总存在着某些相似的方面(共性)。把这些相似的方面集中和概括起来,暂时忽略它们之间的差异,这就是抽象。
- 抽象就是抽出事物本质特性而暂时不考虑细节。
一般抽象过程:
- 处理复杂系统的惟一有效的方法是用层次的方式构造和分析它。
- 一个复杂的动态系统首先可以用一些高级的抽象概念构造和理解,这些高级概念又可以用一些较低级的概念构造和理解,如此进行下去,直至最低层次的具体元素。
- 例:过程抽象、数据抽象
软件工程抽象过程: 软件工程过程的每一步都是对软件解法的抽象层次的一次精化。
- 在可行性研究阶段,软件作为系统的一个完整部件;
- 在需求分析期间,软件解法是使用在问题环境内熟悉的方式描述的;
- 当由总体设计向详细设计过渡时,抽象的程度也就随之减少了;
- 最后,当源程序写出来以后,也就达到了抽象的最低层。
3. 逐步求精
为了能集中精力解决主要问题而尽量推迟对问题细节的考虑。逐步求精是人类解决复杂问题时采用的基本方法,也是许多软件工程技术的基础。
作用:
- 帮助软件工程师把精力集中在与当前开发阶段最相关的那些方面上,而忽略那些对整体解决方案来说虽然是必要的,然而目前还不需要考虑的细节。
- 确保每个问题都将被解决,而且每个问题都将在适当的时候被解决,但是,在任何时候一个人都不需要同时处理7个以上知识块。
4. 信息隐藏和局部化
- 信息隐藏: 应该这样设计和确定模块,使得一个模块内包含的信息(过程和数据)对于不需要这些信息的模块来说,是不能访问的。
- 局部化: 局部化的概念和信息隐藏概念是密切相关的。所谓局部化是指把一些关系密切的软件元素物理地放得彼此靠近。显然,局部化有助于实现信息隐藏。
信息隐藏和局部化的作用:
- “隐藏”意味着有效的模块化可以通过定义一组独立的模块而实现,这些独立的模块彼此间仅仅交换那些为了完成系统功能而必须交换的信息。
- 使用信息隐藏原理作为模块化系统设计的标准就会带来极大好处。因为绝大多数数据和过程对于软件的其他部分而言是隐藏的,在修改期间由于疏忽而引入的错误就很少可能传播到软件的其他部分。
5. 模块独立
模块独立的概念是模块化、抽象、信息隐藏和局部化概念的直接结果。
希望这样设计软件结构,使得每个模块完成一个相对独立的特定子功能,并且和其他模块之间的关系很简单。
重要性:
- 有效的模块化的软件比较容易开发出来。
- 独立的模块比较容易测试和维护。
模块独立程度的两个定性标准度量:
- 耦合衡量不同模块彼此间互相依赖(连接)的紧密程度。耦合要低,即每个模块和其他模块之间的关系要简单;
- 内聚衡量一个模块内部各个元素彼此结合的紧密程度。内聚要高,每个模块完成一个相对独立的子功能。
1. 耦合
>
- 耦合:是对一个软件结构内不同模块之间互连程度的度量。
- 要求:在软件设计中应该追求尽可能松散耦合的系统。
- 可以研究、测试或维护任何一个模块,而不需要对系统的其他模块有很多了解;
- 模块间联系简单,发生在一处的错误传播到整个系统的可能性很小;
- 模块间的耦合程度强烈影响系统的可理解性、可测试性、可靠性和可维护性。
耦合程度的度量
>
- 非直接耦合/完全独立(no direct coupling)
如果两个模块中的每一个都能独立地工作而不需要另一个模块的存在,那么它们完全独立。在一个软件系统中不可能所有模块之间都没有任何连接。
- 数据耦合(data coupling)
如果两个模块彼此间通过参数交换信息,而且交换的信息仅仅是数据,那么这种耦合称为数据耦合。
评价:
- 系统中至少必须存在这种耦合。一般说来,一个系统内可以只包含数据耦合。$\qquad$数据耦合是理想的目标。
- 维护更容易,对一个模块的修改不会是另一个模块产生退化错误。
- 控制耦合(control coupling)
如果两个模块彼此间传递的信息中有控制信息,这种耦合成为控制耦合。
评价:
- 控制耦合往往是多余的,把模块适当分解之后通常可以用数据耦合代替它。
- 被调用的模块需知道调用模块的内部结构和逻辑,降低了重用的可能性。
- 特征耦合(stamp coupling)
当把整个数据结构作为参数传递而被调用的模块只需要使用其中一部分数据元素时,就出现了特征耦合。
评价:被调用的模块可使用的数据多于它确实需要的数据,这将导致对数据的访问失去控制,从而给计算机犯罪提供了机会。
无论何时把指针作为参数进行传递,都应该仔细检查该耦合。
- 公共环境耦合(common coupling)
当两个或多个模块通过一个公共数据环境相互作用时,它们之间的耦合称为公共环境耦合。公共环境可以是全程变量、共享的通信区、内存的公共覆盖区、任何存储介质上的文件、物理设备等等。
类型:
- 一个模块往公告环境送数据,另一个模块从公共环境取数据。数据耦合的一种形式,是比较松散的耦合。
- 两个模块都既往公共环境送数据又从里面取数据,这种耦合比较紧密,介于数据耦合和控制耦合之间。
- 内容耦合(content coupling)
最高程度的耦合是内容耦合。如果出现下列情况之一,两个模块间就发生了内容耦合:
- 一个模块访问另一个模块的内部数据;
- 一个模块不通过正常入口转到另一个模块的内部;
- 两个模块有一部分程序代码重叠;
- 一个模块有多个入口。
耦合是影响软件复杂程度的一个重要因素。
应该采取下述设计原则:
- 尽量使用数据耦合,
- 少用控制耦合和特征耦合,
- 限制公共环境耦合的范围,
- 完全不用内容耦合。
2. 内聚
- 内聚: 标志一个模块内各个元素彼此结合的紧密程度,它是信息隐藏和局部化概念的自然扩展。简单地说,理想内聚的模块只做一件事情。
- 要求:设计时应该力求做到高内聚,通常中等程度的内聚也是可以采用的,而且效果和高内聚相差不多;但是,低内聚不要使用。
- 内聚和耦合是密切相关的,模块内的高内聚往往意味着模块间的松耦合。实践表明内聚更重要,应该把更多注意力集中到提高模块的内聚程度上。
内聚程度的度量
- 偶然内聚(coincidental cohesion)————低内聚
如果一个模块完成一组任务, 这些任务彼此间即使有关系, 关系也是很松散的, 就叫做偶然内聚。
评价:
- 模块内各元素之间没有实质性联系,很可能在一种应用场合需要修改这个模块,在另一种应用场合又不允许这种修改,从而陷入困境;
- 可理解性差,可维护性产生退化;
- 模块是不可重用的。
解决方案:将模块分成更小的模块, 每个小模块执行一个操作。
- 逻辑内聚(logical cohesion)————低内聚
如果一个模块完成的任务在逻辑上属于相同或相似的一类,则称为逻辑内聚。
评价:
- 接口难以理解, 造成整体上不易理解;
- 完成多个操作的代码互相纠缠在一起,即使局部功能的修改有时也会影响全局,导致严重的维护问题;
- 难以重用。
解决方案: 模块分解。
- 时间内聚(temporal cohesion)————低内聚
如果一个模块包含的任务必须在同一段时间内执行,就叫时间内聚。
评价:
- 时间关系在一定程度上反映了程序某些实质,所以时间内聚比逻辑内聚好一些。
- 模块内操作之间的关系很弱,与其他模块的操作却有很强的关联。
- 时间内聚的模块不太可能重用。
- 过程内聚(procedural cohesion)
- 如果一个模块内的处理元素是相关的,而且必须以特定次序执行,则称为过程内聚。
- 使用程序流程图作为工具设计软件时,常常通过研究流程图确定模块的划分,这样得到的往往是过程内聚的模块。
评价:
- 比时间内聚好,至少操作之间是过程关联的。
- 仍是弱连接,不太可能重用模块。
解决方案:分割为单独的模块,每个模块执行一个操作
- 通信内聚(communicational cohesion)
如果模块中所有元素都使用同一个输入数据和(或)产生同一个输出数据,则称为通信内聚。即在同一个数据结构上操作。
评价:模块中各操作紧密相连,比过程内聚更好。不能重用。
解决方案:分成多个模块,每个模块执行一个操作。
- 顺序内聚(sequential cohesion)
如果一个模块内的处理元素和同一个功能密切相关,而且这些处理必须顺序执行,则称为顺序内聚。
评价:根据数据流图划分模块时,通常得到顺序内聚的模块,这种模块彼此间的连接往往比较简单。
- 功能内聚(functional cohesion)
如果模块内所有处理元素属于一个整体,完成一个单一的功能,则称为功能内聚。功能内聚是最高程度的内聚。
评价:
- 模块可重用,应尽可能重用;
- 可隔离错误,维护更容易;
- 扩充产品功能时更容易。
七种内聚的优劣评分结果
类型 分数 高内聚 功能内聚 10分 顺序内聚 9分 中内聚 通信内聚 7分 过程内聚 5分 低内聚 时间内聚 3分 逻辑内聚 1分 偶然内聚 0分 设计时力争做到高内聚,并且能够辨认出低内聚的模块。
3. 启发准则
1. 改进软件结构提高模块独立性
通过模块分解或合并,降低耦合提高内聚。
两个方面:
- 模块功能完善化。 一个完整的模块包含:
- 执行规定的功能的部分
- 出错处理的部分
- 返回一个”结束标志”
- 消除重复功能, 改善软件结构。
2. 模块规模应该适中
- 经验表明,一个模块的规模不应过大,最好能写在一页纸内。通常规定50~100行语句,最多不超过500行。数字只能作为参考,根本问题是要保证模块的独立性。
- 过大的模块往往是由于分解不充分,但是进一步分解必须符合问题结构,一般说来,分解后不应该降低模块独立性。
- 过小的模块开销大于有效操作,而且模块数目过多将使系统接口复杂。
3. 深度、宽度、扇出和扇入都应适当
深度: 软件结构中控制的层数,它往往能粗略地标志一个系统的大小和复杂程度。
宽度: 软件结构内同一个层次上的模块总数的最大值。
扇出: 一个模块直接控制(调用)的模块数目。
扇入: 有多少个上级模块直接调用它。
4. 模块的作用域应该在控制域之内
- 模块的作用域: 定义为受该模块内一个判定影响的所有模块的集合。
- 模块的控制域: 这个模块本身以及所有直接或间接从属于它的模块的集合。
- 在一个设计得很好的系统中,所有受判定影响的模块应该都从属于做出判定的那个模块,最好局限于做出判定的那个模块本身及它的直属下级模块。
解决方案:
- 把模块 A 中的判定移到模块 M 中;
- 把模块 G 移到模块A下面,作为它的下级模块。
5. 力争降低模块接口的复杂程度
模块接口复杂是软件发生错误的一个主要原因。应该仔细设计模块接口,使得信息传递简单并且和模块的功能一致。
例:解一元二次方程的函数
- [ ] QUAD_ROOT(TBL, X)
- 其中数组 TBL 传送方程的系数
- 数组 X 送回求得的根
- [x] QUAD_ROOT(A, B, C, ROOT1, ROOT2)
6. 设计单入口单出口的模块
警告软件工程师不要使模块间出现内容耦合。当从顶部进入模块并且从底部退出来时,软件是比较容易理解的,因此也是比较容易维护的。
7. 模块功能应该可以预测
- 模块的功能应该能够预测,但也要防止模块功能过分局限。
- 功能可预测:如果一个模块可以当做一个黑盒子,只要输入的数据相同就产生同样的输出,这个模块的功能就是可以预测的。
4. 软件结构的图形工具:层次图(H图)、HIPO图、结构图
1. 层次图(H图)
层次图用来描绘软件的层次结构。很适于在自顶向下设计软件的过程中使用。
层次图和层次方框图的区别:
层次图 层次方框图 作用 描绘软件结构 描绘数据结构 矩形框 模块 数据元素 连线 调用关系 组成关系
2. HIPO图
- HIPO图是美国IBM公司发明的“层次图+输入/处理/输出图”的英文缩写。
- 为了能使HIPO图具有可追踪性,在H图(层次图)里除了最顶层的方框之外,每个方框都加了编号。
和H图中每个方框相对应,应该有一张IPO图描绘这个方框代表的模块的处理过程。模块在H图中的编号便于追踪了解这个模块在软件结构中的位置。
3. 结构图
基本符号:
- 方框代表一个模块;
- 方框之间的直线表示模块的调用关系;
- 尾部是空心圆箭头表示传递的是数据;
- 尾部是实心圆箭头表示传递的是控制信息。
附加符号:
- 选择调用: 判定为真时调用 A,为假时调用 B。
- 模块 M 循环调用模块 A、B、C。
注意:
- 层次图和结构图并不严格表示模块的调用次序,多数人习惯从左到右画模块;
- 层次图和结构图并不指明合适调用下层模块;
层次图和结构图只表明一个模块调用哪些模块,没有表示模块内还有没有其他成分;
通常用层次图作为描绘软件结构的文档;
由层次图导出结构图的过程可以作为检查设计正确性和评价模块独立性的好方法。
5. 面向数据流的设计方法:变换流和事务流
1. 变换流
信息沿输入通路进入系统,同时由外部形式变换成内部形式,进入系统的信息通过变换中心,经加工处理以后再沿输出通路变换成外部形式离开软件系统。
2. 事务流
数据沿输入通路到达一个处理T,T根据输入数据的类型在若干个动作序列中选出一个来执行。处理T称为事务中心,它完成下述任务:
- 接收输入数据
- 分析每个事务以确定它的类型;
- 根据事务类型选取一条活动通路。
3. 变换分析
变换分析是一系列设计步骤的总称,经过这些步骤把具有变换流特点的数据流图按预先确定的模式映射成软件结构。
设计步骤:
- 复查基本系统模型
- 确保系统的输入数据和输出数据符合实际。
- 复查并精化数据流图
- 对需求分析阶段得出的数据流图认真复查,并且在必要时进行精化。
- 不仅要确保数据流图给出了目标系统的正确逻辑模型,而且应该使数据流图中每个处理都代表一个规模适中相对独立的子功能。
- 确定数据流图具有变换特性还是事务特性
- 一个系统中的所有信息流都可以认为是变换流,但是,当遇到有明显事务特性的信息流时,建议采用事务分析方法进行设计。确定数据流的全局特性和局部特性。
- 确定输入流和输出流的边界,从而孤立出变换中心
- 输入流和输出流的边界和对它们的解释有关,不同设计人员可能会在流内选取稍微不同的点作为边界的位置。
- 完成“第一级分解”。
- 位于软件结构最顶层的控制模块 $C_m$ 协调下述从属的控制功能:
- 输入信息处理控制模块 $C_a$ ,管理对内部形式的数据的所有操作;
- 变换中心控制模块 $C_t$,管理对内部形式的数据的所有操作;
- 输出信息处理控制模块 $C_e$,协调输出信息的产生过程。
- 完成“第二级分解”
- 处理映射成软件结构中一个适当的模块。完成第二级分解的方法是:
- 从变换中心的边界开始逆着输入通路向外移动,把输入通路中每个处理映射成软件结构中Ca控制下的一个低层模块;
- 然后沿输出通路向外移动,把输出通路中每个处理映射成直接或间接受模块Ce控制的一个低层模块;
- 最后把变换中心内的每个处理映射成受Ct控制的一个模块。
- 使用设计度量和启发式规则对第一次分割得到的软件结构进一步精化
为了产生合理的分解,得到尽可能高的内聚、尽可能松散的耦合,为了得到一个易于实现、易于测试和易于维护的软件结构,应该对初步分割得到的模块进行再分解或合并。
4. 事务分析
虽然在任何情况下都可以使用变换分析方法设计软件结构,但是在数据流具有明显的事务特点时,也就是有一个明显的“发射中心”(事务中心)时,还是以采用事务分析方法为宜。
- 事务分析的设计步骤和变换分析的设计步骤大部分相同或类似,主要差别仅在于由数据流图到软件结构的映射方法不同;
- 由事务流映射成的软件结构包括一个接收分支和一个发送分支;
- 映射出接收分支结构的方法和变换分析映射出输入结构的方法很相像,即从事务中心的边界开始,把沿着接收流通路的处理映射成模块;
- 发送分支的结构包含一个调度模块,它控制下层的所有活动模块;然后把数据流图中的每个活动流通路映射成与它的流特征相对应的结构
5. 总结:
- 一般说来,如果数据流不具有显著的事务特点,最好使用变换分析;反之,如果具有明显的事务中心,则应该采用事务分析技术。
- 机械地遵循变换分析或事务分析的映射规则,可能会得到一些不必要的控制模块,如果它们确实用处不大,那么可以而且应该把它们合并。
- 如果一个控制模块功能过分复杂,这应该分解为两个或多个控制模块,或者增加中间层次的控制模块。