1、第四章设计方法4.1 模块4.2 概要设计和基本概念4.3 结构化设计(SD方法)概要4.4 块间联系和块内联系4.5 设计技巧4.6 从数据流图导出初始结构图4.7 SD方法小结4.8 概要设计的其他工作4.9 详细设计的基本概念4.10 结构化程序设计(SP)方法4.11 详细设计的描述方式4.12 Jackson方法习题四第四章设计方法分析阶段的工作结果是需求说明书,它明确地描述了用户要求软件系统“做什么”。既然“问题”明确了,我们就可以着手寻求“解答”,即建立一个符合用户要求的软件系统。如果问题较简单,要求一旦确定了,立刻就可以开始编程。但对大型系统来说,为了保证产品的质量,并使开发工
2、作能顺利进行,我们必须先为编程制订一个周密的计划,这项工作就称为设计(Design),设计实际上是为需求说明书到程序间的过渡架起一座桥梁。4.1 模块模块第四章设计方法 设计要决定软件系统的结构,包括数据结构和程序结构,本章只讨论程序结构,第八章将介绍一些为数据库系统组织数据结构的方法。下面先分析大型程序结构上的特征。工程上许多大系统都是由一些较小的单元组成的,如建筑工程中的构件、机器中的各种零部件等。这样做的优点是便于加工制造、便于维修,由于一些部件可以公用,成本也较节省。第四章设计方法同样,一个几万行的程序系统也不应是铁板一块的,它应由许多较小的单元组成(图41),这种单元可称为模块。模块
3、(Module)一词目前尚无统一的定义,通常是指用一个名字可以调用的一段程序语句,在本书中我们可以暂将它理解成类似“子程序”的概念,例如PASCAL中的函数和过程,FORTRAN中的函数和子程序,COBO中的节、段等,以及汇编语言中的子程序都可看作是模块,在这些语言中都有相应的过程调用、函数调用等机制(如CALL语句)实现对模块的调用。第四章设计方法 下面这段用PASCAL语言写的过程就是一个模块。PROCEDURE lowterm(VAR num,den:integer);VAR numcopy,dencopy,remainder:integer;BEGIN numcopy:=num;den
4、copy:den;WHILE dencopy0 DO BEGIN remainder:=numcOpy MOD dencOpy;numcopy:=dencopy;l dencopy:=remainder END;while)第四章设计方法IF numcopy,THEN BEGIN num:=num DIV numcopy;den:一den DIV numcopy END END;lowterm)第四章设计方法 一个 模块具有输入和输出、功能、内部数据、程序代码等四个特性,输入和输出分别是模块需要的和产生的信息,功能是指模块所做的工作,输入输出和功能构成了一个模块的外貌,即模块的外部特性。模块用
5、程序代码完成它的功能,内部数据是仅供该模块本身引用的数据,内部数据和程序代码是模块的内部特性。对模块的外部环境(例如需要调用这个模块的另一个模块)来说,只需了解它的外部特性就足够了,其内部特性应该是不必了解的。第四章设计方法上面这段PASCAL过程,lOWterm是其模块名。模块名一般应能反映该模块的功能,参数num和den是它的输入和输出,变量numcopy、dencopy、remainder是仅供该模块内部使用的数据,从第一个BEG。lN至末一个END之间就是该模块的代码。同第三章中的思想方法一样,“由外向里”是较合理的一种思考过程,所以我们通常是先确定模块的外部特性,再确定其内部特性。因
6、而软件的设计阶段要分成概要设计和详细设计两步进行。概要设计的任务是决定系统中各个模块的外部特性,即其输入输出和功能;详细设计的任务是决定每个模块的内部特性,即其内部的算法过程及使用的数据。第四章设计方法 概要设计(Preliminary Design)又称总体设计(Architectural。Design),它的基本任务是:将系统划分成模块 决定每个模块的功能 决定模块的调用关系 决定模块的界面,即模块间传递的数据 所以概要设计的主要工作就是完成模块分解,确定系统的模块层次结构(图4.1)。这项工作技术上比较复杂,也需要人的创造能力。4.2 概要设计的基本概念概要设计的基本概念第四章设计方法图
7、4.1第四章设计方法 对同一用户需求,可以提出多个设计方案,一个系统的质量在很大程度取决于设计方案的质量,所以在设计阶段只考虑一个设计方案是不够的,我们应该尽量考虑多种可能的方案,并对各个方案的质量进行全面的评价,然后从中选出一个较好的方案来。所谓“较好”是指在一定的限制条件下(如成本、时间、可使用资源等。)能使所期望的目标(可维护性、可靠性、可理解性、效率等)较大限度地得到满足。第四章设计方法 概要设计需交付的文档中,除了对数据结构的描述部分之外,主要就是模块说明部分,它包括模块结构图及每个模块的功能说明。模块结构图(图4.1)描述了系统的模块组成以及模块间的调用关系,每个模块的功能说明则需
8、描述模块的输入输出及其功能(即“做什么,不是“怎么做)。模块的功能说明同样可按词典方式组织起来,每个模块在词典中一个条目,条目中列出模块名,输入、输出、功能,以及模块的限制和一些注释等。第四章设计方法上述文档将作为详细设计和编程的基础,也是测试的依据。概要设计是开发过程中关键的一步,因为软件系统的质量及一些整体特性基本上是在蓥一步决定的。概要设计应该由资历较高、经验较丰富的软件人员担任。下面称他们为设十员。概要设计技术上有相当难度,它需要有一定的方法来指导,从而使设计人员可以较容易地获得好的设计方案。70 年代以来,出现了多种设计方法,其中代表性的有:结构化设计、Parnas方法和Jackso
9、n方法等,此外还有warnier方法、IBM公司的HlPO等。第四章设计方法这些方法都采用了模块化、由顶向下逐步加细等基本思想,它们的差别在于构成模块的原则,结构化设计以数据流图为基础构成模块结构,Parnas方法以信息隐蔽为原则建立模块结构,而Jackson方法则以数据结构为基础建立模块结构。这些方法可以结合起来使用。本章4.34.7介绍结构化设计4.12简介Jackson方法。第四章设计方法在众多的设计疗法中,结构化设计(Structured Design,简称SD方法)是最受人注意、使丑也最广的一个,它由美国IBM公司的w.Stevens、G.Myers和L.Constantine等人提
10、出。这卜方法用于软件系统的总体设计,它可以同分析阶段的sA方法衔接起来使用。介绍SD方法的经典著作是参考文献,由于这本书的作者是Yourdon和Constantine,所以SD方法又常称作YourdonConstantine方法。4.3 结构化设计结构化设计(SD方法方法)概要概要第四章设计方法 SD方法的目标是建立结构良好的程序系统,它的创导者反复研究了模块分解对程序贡量的影响,并在此基础上提出了评价设计质量的两个标准块间联系和块内联系,还舍出了从描述用户要求的数据流图导出模块结构的规则。第四章设计方法 4.3.1 相对独立、单一功能的模块结构相对独立、单一功能的模块结构 SD方法的基本思想
11、是将系统设计成由相对独立、单一功能的模块组成的结构。用SD方法设计的程序系统,由于模块之间是相对独立的,所以每个模块可以独立地被理解、编程、测试、排错和修改,这就使复杂的研制工作得以简化,此外,模块的相对独立性也能有效地防止错误在模块之间扩散蔓延,因而提高了系统的可靠性。所以我们可以说SD方法的长处来自于模块之间的相对独立性,它提高了系统的质量(可理解性、可维护性、可靠性等)也减少了研制所需的人工。第四章设计方法4.3.2 块间联系和块内联系块间联系和块内联系 如何衡量模块之间的相对独立性呢?sD方法提出了块间联系和块内联系这两个标准(如图4.2)。块间联系(coupling,又称耦合度)是指
12、模块之间的联系,它是对模块独立性的直接衡量,块间联系越小就意味着模块的独立性越高,所以这是一个最基本的标准。块内联系(Coh-eSion,又称聚合度)是指一个模块内部各成分(语句或语句段)之间的联隰,块内联系大了,则模块的相对独立性势必会提高。第四章设计方法图4.2第四章设计方法SD方法的目标是使块间联系尽量小,块内联系尽量大。事实上,块间联系和块内联系是同一件事的两个方面,程序中各组成成分间是有联系的,如果将密切相关的成分分散在各个模块中,就会造成很高的块间联系,反之,如果密切相关的一些成分组织在同一模块中,块内联系高了,则块间联系势必也就少了。块间联系和块内联系是sD方法的两个最重要的概念
13、,本章4.4将进一步详细地讨论。第四章设计方法4.3.3 描述方式描述方式 SD方法使用的描述方式是结构图(structure chart,图4.3),它描述了程序的模块结构,并反映了块间联系和块内联系等特性。结构图中的主要成分有:模块它用方框表示,方框中写有模块的名字,一个模块的名字应适当地反映这个模块的功能,这就在某种程度上反映了块内联系。第四章设计方法 调用从一个模块指向另一TN模块的箭头表示前一模块中含有对后一模块的调用。数据调用箭头边上的小箭头表示调用时从一个模块传送给另一模块的数据,小箭头也指出了传送的方向。第四章设计方法 图4.4(a)的结构图说明模块A含有一个或多个对模块B的调
14、用。A调用B时,A将数据x、Y传送给B,B返回到A时,将数据z传送给A。如果B将对Y作修改,然后再将Y回送给A,则Y应出现在调用箭头的两边(图4.4(b)。我们称图4.4中的A为B的调用模块或调用者,称B为A的被调模块或下层模块。有时,调用模块和被调模块对传送的数据使用不同的名字(例如形式参数与实在参数的名字往往不相同),为避免混淆,结构图中模块间传送的数据按调用模块使用的名字(即实在参数名)命名。第四章设计方法 设计结束后,作为最终的文档资料,结构图可采用图4.5的形式。图中对每个调用编上号码,并在图的边上用一个表格列出每个调用的输入和输出参数。第四章设计方法图4.4第四章设计方法图4.5第
15、四章设计方法设计员应该为结构图中的每一个成分(模块和数据)适当地命名,使人能直观地理解其含义。由于目前多数人对英语和汉语拼音都不熟悉,要做到这点看来还有困难,如果能在一个课题组中约定一些命名规则,将会是有帮助的。为使读者容易理解,本书有时在表示模块的方框内用中文说明该模块的功能。第四章设计方法 除上述基本符号外,结构图中可以再加上一些辅助性的符号,如图4.6表示模块A有条件地调用模块B,并有条件地调用C或D,图中的菱形符号表示一个条件。图4.7表示模块A循环地调用B和C,这里弧形箭头表示循环。图4.8(a)中带有双竖线的方框表示现成的模块(如程序库中现有的模块),它们不必再另行编写,这种模块总
16、出现在结构图的底层。设计员可根据具体情况决定是否有必要画出这些辅助性的符号。第四章设计方法 结构图并不一定是树形的,许多程序的结构呈“清真寺”状(图4.8(a),即顶是尖的,中间较宽,而底部较窄。如果所使用的高级语言允许递归,则程序结构可以是图4.8(6)的形状。应该注意的是,一个模块在结构图中只能出现一次,否则修改模块结构时就需要修改多处,这容易造成错误。为了避免线条交叉过多,可采用图4.8(c)的表示方式:在某些地方用圆表示被调模块。第四章设计方法为了便于理解程序的整个结构,设计员应将整个结构图画在一张纸上。图4.3是一张完整的结构图,它描述了模块、模块间的调用、模块间传送的参数等含良义。
17、这个程序从输入文件读入数据,经编辑和合理性检查后进行计算,再将结果作为报告胂的一行打印,直至读到输入文件的文件尾时结束计算,并将累计的结果作为报告的最后一行印出。这个程序由11个模块组成:主模块REPORT要管理GET V.ALI D等3个模块,FET VALlD又要管理GET EDlTED等两个模块,。第四章设计方法这个结构是这样工作的:顶层的主麟块REPORT首先得到控制,它调用GET VALlD期望获得合理的输入数据,GET、VALID又调用GET EDlTED,所以控制很快达到底层的模块READ,它从输入文件读入数据lN并返回给GET EDITED,GET EDITED将数据IN送给模
18、块EDIT进行编辑从而获得编辑后的数据:EDITED,GET EDlTED再将EDlTED返回给其上层,最后主模块获得合理的输入数据VALlDATED;主模块再将数据传送给计算模块CALCULATE,并获得计算结果RESULTS,它又将结果交给打印报告的模块PRINT R ElPORT;PRlNT REPORT则要调用印报告头模块PRlTN HEAD.ER、印一行模块PR.1NT和印报告尾模块PRINT TRAILER.才能打印一份完整的报告。第四章设计方法图4.6第四章设计方法图4.7第四章设计方法图4.8第四章设计方法 图4.3也反映了画结构图的一般习惯:输入模块在左,输出模块在右,而计算
19、模块居中。必须指出:“结构图”同“框图”(程序流程图,见图4.9)是不同的。一个程序有层次性和过程性两方面的特点,通常“层次性”反映的是整体性质,“过程性”反映的是局部性质,所以我们一般是先决定程序的层次特性再决定其过程特性。“结构图”描述的是程序的层次特性,即某个模块负责管理哪些模块,这些模块又依次负责管理哪些模块等(可以看出:结构图也可以用来描述现实生活中的组织管理结构,如学校中的系、教研室、教学小组等层次结构)。第四章设计方法“框图描述的是程序的过程特性,即先执行哪一部分,再执行哪一部分等。概要设计时,我们关心的是程序的层次结构而不是执行过程,所以用结构图作为描述手段,而框图一般是在详细
20、设计时才使用的。第四章设计方法4.3.4 步骤步骤 SD方法可分两步进行:1)建立一个满足系统说明书要求的初始结构图。2)对结构图作逐步改进,即在结构图中找出块间联系和块内联系尚可改进之处,然后对有关部分的结构作适当的修改,以提高块内联系和减少块间联系。这里第一步可用比较简单的规则做到,而第二步则需要设计员对一些可能的方案作反复比较和权衡,设计员的经验在这里也是很重要的因素,所以第二步的工作量往往比第一步大。第四章设计方法本章4.4将讨论块间联系和块内联系这两个基本概念,4.5介绍结构图的改进,并详细讨论一个实例,4.6叙述建立初始结构图的规则,也提供了实例。第四章设计方法 SD方法以“块间联
21、系小,块内联系大”作为衡量模块结构质量的原则。下面分别讨论块间联系和块内联系。这两个概念并没有严格的定义,但是通过对一些典型实例的讨论,相信可以体会到它们的含义。4.4 块问联系和块内联系块问联系和块内联系第四章设计方法 4.4.1 块间联系的各种类型块间联系的各种类型 块间联系的大小一般可从三个角度来衡量:(1)方式块间联系是通过怎样的方式进行的。(2)作用块间共用的信息是作什么用的。(3)数量块间共用信息的多少。这几个角度可以形象地用图4.10的三维坐标来表示。下面分别讨论这三个角度。第四章设计方法 1.联系的方式联系的方式 块间联系的方式一般有两种:“用过程语句调用”或“直接引用”。用过
22、程语句调用是通过模块的名字调用整个模块,而“直接引用”是指一个模块直接存取另一模块内部的某些信息,这两种方式相比,前者的块间联系较低,而后者的块间联系则很高,下面用例子说明之。第四章设计方法 图4.1.1(a)是某个缺乏经验的设计员设计的结构。模块GET A COMMAND的功能是从终端获取一条命令,为完成该功能,它调用模块READ TERMINAL,后者的功能是从终端读入一行字符(L1NE),GET A COMMAND对这行字符再进行处理就可得到一条命令。假设系统有多个终端,从哪个终端读呢?READTER-M1NAL需要终端号,设计员在模块GET ACOMMAND中安排了一个单元存放终端号T
23、ERMNuM。READ TER_MINAL执行时引用这个单元,然后从所指终端读入,并将一行字符返回给GET A COMMAN D。该图中从模块READ TERMlNAL内部到模块GET A COMMAND内部的箭头就表示“直接引用”。第四章设计方法现在假设系统需要作一个修改:增加一个模块GET A DATA LINE,其功能是从终端获取一行数据。修改人员意识到应该利用模块READ TERMINAL作为子模块,下面有几种修改方案:1)GET A DATA LINE在调用READ TERMINAL之前,修改GET A COMMAND中的TERMNUM(图4.1l(6)。这样有可能造成错误,因为原先
24、编写GET A COMMAND时不知道会有其他模块修改TERMNUM,因此在GET A DATA LINE执行后再执行GET A COMMAND时,它就可能使用错误的终端号TERMNUM。第四章设计方法2)如果修改人员意识到上述问题,他可能会在GET A DATA LINE中先保护TERMINAL的当前值,再设置新的值,然后调用READ TERMINAL,返回后再恢复TERMNUM原先的值。但这样做还有问题,因为如果GET A DATA LINE同GET A COMMAND并行执行(例如在多道程序的环境中),则错误仍然存在;即使没有并行执行的可能性,这种设计亦是隐晦的,将来很容易造成错误。第四
25、章设计方法 3)修改模块GET A COMMAND,使其每次调用READ TERMINAL之前重新设置TERMINAL。但这样修改后,编写GET A DATA LINE和程序员还必须去修改模块GET A COMMAND,而这是一个本应与它无关的模块。另外,如果GET A DATA LINE同GET A COMMAND并行执行,则错误仍然存在。第四章设计方法4)GET A DATA LINE调用READ TERMINAL前先保护TERMINAL原先值再设置新值,从READ TERMINAL返回后再恢复原先值;对GET A COMMAND亦作类似的修改,即调用READ TERMINAL之前和之后分
26、别保护或恢复TERMINAL的原先值。这样,如果GET A COMMAND的保护或恢复指令有错时,GET A DATA LINE执行时就会失误。为了解决GET A DATA LIN E的失误,必需去研究其他的模块,也就是说,一个模块的错误可能造成另一个模块执行时失误,因此为解决某个模块的失误,需要到其他模块去找原因。可以想像,这样的排错是极其困难的。第四章设计方法 5)修改人员另外编写一个读终端模块,不再利用原有的READ TERMINAL。现在,因为有两个不同的读终端模块,首先是增加了额外的工作量;第二,如果将来终端的硬件特征有了变化时,程序员必须记着有两个读终端模块要随之作修改。上面的几种
27、修改方案都有各种问题,没有一个是令人满意的。第四章设计方法至此,我们已可以罗列“直接引用”的种种缺点:由于界面不清楚,理解某个模块时必须同时去了解另一模块内部的情况,编程时亦是如此;修改时往往要涉及多个模块;排错时亦必须同时分析几个模块才能找出错误的原因。总之,“直接引用”使两个模块间出现了密切的联系,致使理解、编程、修改、排错时,这两个模块不能单独处理,造成“分而不解”的局面,两个模块实际上不能成为“独立的”单元。第四章设计方法“直接引用”是病态的,它造成极强的块间联系,这种块间联系方式增加了开发工作的难度,使理解、编程、修改、排错等活动都难以进行。对图4.11的例子,有经验的设计员会认识到
28、造成上述修改难题的根本原因是GET A COMMAND和READ TERMINAL之间病态的直接引用,所以原先的设计应该将终端号TER.MNuM作为READFERMINAL的一个输入参数显式传送给它(图4.11(c)。第四章设计方法图4.9第四章设计方法图4.10第四章设计方法图4.11第四章设计方法 图4.1l(c)的块间联系方式是用过程语句调用,此时两个模块共用的信息是作为过程语句的参数显式传送的,因而每个模块的输入输出数据明显可见。另外,用过程语句调用是通过引用模块名调用整个模块,而不是像“直接引用”那样,用模块名之外的其他名字引用模块内部个别的信息。由于采用了过程语句调用方式,图4.1
29、1(c)中两个模块间的联系明显松散,这样的方式界面清晰、易于理解,避免了“直接引用”的种种缺点。对于同样的修改要求,显然,图4.11(d)就是直接的解答。第四章设计方法 2.共用信息的作用共用信息的作用 模块间共用的信息可以作“控制信息”用,也可以作“数据”用,相比之下,作“混合”(即控制数据)用块间联系最高,作“控制”用次之,作“数据”用则块间联系最低。下面用一 些例子说明。“混合”型是指一个模块修改另一模块的指令。图4.1 2(a)中,模块A修改模块B中的指令L,由于从修改一方看来,L是数据,而从被修改一方看来L是指令,所以称为“混合”型,或“数据控制”型。第四章设计方法图 4.12第四章
30、设计方法这种情况的缺点是显然的:如果模块A将L改错了,则模块B执行时会失误,即修改一方的错误,会使被修改一方执行时失误,这样为了解决某个模块的失误,必须到另一模块去寻找致错的原因;另外,在理解、编程、修改时,这两个模块也是难以单独考虑的。所以共用信息作“数据控制”型,其块间联系是很高的。第四章设计方法 共用信息作“控制”用的情况有好几种:一种是一个模块直接转向另一模块内部的某个位置。图4.1 2(6)中,模块A转向模块B中的某个位置L,这里L对双方来说都是指令,所在称为“控制”型。这种情况的缺点是:一个模块的内部改动可能对其他模块有直接的影响。如模块B欲将L前的某些指令移到L之后,此时就必需考
31、虑这一改动会给模块A带来什么影响,这样的程序是不易维护的;同样,在理解和编程时,这两个模块也是“分而不解”。第四章设计方法 读者应该注意到:图4.1 2(a)和(b)都是引用了模块名之外的另一个名字(模块的内部名),因而都是病态的。另一种情况是:将控制信号作为参数显式传送到另一模块。图4.1 3(a)中,模块A将一个参数“平均最高”传送给模块B,模块B按这个参数的值是“平均”还是“最高”,取出“平均成绩”或取出“最高成绩”回送给模块A。这里参数“平均最高”是一个开关量,它不是用户的现实环境中存在的一个“数据”,而是设计员创造的一个控制信号,它的作用是告诉模块B如何工作。第四章设计方法这种情况下
32、,模块A通过用作控制信号的开关量实际上控制着模块B的内部逻辑,如果A将开关量置错了,模块B就会按另一种方式工作。这样的设计方案给理解带来了一些困难,因为首先必须理解开关量的作用;第二,也为编程增加了不必要的麻烦:模块A必须设置开关值,再调用模块B,同时还要记着开关值是“平均”还是“最高”,以便从模块B返回后作不同的处理;而模块则必须解释开关值,再决定如何执行。第四章设计方法 总之,这种设计方案完全是人为地增加了不必要的额外负担,调用模块向被调模块传送“控制”型参数(开关量)的情况是完全可以避免的,图4.1 3(a)只需改成图4.1 3(6)就可以了,这里模块A根据需要调用模块B。或B2,即需要
33、取“平均成绩”时调用B-,需要取“最高成绩”时调用B2,这就降低了模块间的联系,使结构也更易理解并更易实现了。图4.1 3是很典型的设计范例,4.4.2和4.5.3还要进一步探讨。第四章设计方法图4.13第四章设计方法 模块间的共用信息还可作数据用,如图4.11中的LINE和图4.13中的“成绩”。这种联系是问题本身决定的,它是不可避免的。共用信息作数据用不会带来前面三例中的问题。第四章设计方法 3.共用信息的数量共用信息的数量 由于程序中的联系是因共享信息造成的,所以模块间的共用的信息越多则块间的联系越大。有的程序员习惯大量使用FORTRAN中的公共数据块或PASCAL等语言中的全程变量。应
34、该注意到这样做会增加模块间共用的数据量,造成大量的块间联系,给理解、编程、修改都带来许多麻烦。为了提高可理解性、可维护性和可靠性,一个模块最好只引用其调用模块显式传送给它的参数以及它本身的局部变量,这样,模块同系统其他部分的联系就会大大减少。第四章设计方法当然,即便是显式传送的参数,其个数也是越少越好,一般来说,24个参数就足够了。SD方法没有为块间联系给出严格的定义,在前面的讨论中,我们从联系方式、共用信息的作用、共用信息数量的多少等三个角度来理解块间联系的大小;有的资料(如参考文献9等)将这三个角度综合起来,把块间联系由大到小列为下面五种类型:内容型、公共型、控制型、复合型、数据型。下面简
35、单说明每种类型的含义,不再用例子进行讨论。内容型(Content coupling):一个模块直接引用另一模块内部的信息。第四章设计方法公共型(Common coupling):两个模块引用共同的全程数据区。控制型(Contr01.coupl.ing):一个模块传送给另一模块的信息是用于控制该模块内部逻辑的控制信号。复合型(Stamp Coupling):一个模块传送给另一模块的参数是一个复合的数据结构(如包含几个数据单项的记录)。数据型(Data coupling):一个模块传送给另一模块的参数是单个的数据项(或是由单个数据项组成的数组)。第四章设计方法表4.1是Jones总结的各类块间联系
36、的特性,以供读者设计时参考。通过本节对一些实例的讨论,我们可以体会到模块分解的目的是为了降低复杂性,如果能将一个复杂的大问题,化为几个孤立的小问题,从而可以分而治之、各个击破,就降低了问题的复杂性。诸如“直接引用”型、“混合”型、“公共”型等所谓的模块“分解”的方案,实际上是“分而不解”,后继的开发活动(理解、编程、排错、修改等)不能对每个模块单独进行,所以无助于降低复杂性。第四章设计方法第四章设计方法 通过上述讨论,读者还可以看出:软件质量(即可维护性、可理解性、可靠性等)很大程度上取决于模块分解的情况。为了降低开发活动的复杂性,并提高软件质量,SD方法指出:模块分解的一个目标是使块间联系尽
37、可能小,达到这个目标的具体手段可总结如下:1)每个模块用过程语句(或函数方式等)调用其他模块。2)模块间传送的参数作数据用。3)模块间共用的信息(如参数等)尽量少。第四章设计方法 如果程序将用高级语言编写,则“直接引用”、“混合”型等情况就肯定不会出现了,此时要使块间联系尽量小,我们只需注意“消除开关量”和“减少共用信息量”这两点就够了。第四章设计方法4.4.2 块内联系的各种类型块内联系的各种类型 块内联系是指一个模块内部各成分之间(如语句之间或语句段之间)的联系。SD方法的另一个目标是:块内联系尽量大,也就是说,模块分解时应尽量把相互有密切联系的成分划分在同一模块,而不要把不相关的成分凑到
38、一个模块中。块内联系常见的类型从小到大可列为六类:偶然型、逻辑型、瞬时型=、通讯型、顺序型、功能型。上述6种类型中,前面三种的块内联系是相当低的,应该尽量避免,通讯型和顺序型一般说是可以的,功能型的块内联系最强,是争取的目标。第四章设计方法 为了决定某个模块的块内联系是什么类型的,只需问一个问题:“为什么要将这些成分放在同一模块中?”,就可以根据以下6种回答的“原因”作出决定:1)没有什么原因,完全是偶然的。2)这些成分逻辑上相似。3)这些成分需要同时执行。4)这些成分引用共同的数据。5)其中某个成分的输出是另一个成分的输入。6)所有这些成分结合起来恰好完成了一个具体任务。下面举例说明各种类型
39、的含义和特点。第四章设计方法 1.偶然型偶然型(Coincidental Cohesion)图4.14模块T中的三个语句实际上没有任何联系(假定A、B、D不在文件CARDFlLE中),但是因为模块P、Q、R、S中都含有这样的语句段,设计员为节省空间将它们放在模块T中。当模块中各成分间实际上没联系时,块内联系就属于“偶然型”这一类。第四章设计方法图4.14第四章设计方法“偶然型”块内联系的主要缺点是不易修改,如果将来模块P不需要做“MOVE A TO B”,而是“MOVE A TO E”。或者模块R不要做“READ CARD FILE”,而是“READ TAPE FILE”,模块T的修改将会很复
40、杂。由于模块T中的各个语句本来就是毫无联系的,所以出现这种情况的可能性相当大,即某个调用模块需要的修改并不是其他调用模块所需要的。第四章设计方法 偶然型块内联系的另一个缺点是:模块T的含义不易理解,我们甚至难以为这个模块起一个合适的名字。显然,这种模块也是难以测试的。偶然型块内联系通常是为了节省空间而产生的,所以只要有足够的空间就应避免构造这种模块,免得造成维护时的困难。这里,我们看到了效率同可理解性、可维护性相抵触的实例。2.逻辑型逻辑型(I,ogical cohesion)图4.15(a)中模块A、B、C的功能是相似的,设计员把它们合并成一个模块ABC(图4.1 5(b)。将几个逻辑上相似
41、的功能放到一个模块中,其块内联系就属“逻辑型”这一类。第四章设计方法图4.15第四章设计方法 可以想像,模块ABC本身的程序流程图会像图4.15(c)那样:各个功能可以共用某一段程序(事实上就是为了节省这些空间,设计员才构造了这个模块),但对各个功能中不相同的部分,程序必须判别一个开关量后,才能选择执行某一个分支。同“偶然型”联系一样,“逻辑型”联系的主要缺点也是不易修改:当某个调用模块需要修改某段共用的程序时,另一调用模块可能并不希望这样做。第四章设计方法 另外,这种模块需要每个调用者都传送给它一个开关量,以区别是来自哪个模块的调用,这既增加了块间联系,编写起来又不方便。其次,每次将这种模块
42、调入内存时,实际上只执行模块中的一部分程序,例如X调用ABC时并不执行其中为Y、Z编写的那些部分,所以从效率角度看也不经济。第四章设计方法 上一节中的图4.1 3(a)是一个很典型的例子,模块B包括两个成分:取平均成绩和取最高成绩,由于这两个成分逻辑上有相同之处,为了节省空间,设计员把它们合并在同一模块中。某次调用究竟执行哪个功能,由调用模块传送给它的一个控制信号(开关量)决定。调用模块向被调模块传送开发量往往是被调模块含有多个功能的迹象:对开关量的不同值,被调模块将执行不同的功能,所以这种设计方案实际上是将多个功能凑合到一个界面上。第四章设计方法“多个功能一个界面的设计方案是有许多缺点的,除
43、上面提到的难理解、难编程之外,还会带来修改上的困难。如果以后用户希望将最高成绩连同获得最高成绩的学生名一起打印出来,此时模块A和B的界面上就要增加一个参数“学生名”。“取平均成绩”原来是同“取最高成绩”不相关的另一个功能,本应不受这一修改的影响,但由于设计员人为地将这两个功能合在同一界面上,所以模块A要取平均成绩时,也必须多设置一个参数了。上一节已经指出,“多个功能一个界面”的设计方案是完全可以避免的。“偶然型”和“逻辑型”联系都是很弱的,它们都是为了节省空间,而把没有联系的成分放到一个模块中了。第四章设计方法 3.瞬时型瞬时型(Ternporal Cohesion)“瞬时型”联系是同时间有关
44、的,即将需要同时执行的成分放在同一模块中,如初始化模块(为各种变量置初值,打开若干个文件等)或结束模块等。比起每次调用只执行其中一部分程序的“逻辑型联系来说,“瞬时型”联系是稍强的。总的说来,以上三种块内联系都是很弱的,因为模块中的成分并没有共用数据。以下三种块内联系则是较强的,因为模块中的成分共用了数据。第四章设计方法 4.通讯型通讯型(Communicational Cohesion)“通讯型”联系是指模块中的成分引用共同的数据。图4.16中的三个模块都属于这一类,模块A中包含三个部分,这三部分使用同一数据来源产生几个报告;模块B包含两个部分,一部分将输入数据存入,另一部分将输入数据打印;
45、模块C中的两个部分都是对同一文件进行管理。“通讯型”比“顺序型”稍弱,因为各成分的执行次序可以是任意的。第四章设计方法图4.16第四章设计方法 5.顺序型顺序型(Sequential Cohesion)“顺序型”联系是指模块中某个成分的输出是另一个成分的输入。图4.17中的两个模块都属于这一类型:模块A读入数据并进行编辑,模块B进行累加并将结果打印。第四章设计方法图4.17第四章设计方法“顺序型”比“通讯型”强,因为无论从数据的角度或执行的先后次序来看,模块中某一部分的执行依赖于另一部分,但是同“功能性”相比,“顺序型”还是相当弱的,因为模块中可能包含了几个功能,也可能仅包含某个功能的一部分。
46、与“功能型”相比,“通讯型”和“顺序型”的一个缺点是复用性较差。第四章设计方法 6.功能型功能型(Functional Coh.esion)一个模块包括并仅包括为完成某一个具体任务所必需的所有成分,或者说,模块中所有成分结合起来是为了完成一个具体的任务,则其块内联系就是“功能型”的。什么是“一个具体的任务”呢?下面一些例子可以帮助理解,如:求平方根 计算每小时工资 计算利息 印出支票 解一个方程 .第四章设计方法 模块中所有成分是因为要完成某一个功能而聚合在一起的,这种模块具有很高的聚合度,SD方法的目标是获得这种功能型的模块,也就是说,要使每个模块只执行一个功能。如何判别一个模块是否是“功能
47、型”的呢?较有效的方法可以是:从它的调用者的角度用一个短句简略地描述这个模块“做什么”(不是“怎么做”!),然后分析这个短句就可发现这个模块是完成一个具体任务,还是多个任务,还是一些相互无关的杂事。第四章设计方法 用上述方法观察图4.144.17的实例,容易判断它们都不是功能型的模块,因为描述这些模块的短句中含有多个动词、“和”、“或”,或者是“先再”之类与时间顺序有关的词,有的模块甚至无法用一个短句来描述它是做什么的(如图4.1 4的模块T)。再看图4.1 8(a),它是某操作系统中负责分析、处理命令的一部分。模块A从其调用模块接受一行命令,经分析后按命令名分别调用相应的下层模块作命令处理。
48、第四章设计方法从模块A的调用者看来,A的作用是“处理一个命令”,所以它是功能型的,至于模块A是“怎么做”的,它是否又要调其他模块,它的调用者是不关心,也不知道的。从模块B的调用者看来,B的作用是“处理SEND命令”,所以它也是功能性的。同理图中其他的模块也都是功能型的。以描述模块功能的短句为基础,我们可以为功能型模块起一个适合的名字,如“处理命令”,“处理SEND”等。第四章设计方法图4.18第四章设计方法图4.18(b)是个求解一元二次方程的程序。从模块A的调用者看来,模块A的功能是“求ax2+bx+c=0的根”,显然,这是一个功能,所以A是功能型的。模块A是怎么样完成求根功能的呢?一般要做
49、下面四件事:1)计算b2-4ac。2)求b2-4ac的平方根,这里又要调用模块B。3)计算第一个根。4)计算第二个根。第四章设计方法但是模块A的调用者对这些是完全不知道的,这些细节是A的内部情况,这些细节聚合在一起对外恰好完成一个求根任务。再看模块B。从它的调用者A看来,B的功能是求平方根,所以B也是功能型的。显然,B的内部也有许多细节,但模块A是不必知道的。第四章设计方法 读者可以再查看图4.3的实例,这里每个模块都是功能型的,因为从其调用者的角度看来,它们都是完成一个功能:读入、编辑、检查合理性、印报告、印一行等。功能型模块的界面一般都比较清晰,所以易于理解,同其他模块的联系也必然较低。一
50、个功能型的模块往往可以单独地被理解、进一步加细、再进行编程,最后再同其他模块连接起来。功能型模块也易于测试和维护。第四章设计方法 功能型模块的另一长处是复用性较好,许多功能型模块集中在一起可以构成一个模块库,选择其中的一些模块集成起来,很容易构造一个新的程序。总之,功能型联系是一种最强的联系。表4.2是Jones给出的备类块内联系的特性,供读者设计时参考。第四章设计方法第四章设计方法4.4.3 设计总则设计总则 4.4.1和4.4.2分别讨论了块间联系和块内联系两个概念,块间联系尽可能小,就是希望模块间联系尽可能相对独立,因此各模块可以单独开发和维护,从而降低了复杂性;块内联系尽量大,就是希望