人们在开发计算机软件的长期实践中积累了丰富的经验,总结这些经验得出了一些启发式规则。这些启发式规则虽然不像上一节讲述的基本原理和概念那样普遍适用,但是在许多场合仍然能给软件工程师以有益的启示,往往能帮助他们找到改进软件设计提高软件质量的途径。下面介绍几条启发式规则。
设计出软件的初步结构以后,应该审查分析这个结构,通过模块分解或合并,力求降低耦合提高内聚。例如,多个模块公有的一个子功能可以独立成一个模块,由这些模块调用;有时可以通过分解或合并模块以减少控制信息的传递及对全程数据的引用,并且降低接口的复杂程度。
经验表明,一个模块的规模不应过大,最好能写在一页纸内(通常不超过60行语句)。有人从心理学角度研究得知,当一个模块包含的语句数超过30以后,模块的可理解程度迅速下降。
过大的模块往往是由于分解不充分,但是进一步分解必须符合问题结构,一般说来,分解后不应该降低模块独立性。
过小的模块开销大于有效操作,而且模块数日过多将使系统接口复杂。因此过小的模块有时不值得单独存在,特别是只有一个模块调用它时,通常可以把它合并到上级模块中去而不必单独存在。
深度表示软件结构中控制的层数,它往往能粗略地标志一个系统的大小和复杂程度。深度和程序长度之间应该有粗略的对应关系,当然这个对应关系是在一定范围内变化的。如果层数过多则应该考虑是否有许多管理模块过分简单了,能否适当合并。
宽度是软件结构内同一个层次上的模块总数的最大值。一般说来,宽度越大系统越复杂。对宽度影响最大的因素是模块的扇出。
扇出是一个模块直接控制调用的模块数目,扇出过大意味着模块过分复杂,需要控制和协调过多的下级模块;扇出过小(例如总是1)也不好。经验表明,一个设计得好的典型系统的平均扇出通常是3或4(扇出的上限通常是5-9)。
扇出太大一般是因为缺乏中间层次,应该适当增加中间层次的控制模块。扇出太小时可以把下级模块进一步分解成若干个子功能模块,或者合并到它的上级模块中去。当然分解模块或合并模块必须符合问题结构,不能违背模块独立原理。
一个模块的扇入表明有多少个上级模块直接调用它,扇入越大则共享该模块的上级模块数据越多,这是有好处的,但是,不能违背模块独立原理单纯追求高扇入。
观察大量软件系统后发现,设计得好的软件结构通常顶层扇出比较高,中层扇出较少,底层扇入到公共的使用模块中去(底层模块有高扇入)。
模块的作用域定义为受该模块内一个判定影响的所有模块的集合。模块的控制域是这个模块本身以及所有直接或者间接从属于它的模块的集合。例如,在图4.2中的模块A的控制域是A、B、C、D、E、F等模块的集合。
在一个设计得很好的系统中,所有受判定影响的模块应该都从属于做出判定的那个模块,最好局限于作出判定的那个模块本身及它的直属下级模块。例如,如果图4.2中模块A做出的判定只影响模块B,那么是符合这条规则的。但是,如果模块A做出的判定同时还影响模块G中的处理过程,又会有什么坏处呢?首先,这样的结构使得软件难于理解。其次,为了使得A中的判定能影响G中的处理过程,通常需要在A中给一个标记设置状态以指示判定的结果,并且应该把这些标记传递给A和G的公共上级模块M,再由M把它传给G。这个标记是控制信息而不是数据,因此将使模块间出现控制偶合。
图4.2 模块的作用域和控制
怎样修改软件结构才能使作用域是控制域的子集呢?一个方法是把做判定的点往上移,例如,把判定从模块A中移到模块M中。另一个方法是把那些在作用域内但不在控制城内的模块移到控制域内,例如,把模块G移到模块A的下面,成为它的直属下级模块。
到底采用哪种方法改进软件结构,需要根据具体问题统筹考虑。一方面应该考虑哪种方法更现实;另一方面应该使软件结构能最好地体现问题原来的结构。
模块接口复杂是软件发生错误的一个主要原因。应该仔细设计模块接口,使得信息传递简单并且和模块的功能一致。
例如求一元二次方程的根的模块QUAD_ROOT(TBL,X),其中用数组TBL传送方程的系数,用数组X回送求得的根。这种传递信息的方法不利于对这个模块的理解,不仅在维护期间容易引起混淆,在开发期间也可能发生错误。下面这种接口可能是比较简单的:
QUAD_ROOT(A, B, C, ROOT1, ROOT2)
其中A,B,C是方程的系数,ROOT1和ROOT2是算出的两个根。
接口复杂或不一致(即看起来传递的数据之间没有联系),是紧耦合或低内聚的征兆,应该重新分析这个模块的独立性。
这条启发式规则警告软件工程师不要使模块间出现内容耦合。当从顶部进入模块并且从底部退出来时,软件是比较容易理解的,因此也是比较容易维护的。
模块的功能应该能够预测,但也要防止模块功能过分局限。
如果一个模块可以当做一个黑盒子,也就是说,只要输入的数据相同就产生同样的输出,这个模块的功能就是可以预测的。带有内部“存储器”的模块的功能可能是不可预测的,因为它的输出可能取决于内部存储器(例如某个标记)的状态。由于内部存储器对于上级模块而言是不可见的,所以这样的模块既不易理解又难于测试和维护。
如果一个模块只完成一个单独的子功能,则呈现高内聚;但是如果一个模块任意限制局部数据结构的大小,过分限制在控制流中可以做出的选择或者外部接口的模式,那么这种模块的功能就过分局限,使用范围也就过分狭窄了。在使用过程中将不可避免地需要修改功能过分局限的模块,以提高模块的灵活性,扩大它的使用范围;但是,在使用现场修改软件的代价是很高的。
以上列出的启发式规则多数是经验规律,对改进设计,提高软件质量,往往有重要的参考价值;但是它们既不是设计的目标也不是设计时应该普遍遵循的原理。