4.1 软件设计过程
从工程管理的角度来看,软件设计分两步完成:
- 概要设计,将软件需求转化为数据结构和软件的系统结构。
- 详细设计,即过程设计。通过对系统结构进行细化,得到软件的详细数据结构和算法。
从工程管理角度看软件设计过程
从技术角度来看,软件设计包括:
- 数据设计:将实体关系图中描述的对象和关系,以及数据字典中描述的详细数据内容转化为数据结构的定义。
- 体系结构设计:划分软件系统模块及模块之间的关系。
- 接口设计:根据数据流图定义软件内部各成份之间、软件与其它协同系统之间及软件与用户之间的交互机制。
- 过程设计(即详细设计):把结构成份(模块)转换成软件的过程性描述。
把分析模型转换成软件的过程
软件设计是后续开发及软件维护工作的基础,没有设计的软件系统是一个不稳定的系统。
目标系统的运行环境
在设计目标系统时,软件设计人员要充分认识和分析目标系统的运行环境,以便在设计时考虑运行的约束条件及系统接口。
目标系统的运行环境
4.2 概要设计的目标和任务
4.2.1 概要设计的目标
概要设计又称为总体设计,其基本目的就是回答“概括地说系统应该如何实现”。
软件设计的目标,就是为系统制定总的蓝图,权衡各种技术和实施方法的利弊,合理利用各种资源,精心规划出系统总的设计方案。这是一个将软件系统需求转换为目标系统体系结构的过渡过程。
在该阶段,软件设计人员审查可行性研究报告、需求规格说明书,在此基础上将系统划分为层次结构和模块,决定各模块的功能、模块的调用关系。
4.2.2 概要设计的任务
概要设计的主要任务是把需求分析得到的 DFD 转换为软件结构和数据结构。
设计软件结构的具体任务是:将一个复杂系统按功能进行模块划分、建立模块的层次结构及调用关系、确定模块间的接口及人机界面等。
数据结构设计包括数据特征的描述、确定数据的结构特性以及数据库的设计。
4.2.3 概要设计的具体任务
概要设计的具体任务包括:
- 制定软件设计规范;
- 软件体系结构设计;
- 处理方式设计;
- 数据结构设计;
- 可靠性设计;
- 编写概要设计说明书;
- 概要设计评审;
4.3 概要设计原则
概要设计要遵循的原则有:模块化;抽象;自顶向下,逐步细化;信息隐蔽;模块独立性。其中,模块独立性是最核心的原则。
4.3.1 模块化
一个软件系统可按功能不同划分成若干功能模块。软件系统的层次结构正是模块化的具体体现。
把一个大而复杂的软件系统划分成易于理解的比较单纯的模块结构,这些模块可以被组装起来以满足整个问题的需求。
模块化
模块:是组成目标系统逻辑模型和物理模型的基本单位,它的特点是可以组合、分解和更换。系统中任何一个处理功能都可以看成是一个模块。
根据模块功能具体化程度的不同,可以分为逻辑模块和物理模块。在系统逻辑模型中定义的处理功能可视为逻辑模块,物理模块是逻辑模块的具体化,可以是一个计算机程序、子程序或若干条程序语句,也可以是人工过程的某项具体工作。
一个模块应具备以下4个要素:
- 输入和输出:模块的输入来源和输出去向都是同一个调用者,即一个模块从调用者那里取得输入,进行加工后再把输出返回调用者。
- 处理功能:指模块把输入转换成输出所做的工作。
- 内部数据:指仅供该模块本身引用的数据。
- 程序代码:指用来实现模块功能的程序。
前两个要素是模块的外部特性,即反映了模块的外貌。后两个要素是模块的内部特性。在结构化设计中,主要考虑的是模块的外部特性,其内部特性只作必要了解,具体的实现将在系统实施阶段完成。
Meyer 的良好模块设计标准:
- 模块可分解性:可将系统按问题/子问题分解的原则分解成系统的模块层次结构。
- 模块可组装性:可利用已有的设计构件组装成新系统,不必一切从头开始。
- 模块可理解性:一个模块可不参考其他模块而被理解。
- 模块连续性:对软件需求的一些微小变更只导致对某个模块的修改而整个系统不用大动。
- 模块保护:将模块内出现异常情况的影响范围限制在模块内部。
问题复杂性、开发工作量和模块数之间的关系
设 C(x) 为问题 x 所对应的复杂度函数,E(x) 为解决问题 x 所需要的工作量函数。对于两个问题 P1 和P2,如果:
C(P1)>C(P2)
即问题 P1 的复杂度比 P2 高,则显然有:
E(P1)>E(P2)
即解决问题 P1 比 P2 所需的工作量大。
根据解决一般问题的经验,规律为:
C(P1+P2)>C(P1)+C(P2)
即解决由多个问题复合而成的大问题的复杂度大于单独解决各个问题的复杂度之和,则:
E(P1+P2)> E(P1)+E(P2)
模块大小、模块数目与费用的关系
如果模块是相互独立的,当模块变得越小,每个模块花费的工作量越少;但当模块数增加时,模块间的联系也随之增加,把这些模块联接起来的工作量(接口成本)也随之增加。
因此,存在一个模块个数 M ,它使得总的开发成本达到最小。
实践证明,一般人们能够同时考虑的问题个数为 7 ± 2 ,因此,一个软件项目划分 5-9 个模块较好。
模块分割方法
- 横向分割,根据输入、处理、输出等功能的不同来分割模块。
- 纵向分割,根据系统对信息处理过程中不同的阶段来分割模块。
4.3.2 抽象
人类在认识复杂现象的过程中使用的最强有力的思维工具是抽象。人们在实践中认识到,现实世界中一定事物、状态或过程之间总存在着某些相似的方面(共性)。抽象就是抽出事物的本质特性而暂时不考虑它们的细节。这样可以集中精力分析事物的主要问题,而细节问题靠进一步细化。
在软件工程过程中,从系统定义到实现,每进展一步都可以看做是对软件解决方案的抽象化过程的一次细化。
而在从概要设计到详细设计的过程中,抽象化的层次逐次降低。当产生源程序代码时到达最低的抽象层次。
4.3.3 自顶向下,逐步细化
将软件的体系结构按自顶向下的方式,对各个层次的过程细节和数据细节逐层细化,直到用程序设计语言的语句能够实现为止,从而最后确立整个软件的体系结构。
系统结构图
4.3.4 信息隐蔽
信息隐蔽是指一个模块的实现细节对于其它模块来说是隐蔽的。就是说,模块中所包含的信息(包括数据和过程)不允许其它不需要这些信息的模块使用。
通过信息隐蔽,可定义和实施对模块的过程细节和局部数据结构的存取限制。如定义公共变量和私有变量。
信息隐蔽的概念类似于软件开发中的「最小特权原则」:
最小特权原则/最小授权或最小暴露原则:在软件设计中,应该最小限度地暴露必要内容,而将其他内容都 “隐藏” 起来,比如某个模块或对象的 API 设计。
4.3.5 模块独立性
模块独立性是指软件系统中每个模块只涉及软件要求的具体的子功能, 而和软件系统中其它模块的接口是简单的。
度量模块独立性有两个准则:
- 耦合: 耦合是模块间互相联系的紧密程度的度量。它取决于各个模块之间接口的复杂程度,一般由模块之间的调用方式、传递信息的类型和数量来决定。
- 内聚:内聚是一个模块内部各个元素彼此结合的紧密程度的度量。
4.3.5.1 模块耦合
块间耦合:耦合性是程序结构中各个模块之间相互关联的度量,它取决于各个模块之间接口的复杂程度、调用模块的方式以及哪些信息通过接口。
耦合性
非直接耦合:也称偶然耦合,是指两个模块之间没有直接关系,它们之间的联系完全是通过主模块的控制和调用来实现的。非直接耦合的模块独立性最强。
非直接耦合
数据耦合:一个模块访问另一个模块时,彼此之间通过参数交换信息,且局限于数据信息(非控制信息)。一个好的软件系统,都需要进行各种数据的传输,某些模块的输出数据作为另一模块的输入数据。
数据耦合
标记耦合:一组模块通过参数表传递记录信息,这组模块共享了该记录,就是标记耦合。传递的记录是某一数据结构的子结构,而不是简单变量。在软件设计时应尽量避免这种耦合。
标记耦合
控制耦合:如果一个模块通过传送控制信息来控制另一模块的功能,就是控制耦合。控制耦合属于中等程度的耦合,它增加了系统的复杂性。
控制耦合
外部耦合:一组模块都访问同一全局简单变量而不是同一全局数据结构,而且不是通过参数表传递该全局变量的信息,则称之为外部耦合。
公共耦合:若一组模块都访问同一个公共数据环境,则它们之间的耦合就称为公共耦合。公共数据环境可以是全局数据结构、共享的通信区、内存的公共覆盖区等。
公共耦合的复杂程度随耦合模块的个数增加而显著增加。若只是两模块间有公共数据环境,则公共耦合有两种情况:松散的公共耦合和紧密的公共耦合。
- 松散的公共耦合:一个模块往公共数据区传送数据,而另一个模块从公共数据区接收数据。
- 紧密的公共耦合:两个模块既往公共数据区传送数据,又从公共数据区接收数据。
松散的公共耦合
紧密的公共耦合
内容耦合
如果发生下列情形之一,则两个模块之间就发生了内容耦合:
- 一个模块直接访问另一个模块的内部数据;
- 一个模块不通过正常入口转到另一模块内部;
- 两个模块有一部分程序代码重迭;
- 一个模块有多个入口。
内容耦合
软件设计应追求尽可能松散耦合,避免强耦合,这样模块间的联系就越小,模块的独立性就越强,对模块的测试、维护就越容易。
因此建议:尽量使用数据耦合,少用控制耦合,限制公共耦合,完全不用内容偶合。
4.3.5.2 模块内聚
模块内聚分为7级:
模块内聚
偶然内聚:当模块内部各元素之间没有联系,或者即使有联系也很松散。则称这种模块为偶然内聚模块。偶然内聚存在很大缺点,它不利于程序的修改与维护。
偶然内聚
逻辑内聚:如果一个模块中包含多个逻辑上相关的功能,每次被调用时,根据传递给该模块的判定参数来确定模块应执行的功能,称作逻辑内聚。
逻辑内聚模块中各功能存在着某种相关的联系,但它执行的不是一种功能,而是多种功能,这样往往增加了软件修改和维护的难度。
逻辑内聚
时间内聚:如果一个模块所包含的任务必须在同一时间内执行称作时间内聚。如初始化模块,对各种变量、数据、栈和寄存器等都在开始执行前期的同一时间段内执行。
过程内聚:如果一个模块内的处理是相关的,而且必须以特定次序执行,则称为过程内聚。
例如,把流程图中的循环部分、判定部分、计算部分分成三个模块,则每个模块都是过程内聚模块。
通信内聚:如果一个模块各功能部分都使用了相同的输入数据,或产生了相同的输出数据,则称为通信内聚。
通信内聚
信息内聚:这种模块能完成多个功能,各个功能都在同一数据结构上操作,每一项功能有一个唯一的入口点。
信息内聚
功能内聚:如果一个模块内所有成分都完成同一个功能,则称这样的模块为功能内聚模块。功能内聚是内聚程度最高的模块,也就是独立性最强的模块。
软件设计中应该注意:力求做到高内聚,尽量少用中内聚,绝对不用低内聚。
4.4 体系结构设计工具
常用的软件体系结构设计工具有结构图(SC)和层次图加输入/处理/输出图(HIPO)。
4.4.1 结构图
在结构化设计方法中,软件结构常常采用 20 世纪 70 年代中期由 Yourdon 等人提出的结构图(SC,Structure Chart)来表示。
结构图能够描述软件系统的模块层次结构,清楚地反映出程序中各模块之间的调用关系和联系。
结构图中的基本符号
结构图中的主要内容
扇出与扇入:扇出表明了该模块可以控制的下级模块的数目。扇入表明了共有多少个模块调用该模块。
扇出与扇入
深度与宽度:深度是指在软件结构中控制的层数。层数越多,程序越复杂,程序的可理解性也就随之下降。宽度表示软件结构中同一层次上的模块总数的最大值。宽度越大,系统越复杂。如下图所示的软件结构图中,深度为5,宽度为8。
深度与宽度
4.4.2 HIPO 图
HIPO(Hierarchy Plus Input/Processing/Output)图是 IBM 公司在 20 世纪 70 年代发展起来的用于描述软件体系结构的图形工具。它实质上是在描述软件总体模块结构的层次图(H图)的基础上,加入了用于描述每个模块输入/输出数据和处理功能的 IPO 图,因此它的中文全名为层次图加输入/处理/输出图。
4.4.2.1 层次图(Hierarchy Chart)
层次图表明各功能模块的隶属关系,它是自顶向下逐层分解得到的一个树型结构。其顶层模块是整个系统的名称,第二层是对系统功能的分解,继续分解可得到第三层、第四层等。
为了使层次图更具有可追踪性,可以为除顶层以外的其他矩形框加上能反映层次关系的编号。
示例:工资计算系统的层次图
4.4.2.2 IPO 图
IPO 图是输入、处理、输出图,它能够方便、清晰地描绘出模块的数据输入、数据加工和数据输出之间的关系。与层次图中每个矩形框相对应,IPO 图描述该矩形框所代表的模块的具体处理细节,作为对层次图中内容的补充说明。
在图中左边的框中列出模块涉及的所有输入数据,中间列出主要的数据加工,右边列出处理后产生的输出数据;图中的箭头用于指明输入数据、加工和输出结果之间的关系。
4.5 概要设计的启发式规则
启发式规则是根据软件体系结构设计经验对概要设计原则进行的进一步补充和说明。
4.5.1 提高模块独立性
为了提高软件中各个模块的独立性,提高程序的可读性、可测试性和可维护性,在软件体系结构设计时应尽可能采用高内聚、低耦合的模块。
如最好实现功能内聚;尽量只使用数据耦合,限制公共耦合的使用,避免控制耦合的使用,杜绝内容耦合的出现。
4.5.2 模块大小要适中
程序中模块的规模过大,会增加程序的复杂性,降低程序的可读性;而模块规模过小,势必会导致程序中的模块数目过多,增加接口的数量和成本。
模块的适当规模没有严格的规定,但普遍的观点是模块中的语句最好保持在 50-150 行之间。
为了使模块的规模适中,在保证模块独立性的前提下,可对程序中规模过小的模块进行合并或对规模过大的模块进行分解。
4.5.3 模块应具有高扇入和适当的扇出
若模块的扇出过大,则会使该模块的调用控制过于复杂。根据实践经验,模块的平均扇出通常为 3 或 4 为好。
模块的扇入越大,则说明共享该模块的上级模块数越多,或者说该模块在程序中的重用性越高,这正是程序设计所追求的目标之一。
在一个好的软件结构中,模块应具有较高的扇入和适当的扇出。但绝不能为了单纯追求高扇入或合适的扇出而破坏了模块的独立性。
扇出扇入结构的调整
一个良好的软件结构,通常顶层的扇出数较大,中间层的扇出数较小,底层的扇入数较大,即瓮形结构,如图所示。
软件结构图示例
4.5.4 软件结构中的深度和宽度不宜过大
对宽度影响最大的因素是模块的扇出,模块可以调用的下级模块数越多,软件结构的宽度就越大。
软件结构中的深度和宽度是相互对立的两个方面,降低深度会引起宽度的增加,而降低宽度又会带来深度的增加。因此,设计软件结构时要在深度和宽度之间作出平衡和折衷。
4.5.5 模块的作用域应处于控制域之内
模块的作用域是指受该模块内判定条件影响的所有模块范围。
模块的控制域是指该模块本身以及所有该模块的下属模块(包括该模块可以直接调用的下级模块和可以间接调用的更下层的模块)。
例如,在上图中,模块C的控制域为模块C、E和F;若在模块C中存在一个对模块D、E和F均有影响的判定条件,即模块C的作用域为模块C、D、E和F(图中带阴影的模块),则显然模块C的作用域超出了其控制域。由于模块D在模块C的作用域中,因此模块C对模块D的控制信息必然要通过上级模块B进行传递,这样不但会增加模块间的耦合性,而且会给模块的维护和修改带来麻烦(若要修改模块C,可能会对不在它控制域中的模块D造成影响)。
软件设计时应使各个模块的作用域处于其控制域范围之内。若发现不符合此设计原则的模块,可通过下面的方法进行改进:
- 将判定位置上移。如将图中的模块C中的判定条件上移到上级模块B中或将模块C整个合并到模块B中。
- 将超出作用域的模块下移。如将图中的模块D移至模块C的下一层上,使模块D处于模块C的控制域中。
模块的控制域与作用域
4.5.6 尽量降低模块的接口复杂度
由于复杂的模块接口是导致软件出现错误的主要原因之一,因此在软件设计中应尽量使模块接口简单清晰,如减少接口传送的信息个数以及确保实参和形参的一致性和对应性等。
降低模块的接口复杂度,可以提高软件的可读性,减少出现错误的可能性,并有利于软件的测试和维护。
4.5.7 设计单入口、单出口的模块
这条规则要求在软件设计时不要使模块间出现内容耦合。如果软件在模块调用时是从顶部进入模块并且从底部退出来,这样的软件比较容易理解,也容易维护。
4.5.8 模块功能应该可以预测
如果把一个模块当做一个黑盒子,只要输入相同的数据就会产生相同的结果,这个模块的功能就是可以预测的。
可预测的模块功能
4.6 面向数据流的设计方法
面向数据流的设计方法定义了一些不同的“映射”,利用这些映射可以把数据流图变换成软件结构图。
任何软件系统都可以用数据流图表示,所以面向数据流的设计方法理论上可以设计任何软件的结构。通常所说的结构化设计(SD)方法,也就是基于数据流的设计方法。
面向数据流的设计方法
结构化设计(SD,Structure Design)方法,是基于模块化、自顶向下、逐步细化等结构化程序设计技术的一种软件体系结构设计方法。
4.6.1 结构化设计方法实施的步骤
- 首先研究、分析和审查数据流图。从软件的需求规格说明中弄清数据流的加工过程,对于发现的问题及时解决。
- 然后根据数据流图确定数据处理的类型。典型的数据流有两种类型:变换流和事务流。针对两种不同类型分别进行分析处理。
- 由数据流图推导出系统的初始结构图。
- 利用启发式规则改进系统初始结构图,直到得到符合要求的结构图为止。
- 修订和补充数据字典。
变换流:信息沿数据通路,先通过物理输入,由系统变换为逻辑输入,然后通过变换中心处理,再将信息的逻辑输出变换为物理输出。具有这种特性的信息流称为变换流。
变换流
事务流:信息沿数据通路到达一个处理中心(事务中心),然后根据信息的类型来决定从若干动作序列中选择一个来执行,这样的信息流称为事务流。一个事务流由输入、处理和若干动作路径组成。
事务流
从数据流图导出软件系统结构图的方法
在软件的需求分析阶段,数据流是软件开发人员考虑问题的出发点和基础。数据流从系统的输入端到输出端,要经历一系列的变换或处理。用来表现这个过程的数据流图(DFD) 实际上就是软件系统的逻辑模型。
面向数据流的设计要解决的任务,就是在上述需求分析的基础上,将 DFD 图映射成软件系统结构图-SC图。
4.6.2 变换分析
4.6.2.1 变换型系统结构图
变换型数据流图由输入、变换(主加工) 和输出三部分构成。
变换型数据流图
变换型数据处理工作过程大致分为三步,即取得数据、变换数据和给出数据。相应地,变换型系统结构图由输入、中心变换和输出三部分组成。
变换型系统结构图
变换分析--将具有变换型的DFD图导出为SC图
变换分析:
- 在数据流图上区分系统的逻辑输入、逻辑输出和变换中心部分,并标出它们的边界。
- 进行一级分解,设计系统模块结构的顶层和第一层。
- 进行二级分解,设计中、下层模块。
- 将输入模块 Ci、变换模块 Ct、输出模块 Co 组装在主控模块 Cm 下,获得完整的 SC 图。
- 在DFD图上标出逻辑输入、逻辑输出和变换中心的边界
- 完成第一级分解
- 完成第二级分解
对逻辑输入的分解
对输出的分解
- 将输入模块Ci、变换模块Ct、输出模块Co组装在一起,获得完整的SC图。
示例:根据汽车仪表板的数据流图转换软件结构图的过程
汽车数字仪表板系统的数据流图
划分输入、输出边界的数据流图
第一级分解
数字仪表板系统的第一级分解
分解后的输入结构
分解后的变换结构
分解后的输出结构
对上述三个模块进行分析细化(分解或合并)后组装在一起,即形成如下的软件体系结构图:
细化后的数字仪表盘系统软件结构图
运用变换分析建立系统的SC时需注意以下几点:
- 在设计模块的次序时,应对一个模块的全部下属模块都设计完成后,再转向另一个模块的下层模块进行设计。
- 在设计下层模块时,应考虑模块的耦合和内聚问题,以提高SC图的设计质量。
- 注意“黑盒”技术的使用(即只考虑模块的功能而不考虑内部实现的细节)。
4.6.3 事务分析
事务:引起、触发或启动某一动作或一串动作的任何数据、控制信号、事件或状态的变化。
事务分析与变换分析一样,也是从分析数据流图开始,自顶向下,逐步分解,建立系统结构图。
- 在事务型数据流图中,它接受一项事务,根据事务处理的特点和性质,选择分派一个适当的处理单元,然后给出结果。
- 在事务型系统结构图中,事务中心模块按所接受的事务的类型,选择某一事务处理模块执行。
- 每个事务处理模块可能要调用若干个操作模块,而操作模块又可能调用若干细节模块。
事务分析主要任务是实现事务型的数据流图到软件结构图的转换。实现这种转换,可以通过以下几个设计步骤来进行:
- 确定事务中心。
- 将事务型数据流图转换为仅有高层模块的结构图。
- 进一步分解结构图的接收模块和发送模块。
事务分析的映射方法
变换分析是软件系统结构设计的主要方法,任何类型的数据流图都可以转换成软件结构图。
一般来说,一个大型软件系统是变换型结构和事务型结构组成的混合结构。通常是以变换分析为主,事务分析为辅的方式进行软件结构设计。
混合型结构图
4.6.4 软件模块结构的改进
- 完整模块功能。
- 设计指定功能部分;
- 设计出错处理部分;
- 消除重复功能,改善软件结构。
- 删除功能重复模块;
- 处理局部相似模块;
局部相似模块的处理方法
- 优化最耗时模块的算法。
- 分离大量占用资源(处理器或内存)的模块。必要时使用机器语言设计这些模块的代码。
4.6.5 设计后的处理
- 为每一个模块写一份处理说明。
- 为每一个模块提供一份接口说明。
- 确定全局数据结构和局部数据结构。
- 指出所有的设计约束和限制。
- 进行概要设计的评审。
- 进行设计的优化。
4.7 概要设计说明书
概要设计说明书是体系结构设计阶段中最重要的技术文档,其主要内容应包括:
- 引言:用于说明编写本说明书的目的、背景,定义所用到的术语和缩略语,以及列出文档中所引用的参考资料等。
- 概要设计:用于说明软件的需求规定、运行环境要求、处理流程及软件体系结构等。
- 运行设计:用于说明软件的运行模块组合、运行控制方式及运行时间等。
- 模块设计:用于说明软件中各模块的功能、性能及接口等。
- 数据设计:用于说明软件系统所涉及的数据对象及数据结构的设计。
- 出错处理设计:用于说明软件系统可能出现的各种错误及可采取的处理措施。