划分出组成系统的物理元素——程序、文件、数据库、人工过程和文档等等,但是每个物理元素仍然处于黑盒子级。
体系结构设计
设计软件的结构,确定系统中每个程序是由哪些模块组成的,以及这些模块相互间的关系。
两个主要阶段组成:
系统设计阶段:确定系统的具体实现方案;
结构设计阶段:确定软件结构。
典型的总体设计过程包括下述9个步骤:
1.设想供选择的方案
以数据流图为基础,寻找实现目标系统的各种不同的方案。
2.选取合理的方案
至少选取低成本、中等成本和高成本的三种方案。
对每个合理的方案分析员都应该准备下列资料:
(1)系统流程图;
(2)组成系统的物理元素清单;
(3)成本/效益分析;
(4)实现这个系统的进度计划。
3.推荐最佳方案
4.功能分解
确定软件结构,从实现角度把复杂的功能进一步分解。
通常分为两个阶段完成:
结构设计:确定程序由哪些模块组成,以及这些模块之间的关系。总体设计阶段的任务
过程设计:确定每个模块的处理过程。
详细设计阶段的任务
功能分解导致数据流图的进一步细化,同时用IPO图简要描述细化后每个处理的算法。
5.设计软件结构
把模块组织成良好的层次系统,顶层模块调用它的下层模块以实现程序的完整功能,每个下层模块再调用更下层的模块,从而完成程序的一个子功能,最下层的模块完成最具体的功能。
软件结构(即由模块组成的层次系统)可以用层次图或结构图来描绘,第5.4节将介绍这些图形工具。
如果数据流图已经细化到适当的层次,可以直接从数据流图映射出软件结构,这就是第5.5节中将要讲述的面向数据流的设计方法。
6.设计数据库
7.制定测试计划
开发早期考虑测试问题,可提高软件的可测试性。
8.书写文档
(1)系统说明。主要内容包括:
系统流程图描绘的系统构成方案,组成系统的物理元素清单,成本/效益分析;
精化的数据流图,用层次图或结构图描绘的软件结构,用IPO图描述的各个模块的算法,模块间的接口关系等。
(2)用户手册
(3)测试计划
(4)详细的实现计划
(5)数据库设计结果
9.审查和复审
好的设计准则
模块化
抽象
逐步求精
信息隐藏和局部化
模块独立
模块:是由边界元素限定的相邻程序元素(例如,数据说明,可执行的语句)的序列,而且有一个总体标识符代表它。按照模块的定义,过程、函数、子程序和宏等,都可作为模块。
模块化:把程序划分成独立命名且可独立访问的模块,每个模块完成一个子功能,把这些模块集成起来构成一个整体,可以完成指定的功能,满足用户的需求。
模块化的根据:把复杂的问题分解成许多容易解决的小问题,原来的问题也就容易解决了。
模块化的后果:随着模块数目增加,设计模块间接口所需要的工作量也将增加。
根据这两个因素,得出了图中的总成本曲线。每个程序都有一个最适当的模块数目M,使得系统的开发成本最小。
图5.1模块化和软件成本
使软件结构清晰,容易设计也容易阅读和理解。
提高软件的可靠性。
程序错误局限在有限的模块中,使软件容易测试和调试。
提高了软件的可修改性。
即使有变动,往往只涉及少数几个模块,
有助于软件开发工程的组织管理。
一个大型程序由许多程序员分工编写,分配技术熟练的程序员编写困难的模块。
抽象:把事务相似的方面集中和概括起来,暂时忽略它们之间的差异。
抽象就是抽出事物的本质特性而暂时不考虑它们的细节。
Miller法则:一个人在任何时候都只能把注意力集中在(7±2)个知识块上。
逐步求精是人类解决复杂问题时采用的基本方法。
逐步求精实际上是自顶向下的细化过程。
高抽象级别定义的功能,仅作概念性地描述,没有提供功能的内部工作情况。
求精要求设计者细化原始陈述,随着每个后续求精(即细化)步骤的完成,提供越来越多的细节。
抽象与求精是一对互补的概念。
抽象使得设计者能够说明过程和数据,同时却忽略低层细节。
求精则帮助设计者在设计过程中逐步揭示出低层细节。
这两个概念都有助于设计者在设计演化过程中创造出完整的设计模型。
信息隐藏:一个模块内包含的信息(过程和数据)对于不需要这些信息的模块来说,是不能访问的。
局部化:是指把一些关系密切的软件元素物理地放得彼此靠近。
在模块中使用局部数据元素是局部化的一个例子。有助于实现信息隐藏。
模块独立的概念是模块化、抽象、信息隐藏和局部化概念的直接结果。
模块独立:具有独立功能而且和其他模块之间没有过多的相互作用的模块。
模块独立的重要性:
第一,具有独立模块的软件比较容易开发。
第二,独立模块比较容易测试和维护。
错误传播范围小,容易测试与修改,需要扩充功能时能够“插入”模块。
模块独立程度的度量标准:内聚和耦合。
耦合:模块间互相依赖(连接)的紧密程度;
内聚:模块内部各个元素彼此结合的紧密程度。
1.耦合
影响耦合强度的因素
一个模块对另一个模块的引用
一个模块向另一个模块传递的数据量
一个模块施加到另一个模块的控制的数量
模块之间接口的复杂程度
耦合的类型
内容耦合强
公共耦合|
控制耦合|
标记耦合↓
数据耦合弱
在软件设计中应该追求尽可能松散耦合。
对模块的测试或维护时,不需要对系统的其他模块有很多了解。
此外,由于模块间联系简单,发生在一处的错误传播到整个系统的可能性就很小。
因此,模块间的耦合程度强烈影响系统的可理解性、可测试性、可靠性和可维护性。
数据耦合
数据耦合:模块彼此间通过参数交换信息,交换的信息仅仅是数据。
数据耦合是低耦合。系统中至少必须存在这种耦合,因为只有当某些模块的输出数据作为另一些模块的输入数据时,系统才能完成有价值的功能。
一般说来,一个系统内可以只包含数据耦合。
标记耦合(Stampcoupled)
若两个模块间传递的参数中至少有一个是数据结构,如字符串或记录,并且在模块中仅用到该数据结构中的部分元素,则称这两个模块之间存在标记耦合。
在这种情况下,被调用的模块可以使用的数据多于它确实需要的数据,将导致对数据的访问失去控制,从而给计算机犯罪提供了机会。
控制耦合(Controlcoupled)
控制耦合:一个模块向另一个模块传递控制信息,接收信息的模块的动作根据信息值进行调整。
控制耦合是中等程度的耦合,它增加了系统的复杂程度。在把模块适当分解之后通常可以用数据耦合代替它。
公共耦合(Commoncoupled)
两个模块共享全局的数据区域,称他们为公共耦合。
不要使用全局变量
公共耦合
耦合的复杂程度随耦合模块的个数而变化,随个数的增加显著增加。
两个模块的公共耦合有两种可能:
(1)一个模块往公共环境送数据,另一个模块从公共环境取数据。这是数据耦合的一种形式,是比较松散的耦合。
(2)两个模块都既往公共环境送数据又从里面取数据,这种耦合比较紧密,介于数据耦合和控制耦合之间。
内容耦合(Contentcoupled)
内容耦合的三种情况:
一个模块修改另一个模块的语句(Lisp具有此种能力)
一个模块引用或者修改另一个模块内部的数据
一个模块不通过正常入口而跳转到另一个模块的内部
模块独立性与耦合的关系
总之,耦合是影响软件复杂程度的一个重要因素。应该采取下述设计原则:
尽量使用数据耦合,少用控制耦合和标记耦合,限制公共环境耦合的范围,完全不用内容耦合。
2.内聚
内聚标志一个模块内各个元素彼此结合的紧密程度。理想内聚的模块只做一件事情。
度量一个模块内部各成分之间相互关联的强度
偶然内聚弱
逻辑内聚
过程内聚↓
通信内聚
顺序内聚
功能内聚强
偶然内聚(coincidentalcohesion)
如果一个模块的各成分之间毫无关系,则称为偶然内聚。
逻辑内聚(logicallycohesive)
Consideramodulecalled"On_Really_Bad_Failure"thatisinvokedwhenaReally_Bad_Failurehappens.Themoduleperformsseveraltasksthatarenotfunctionallysimilarorlogicallyrelated,butalltasksneedtohappenatthemomentwhenthefailureoccurs.
Themodulemight
cancelalloutstandingrequestsforservices
cutpowertoallassemblylinemachines
notifytheoperatorconsoleofthefailure
makeanentryinadatabaseoffailurerecords
过程内聚(procedurallycohesive)
如果一个模块内部的各个处理成分必须以特定的次序执行,则称为过程内聚。
通信内聚(communicationallycohesive)
如果一个模块的所有成分都操作同一数据集或生成同一数据集,则称为通信内聚。
顺序内聚(sequentiallycohesive)
功能内聚(functionallycohesive)
模块的所有成分对于完成单一的功能都是基本的。
设计时尽量使用高内聚,低耦合模块。
高内聚:尽量使用内聚度高的模块;中内聚也可;低内聚很坏,不要采用。
中内聚:过程内聚,通信内聚
高内聚:顺序内聚,功能内聚;
低耦合:尽量使用数据耦合,少用控制耦合和标记耦合,限制公共耦合的范围,完全不用内容耦合。
改进软件设计,提高软件质量的途径。
改进软件结构提高模块独立性
模块规模应该适中
深度、宽度、扇出和扇入应适中
模块的作用域应该在控制域之内
力争降低模块接口的复杂性
设计单入口和单出口的模块
模块功能应该可以预测
1.改进软件结构提高模块独立性
降低耦合
提高内聚
2.模块规模应该适中
模块规模:
不超过60行
超过30,可理解程度迅速下降
模块数量:
适中
模块过大:往往是由于分解不充分。
模块过小:导致模块数目过多,使系统接口复杂。可以把它合并到上级模块中去。
3.深度、宽度、扇出和扇入都应适当
深度:表示软件结构中控制的层数。
能粗略地标志一个系统的大小和复杂程度。如果层数过多,应考虑管理模块是否过分简单,能否适当合并。
宽度:软件结构内同一个层次上的模块总数的最大值。
宽度越大系统越复杂。对宽度影响最大的因素是模块的扇出。
扇出:是一个模块直接控制(调用)的模块数目。
扇出过大意味着模块过分复杂,需要控制和协调的下级模块过多;扇出过小(例如总是1)也不好。
通常是3或4(上限是5~9)。
扇出太大:缺乏中间层次,应适当增加中间层次的控制模块。
扇出太小:把下级模块进一步分解成若干个子功能模块,或者合并到它的上级模块中去。
分解或合并模块应符合问题结构,不能违背模块独立原理。
扇入:表明有多少个上级模块。扇入越大则共享该模块的上级模块数目越多,这是有好处的。
好的软件结构通常顶层扇出比较高,中层扇出较少,底层模块有高扇入。
系统的模块结构呈现为“葫芦形”。
4.模块的作用域应该在控制域之内
模块的作用域:受该模块内一个判定影响的所有模块的集合。
模块的控制域:模块本身以及所有直接或间接从属于它的模块的集合。
例如,在图5.2中模块A的控制域是A、B、C、D、E、F等模块的集合。
受判定影响的模块应在做出判定的那个模块的控制域之内。
图5.2模块的作用域和控制域
5.力争降低模块接口的复杂程度
应该仔细设计模块接口,使得信息传递简单并且和模块的功能一致。
6.设计单入口单出口的模块
使模块间避免出现内容耦合。当从顶部进入模块并且从底部退出来时,软件是比较容易理解的,因此也是比较容易维护的。
7.模块功能应该可以预测
只要输入的数据相同就产生同样的输出,这个模块的功能就是可以预测的。
带有内部“存储器”的模块的功能可能是不可预测的,不宜测试与维护。
层次图:用来描绘软件的层次结构。
形式和描绘数据结构的层次方框图相同,但表现的内容却完全不同。
层次图中的一个矩形框代表一个模块,方框间的连线表示调用关系
层次方框图中的一个矩形框代表数据的子集,方框间的连线表示组成关系。
图5.3是层次图的一个例子。
图5.3正文加工系统的层次图
HIPO:“层次图加输入/处理/输出图”的英文缩写。
为了能使HIPO图具有可追踪性,在H图(层次图)里除了最顶层的方框之外,每个方框都加了编号。编号规则和数据流图的编号规则相同。
例如,图5.3加了编号后得到图5.4。
H图中每个方框对应一张IPO图,描绘这个方框代表的模块的处理过程。每张IPO图内都应标出它所描绘的模块在H图中的编号。
图5.4带编号的层次图(H图)
结构图和层次图类似,也是描绘软件结构的图形工具。
结构图基本符号:
方框——模块
方框间连线——模块调用关系(上方的模块调用下方的模块)
带注释的箭头——模块间传递的信息
箭头尾部空心圆——数据信息
箭头尾部实心圆——控制信息
图5.5结构图的例子——产生最佳解的一般结构
还有一些附加的符号,可以表示模块的选择调用或循环调用。
图5.6判定为真时调用A,为假时调用B
图5.7模块M循环调用模块A、B、C
面向数据流的设计方法:把数据流图中的信息流映射成软件结构。信息流的类型决定了映射的方法。信息流有下述两种类型。
1.变换流
变换流:具有较明显的输入、变换(或称主加工)和输出界面的数据流图。
参看图5.8
图5.8变换流
2.事务流
事务流:数据沿输入通路到达一个处理T,这个处理根据输入数据的类型在若干个动作序列中选出一个来执行。
此时数据流图形状如图5.9,是“以事务为中心的”。
图5.9中的处理T称为事务中心,它完成下述任务:
(1)接收输入数据(输入数据又称为事务);
(2)分析每个事务以确定它的类型;
(3)根据事务类型选取一条活动通路。
图5.9事务流
3.设计过程
图5.10(见书96页)说明了使用面向数据流方法逐步设计的过程。
变换分析:把数据流图按预先确定的模式映射成软件结构的一系列设计步骤的总称。
下面通过一个例子说明变换分析的方法。
1.例子
考虑汽车数字仪表板的设计。
假设的仪表板将完成下述功能:
(1)通过模数转换实现传感器和微处理机接口;
(2)在发光二极管面板上显示数据;
(3)指示每小时英里数(mph),行驶的里程,每加仑油行驶的英里数(mpg)等等;
(4)指示加速或减速;
(5)超速警告:如果车速超过55英里/小时,则发出超速警告铃声。
在需求分析阶段建立起相应的数据流图。
2.设计步骤
第1步复查基本系统模型。
复查的目的是确保系统的输入数据和输出数据符合实际。
第2步复查并精化数据流图。
应该对需求分析阶段得出的数据流图认真复查,并且在必要时进行精化。使数据流图中每个处理都代表一个规模适中相对独立的子功能。
假设在需求分析阶段产生的数字仪表板系统的数据流图如图5.11(见书97页)所示。
第3步确定数据流图具有变换特性还是事务特性。
一般地说,一个系统中的所有信息流都可以认为是变换流,但是,当遇到有明显事务特性的信息流时,建议采用事务分析方法进行设计。
从图5.11看出,数据沿着两条输入通路进入系统,然后沿着5条通路离开,没有明显的事务中心。
因此可以认为这个信息流具有变换流的总特征。
第4步确定输入流和输出流的边界,从而孤立出变换中心。
对于汽车数字仪表板的例子,设计人员确定的流的边界如图5.12(见书98页)所示。
第5步完成“第一级分解”。
分解:就是分配控制的过程,对控制的自顶向下的分配----软件结构。
图5.13说明了第一级分解的方法。位于软件结构最顶层的控制模块Cm协调下述从属的控制功能:
输入信息处理控制模块Ca:协调对所有输入数据的接收;
变换中心控制模块Ct:管理对内部形式的数据的所有操作;
输出信息处理控制模块Ce:协调输出信息的产生过程。
数据流图被映射成一个特殊的软件结构,这个结构控制输入、变换和输出等信息处理过程。
图5.13第一级分解的方法
对于数字仪表板的例子,第一级分解得出的结构如图5.14所示。每个控制模块的名字表明了为它所控制的那些模块的功能。
图5.14数字仪表板系统的第一级分解
第6步完成“第二级分解”。
第二级分解:就是把数据流图中的每个处理映射成软件结构中一个适当的模块。
完成第二级分解的方法是:
从变换中心的边界开始沿着输入通路向外移动,把输入通路中每个处理映射成软件结构中Ca控制下的一个低层模块;
然后沿输出通路向外移动,把输出通路中每个处理映射成直接或间接受模块Ce控制的一个低层模块;
最后把变换中心内的每个处理映射成受Ct控制的一个模块。
图5.15表示进行第二级分解的普遍途径。
图5.15第二级分解的方法
对于数字仪表板系统的例子,第二级分解的结果分别用图5.16,5.17和5.18描绘。
这3张图表示对软件结构的初步设计结果。
图5.16未经精化的输入结构
图5.17未经精化的变换结构
图5.18未经精化的输出结构
第7步使用设计度量和启发式规则对第一次分割得到的软件结构进一步精化。
根据模块独立原理进行精化。得到尽可能高的内聚、尽可能松散的耦合。需对初步分割得到的模块进行再分解或合并。
具体到数字仪表板的例子,对于从前面的设计步骤得到的软件结构,还可以做许多修改:
输入结构中的模块“转换成rpm”和“收集sps”可以合并;
模块“确定加速/减速”可以放在模块“计算mph”下面,以减少耦合;
模块“加速/减速显示”可以相应地放在模块“显示mph”的下面。
经过上述修改后的软件结构画在图5.19中。
图5.19精化后的数字仪表板系统的软件结构
在数据流具有明显的事务特点时,也就是有一个明显的“发射中心”(事务中心)时,还是以采用事务分析方法为宜。
事务分析的设计步骤和变换分析的设计步骤大部分相同或类似,主要差别仅在于:
由数据流图到软件结构的映射方法不同。
由事务流映射成的软件结构包括一个接收分支和一个发送分支。
接收分支结构
映射方法和变换分析映射出输入结构的方法很相像。
从事务中心的边界开始,把沿着接收流通路的处理映射成模块。
发送分支结构
包含一个调度模块,它控制下层的所有活动模块;
然后把数据流图中的每个活动流通路映射成与它的流特征相对应的结构。
图5.20说明了上述映射过程。
图5.20事务分析的映射方法
对第一次分割得到的软件结构,总可以根据模块独立原理和启发式设计规则进行优化。
为了产生合理的分解,得到尽可能高的内聚﹑尽可能松散的耦合,最重要的是,为了得到一个易于实现﹑易于测试和易于维护的软件结构,应该对初步分割得到的模块进行再分解或合并。
注意,设计优化应该力求做到在有效的模块化的前提下使用最少量的模块,以及在能够满足信息要求的前提下使用最简单的数据结构。
总体设计阶段的基本目的:是用比较抽象概括的方式确定系统如何完成预定的任务,确定系统的物理配置方案,确定组成系统的每个程序的结构。
总体设计阶段主要由两个小阶段组成。
首先需要进行系统设计;然后进行软件结构设计,确定软件由哪些模块组成以及这些模块之间的动态调用关系。
层次图和结构图是描绘软件结构的常用工具。
在进行软件结构设计时应该遵循的最主要的原理是模块独立原理。
抽象和求精是一对互补的概念。在进行软件结构设计时就是由抽象到具体地构造出软件的层次结构。
软件工程师在开发软件的长期实践中积累了丰富的经验,总结这些经验得出一些很有参考价值的启发式规则。
自顶向下逐步求精是进行软件结构设计的常用途径;
如果已经有了详细的数据流图,也可以使用面向数据流的设计方法,由数据流图映射出软件结构。
这样映射出来的只是软件的初步结构,还必须根据设计原理并且参考启发式规则,认真分析和改进软件的初步结构,以得到质量更高的模块和更合理的软件结构。