4.2.5 启发式规则
在软件设计中除了有设计原理可以帮助软件设计人员设计出好的软件结构,还有大量在工程实践中由设计人员总结出来的设计方法和经验,这些方法和经验可能不具备通用性,但它们却是启发软件设计人员设计出好的软件结构的重要规则,并且随着时间的推移这些规则还会不断丰富和完善。故在软件工程中把这些规则称为“启发式规则”。
一、消除重复功能
多个模块都包含了相同的程序代码段,则这些相同的程序代码段即为重复功能。好的软件设计中的重复功能应该尽可能少。故需要设法消除软件中的重复功能。重复功能描述如下图所示:
图4.16 包含了重复功能的软件结构图
某软件设计人员对上图进行了消除重复功能的软件结构修改,修改结果如图4.17所示。他修改的思想是简单地将Q1和Q2两个模块合并,这种修改有问题吗?
图4.17 第1种消除重复功能的方法
很显然这种修改虽然消除了重复功能,但却破坏了模块之间的耦合(由于内聚和耦合是相互的,故也必会影响模块的内聚)。因为,X和Y除了调用Q'中的相同功能C之外,还需分别调用不同于C的其他子功能,由于Q1和Q2已经合并,则X和Y就必须通过传递控制变量给Q'以完成调用不同于C的其他子功能。根据上述耦合和内聚的知识可以分析出:当前模块间的耦合是控制耦合,而Q模块满足逻辑内聚,软件结构中的耦合和内聚均不理想,故该设计人员的改造是不成功的!
同学们再独立分析一下如图4.18所示的消除重复功能的方法,判断其是否合理。
图4.18 第2种消除重复功能的方法
二、改进软件结构,提高模块独立性
对软件结构的改造不外乎两种方法,即模块分解和模块合并。模块分解是将完成多个功能的模块分解成多个独立完成一个功能的小模块,而模块合并是将多个小模块合并在一起,使合并后的大模块能完成一个特定的功能。通过分解和合并,软件设计人员可以降低软件模块间的耦合度,提高软件模块的内聚度。
从上述的例子也可看出:当控制耦合发生时,需要把被调用模块分解为若干小模块,从而去除模块间的控制耦合,降低耦合度。而当过程内聚发生时,需要把多个过程模块合并成一个更大的内聚度更高的模块,提高内聚度。当然,在对软件结构改进时,需要综合权衡改进前和改进后软件结构中总体的耦合度以及内聚度的变化。
三、模块规模要适中
模块中的程序语句数要适当,模块规模不能太大,规模太大意味着该模块可能实现了多个功能,其内聚度可想而知,而它与其他模块之间的接口也会变得复杂,模块间的耦合度必会变大。此外,在一个大型软件系统中,模块的规模要比较均匀。在“软件详细设计”一章中将会对模块的复杂性进行定量评价,以控制模块的复杂性。
四、软件结构图中的“深度”“宽度”“扇出”和“扇入”要合理
概要设计的重要任务之一就是设计出软件结构图,该图是一个树型结构,树中的每个结点都代表一个模块,而模块之间通过线段相连,表示相互的调用关系。因此,该图显性地表达了构成软件系统的各个模块以及它们之间的调用和被调用关系。
图4.19 某软件的软件结构图
为了根据上图对软件结构进行优劣评价,软件设计人员引入了几个新概念:“软件结构的高度”“软件结构的宽度”“模块的扇出”和“模块的扇入”。
(1)“软件结构的高度”:软件结构树的层次数,即从最顶层父模块到最底层子模块之间的层次数。上图软件结构的高度是5。
(2)“软件结构的宽度”:软件结构中每层均包含若干模块,而这些层次中若某层的模块数最多,则该数值即为软件结构的宽度,如上图软件结构的宽度是7。
(3)“模块的扇出”:一个模块所包含的子模块的数量。上图M模块的扇出度是3。
(4)“模块的扇入”:调用同一个模块的上层模块的数量。上图T模块的扇入度是4。
结合这些概念得到的启发式规则如下:
“软件结构的高度”不宜太大,过大表示软件结构分解过细,模块间的耦合度会激增。但也不宜太小,过小表示软件分解不彻底,模块内聚度太小;“软件结构的宽度”不宜太大,过大表示系统过于复杂,但也不宜太小,过小则表示系统分解不彻底。
“模块的扇出”要合理。经验表明:一个好的软件系统的平均扇出通常是3或4,扇出的上限通常是5~9。扇出太大一般是因为缺乏中间层次,应该考虑适当增加中间层次的控制模块。因此,顶层扇出可以大一些,中间层扇出较少。顶层模块的扇出太小会导致模块的分解不彻底。中间层的扇出不能太大,因为其值太大可能会导致软件结构宽度变大;“模块的扇入”要合理。低层扇入可以尽可能大,以提高模块的可复用性,但要注意没有违背模块独立性原则;尽量避免出现平铺的软件结构。
五、模块的作用域应该在模块的控制域内
作用域:受该模块内一个判定影响的所有模块的集合。
控制域:这个模块本身及其所有直接或间接从属于它的模块的集合。
例如,在下图中C模块的控制域是{C、D、E、F}。C模块的作用域可用伪代码表示:
图4.20 作用域和控制域举例
If(a==true)
{
Goto module D;
}
Else
{
If(b==true)
Goto module E;
Else
Goto module F;
}
可见,C模块的作用域是D模块、E模块和F模块,当前C模块的作用域在控制域之内。但是,若出现作用域超出了控制域,则需要将判定结点上移或把不在控制域内的模块移动到控制域内。
如果,C模块的作用域超出了它的控制域,则需要考虑改造软件结构,例如,若C模块作用域进入到B模块,则需要考虑将判定结点移动到A或者把B模块移入到C模块的控制域之内。
六、力争降低模块接口的复杂性
据前述所讲内容,模块之间的接口复杂了,则它们间的耦合度必然增加了,这不利于设计好的软件结构,故在设计模块接口时要尽可能降低接口间的复杂性。
七、设计单入口、单出口的模块
在好的软件设计中,任何一个模块必须是单入口和单出口的。单入口的含义:从程序顶部进入程序;单出口的含义:从程序的底部退出程序。任何从程序的中部进入程序或退出程序的情况均视为非正常入/出口,这样的模块不但接口复杂了,且其控制流也更加复杂,不利于后续的实现及测试。下面的例子就是一个单入口、多出口的程序:
For(i=0;i<10;i++)
{
If(i==4)
Goto s1;//此处强制跳出了循环语句,是一个非正常出口。
}
S1:i+=10;
八、模块功能应该可以预测
如果把模块视为一个黑盒,则模块功能可以预测的含义是输入相同的数据,必然会产生相同的预期输出结果。可以预测的模块有利于实现及测试。
免责声明:以上内容源自网络,版权归原作者所有,如有侵犯您的原创版权请告知,我们将尽快删除相关内容。