第5章软 件 设 计 本章导读55.1软件设计基础 在软件需求确定后,就进入软件设计阶段。软件设计是软件工程的重要阶段。软件设计的基本目的就是回答“系统应该如何实现”这个问题。软件设计的任务,就是把软件需求规格说明书中规定的功能要素,考虑实际条件,转换为满足软件系统需求的技术方案,为下个阶段的软件实施工作奠定基础。 5.1.1软件设计概述 软件设计活动是获取高质量、低耗费、易维护软件最重要的一个环节,其主要目的是绘制软件的蓝图,权衡和比较各种技术和实施方法的利弊,合理分配各种资源,构建软件系统的详细方案和相关模型,指导软件实施工作的顺利开展。 软件设计可以分为总体设计(也称为概要设计)和详细设计两个阶段。 1. 总体设计 总体设计是决定系统“怎样做”,概要地说就是系统应该如何实现。因此,总体设计从需求分析阶段的工作结果出发,明确可选的技术方案,做好划分软件结构的前期工作,然后划分出组成系统的物理元素,并进行软件的结构设计与数据设计,最后编写出本阶段的阶段性成果——总体设计文档。 总体设计的主要参与者有软件分析人员、用户、软件项目管理人员以及相关的技术专家。软件分析人员完成对目标系统的物理方案和最终的软件结构设计;用户参与评价并最终审批系统的物理方案和最终的软件结构;软件项目管理人员参与评价软件分析人员设计的系统的物理方案和软件结构,并对软件分析人员的设计工作进行指导;相关的技术专家则主要参与评价软件分析人员设计的系统的物理方案以及软件结构。 总体设计过程包括设计供选择的方案、推荐最佳方案、设计软件结构、制订测试计划、编写总体设计文档、审查与复查总体设计文档。 (1) 设计供选择的方案。软件分析人员根据系统要求,提出并分析各种可能的方案,并且从中选出最佳的方案,为以后的工作做好准备。 需求分析阶段得出的数据流图是总体设计的根本出发点。数据流图中的处理可以进行逻辑分组,每一组都代表不同的实现策略。其次对这些分组得出的方案进行分析,产生一系列可供选择的方案。最后结合实际因素,如工程的目标、规模和用户的意见等,从可能的实现方案中选取若干个合理的方案。通常,选取的这些方案中应包括低成本、中成本和高成本几种方案。需要为每个方案提供系统流程图、数据字典、成本效益分析、实现系统的进度计划。 (2) 推荐最佳方案。软件分析人员从合理方案中选择一个最佳方案向用户推荐,并为推荐的方案制订详细的实现计划。 对于软件分析人员推荐的最佳方案,用户和有关专家应该认真审查。如果确认该方案确实符合用户的需要,并且在现有条件下完全能够实现,则应该提请使用部门负责人进一步审批。在使用部门负责人也接受了软件分析人员所推荐的方案后,方可进入总体设计过程的下一步工作,即结构设计阶段。 (3) 设计软件结构。软件结构的设计,首先要把复杂的系统功能分解成简单的功能,即功能分解,同时进一步细化数据流图。分解后,软件分析人员使用层次图或结构图来描述模块组织层的层次结构,实现由上层向下层的调用,最下层的模块完成具体的功能。 (4) 制订测试计划。在软件设计的早期阶段,考虑软件测试问题是非常必要的,有利于提高软件的可测试性。 (5) 编写总体设计文档。总体设计阶段结束时,应该提供的文档有总体设计说明书(包括系统实现方案和软件模块结构)、测试计划(包括测试方案、策略、步骤和结果等)、用户手册(根据总体设计阶段的结果对需求分析阶段的用户手册进行进一步的修改)、详细的实现计划(包括系统目标、总体设计、数据设计、处理方式设计、运行设计和出错设计等)。 (6) 审查与复审总体设计文档。对总体设计的结果要进行严格的技术审查,并在技术审查通过后,使用部门负责人还要从管理的角度进行复审。 2. 详细设计 详细设计的任务是对总体设计阶段划分出的每个模块进行明确的算法描述,即根据总体设计提供的说明文档,确定每个模块的数据结构及具体算法,并选用合适的描述工具,将其清晰准确地表达出来。 详细设计阶段并不具体地编写各个程序的代码,而是设计出程序的“蓝图”,在编码阶段程序员将根据这个蓝图写出实际的程序代码。因此,详细设计的结果在很大程度上决定着最终的程序代码的质量。在软件的生存周期中,在软件测试方案设计、程序代码调试和修改时,都需要先读懂程序代码。因此,在衡量程序代码的质量时,不仅要看其逻辑是否正确、性能是否满足要求,更重要的是要看程序代码是否容易阅读和理解。 详细设计阶段的参与者主要有软件系统用户、软件设计师、程序员及详细设计文档复审专家。软件分析人员和设计人员在前一个阶段进行了总体设计,确定了软件系统总体框架和模块组织结构。在详细设计阶段,需要有经验的软件设计师担任负责人,在上个阶段设计成果的基础上,将软件系统用户、程序员一起组织起来,进行系统各部分的详细设计。在这个阶段,用户参与可以使需求进一步明确细化,界面设计更加符合用户的实际需求,通过各方交流设计出高质量的详细设计说明书。在文件评审阶段,需要文档复审专家进行文件复审,以形成详细设计阶段的最终文档。 详细设计文档主要是给程序员看的,也是程序编码的依据。因此,详细设计的参与者撰写详细设计说明书时,对于模块的逻辑描述,要在确保正确可靠的基础上尽量使其更加清晰、易读。采用结构化程序设计方法,可以改善程序的结构,降低程序复杂度,提高程序的可读性、可测试性和可维护性。 详细设计的一般过程如下。 (1) 对总体设计阶段所确定的抽象性的数据类型进行确切的定义,确定软件各个模块采用的算法和内部数据组织形式,确定对系统内部和外部模块的接口细节。 (2) 确定每个模块的算法。选择适当的图形、表格和语言等描述工具表达每个模块算法的执行过程,写出模块的详细过程性描述。 (3) 为每个模块设计一组测试用例。在详细设计阶段设计每个模块的测试用例,使编码阶段对具体模块的调试或测试更加方便。负责详细设计的人员最了解模块的功能和要求,所以应由他们来完成测试用例的设计。测试内容通常包括输入数据、期望输出结果等。 (4) 编写详细设计说明书。在详细设计结束时,把上述结果进行整理,编写出详细设计说明书,并经过复审后,形成正式文档,作为下个阶段的工作依据。 5.1.2软件设计基本原理 经过多年发展,业界总结出一些基本的软件设计概念与原则,这些概念与原则经过时间的考验,已经成为软件设计人员完成复杂的软件设计问题的基础。主要内容包括将软件划分成若干独立成分的依据,怎样表示不同的成分内的功能细节和数据结构,怎样统一衡量软件设计的技术质量。 1. 模块化 模块化是指解决一个复杂问题时自顶向下逐层把软件系统划分成若干模块的过程。模块完成一个特定的子功能,所有的模块按某种方法组装起来,成为一个整体,完成整个系统所要求的功能。 假设函数C(x)定义了问题x的复杂性,解决它所需的工作量函数为E(x)。对于问题P1和P2,如果C(P1)>C(P2)即P1比P2复杂,那么E(P1)>E(P2),即问题越复杂,所需要的工作量越大。 根据解决一般问题的经验,规律为C(P1+P2)>C(P1)+C(P2)即一个问题由两个问题组合而成的复杂度大于分别考虑每个问题的复杂度之和。这样可以推出: E(P1+P2)>E(P1)+E(P2)由此可知,开发一个大而复杂的软件系统,将它进行适当的分解,不但可降低其复杂性,还可减少开发工作量,从而降低开发成本,提高软件生产率。但是模块划分越多,块内的工作量减少,模块之间接口的工作量增加了,如图51所示。因此在划分模块时,应减少接口的代价,提高模块的独立性。 图51软件设计成本与模块数量关系图 2. 抽象与逐步求精 在现实世界中,事物、状态或过程之间存在共性。把这些共性集中和概括起来,忽略它们之间的差异,这就是抽象。抽象就是抽出事物的本质特性而暂时不考虑它们的细节。 当考虑对任何问题的模块化解法时,可以提出许多抽象的层次。在抽象的最高层次使用问题环境的语言,以概括的方式叙述问题的解法;在较低抽象层次采用更过程化的方法,把面向问题的术语和面向实现的术语结合起来叙述问题的解法;在最低的抽象层次用可以直接实现的方式叙述问题的解法。 软件工程过程的每步都是对软件解法的抽象层次的一次精化。在可行性研究阶段,软件作为系统的一个完整部件;在需求分析期间,软件解法是使用在问题环境内熟悉的方式描述的;当我们由总体设计向详细设计过渡时,抽象的程度也就随之减少了;当源程序写出来以后,也就达到了抽象的最底层。 逐步求精与抽象是紧密相关的,随着软件开发工程的进展,在软件结构每层中的模块,表示了对软件抽象层次的一次精化。层次结构的上一层是下一层的抽象,下一层是上一层的求精。事实上,软件结构顶层的模块,控制了系统的主要功能并且影响全局;在软件结构底层的模块,完成对数据的一个具体处理,用自顶向下、由抽象到具体的方式分配控制,简化了软件的设计和实现,提高了软件的可理解性和可测试性,并且使软件更容易维护。 3. 信息隐蔽和局部化 应用模块化原理时,将产生一个问题: 为了得到一组模块,应该如何分解软件结构?信息隐蔽原理指出,每个模块的实现细节对于其他模块是隐蔽的,即模块中所包括的信息不允许其他不需要这些信息的模块调用。隐蔽表明有效的模块化可以通过定义一组独立的模块而实现,这些独立的模块间仅交换为完成系统功能而必须交换的信息。 模块间的通信仅使用对于实现软件功能的必要信息,通过抽象,可以确定组成软件的过程实体;而通过信息隐蔽,则可以定义和实施对模块的过程细节和局部数据结构的存取限制。局部化的概念和信息隐蔽概念密切相关,局部化是指把一些关系密切的软件元素物理地放得彼此靠近,在模块中使用局部数据元素就是局部化的一个例子。显然,局部化有助于实现信息隐蔽。 如果在测试期间和以后的软件维护期间需要修改软件,使用信息隐蔽原理作为模块化系统设计的标准就会带来极大好处。因为绝大多数数据和过程对于软件的其他部分是隐蔽的,也就是看不见的,在修改期间由于疏忽而引入的错误传播到软件的其他部分的机会就很少。 4. 模块独立性 为了降低软件系统的复杂性,提高可理解性、可维护性,必须把系统划分成为多个模块。模块不能任意划分,应尽量保持其独立性。模块独立性指每个模块只完成系统要求的独立的子功能,并且与其他模块的联系最少且接口简单。 如何衡量软件的独立性呢?根据模块的外部特征和内部特征,提出了两个定性的度量标准——耦合和内聚。 1) 耦合 耦合是指软件系统结构中各个模块之间相互联系紧密程度的一种度量。模块之间联系越紧密,其耦合性就越强,模块的独立性则越差。模块间耦合高低取决于模块之间接口的复杂性、调用的方式及传递的信息。 模块之间的耦合性一般分为7种类型,如图52所示。 图52耦合的类型 非直接耦合指两个模块之间没有直接的关系,它们分别从属于不同模块的控制与调用,它们之间不传递任何信息;数据耦合指两个模块之间有调用关系,传递的是简单的数据值,相当于高级语言中的值传递;标记耦合指两个模块传递的是数据结构,例如,高级语言中的数组名、记录名、文件名等这些名字即为标记,其实传递的是这个数据结构的地址;控制耦合指一个模块调用另一个模块时,传递的是控制变量(如开关、标志等),被调模块通过该控制变量的值有选择地执行块内某些功能;外部耦合指一组模块都访问同一个全局简单变量而不是同一个全局数据结构,并且不通过参数表传递该全局变量的信息;公共耦合指通过一个公共数据环境相互作用的那些模块间的耦合;当一个模块直接使用另一个模块的内部数据,或通过非正常入口而转入另一个模块内部,这种模块之间的耦合为内容耦合。 耦合性是影响软件复杂程度的一个重要因素,在设计中应该尽量使用数据耦合,少用控制耦合,限制公共耦合的范围,完全不用内容耦合。 2) 内聚 内聚是指模块的功能强度的度量。若一个模块内各元素(语句之间、程序段之间)联系的越紧密,则它的内聚性就越高。 模块之间的内聚性一般分为7种类型,如图53所示。 图53内聚的类型 模块内部所有元素都属于一个整体,它们组合在一起是为了完成某个独立的功能,则该模块的内聚是功能内聚;模块内部各部分彼此紧密联系,为实现某个功能结合在一起,并按照顺序方式执行,则该模块的内聚是顺序内聚;模块内部的所有元素都使用相同的输入数据或产生相同的输出结果,则该模块的内聚是通信内聚;模块内部的所有元素彼此相关,但必须遵循特定的过程次序执行,则该模块是过程内聚;模块内部的所有组成部分必须在同一段时间内执行完成(如所有的初始化或终止工作),则该模块是时间内聚;模块内部的各组成部分除了通过逻辑变量(也称控制参数)联系之外无任何联系,则该模块是逻辑内聚;组成模块的元素之间没有实质性的联系,则该模块是偶然内聚。 在设计时更应重视模块内聚,尽量追求功能内聚,少用逻辑内聚和偶然内聚,顺序内聚和通信内聚可以酌情使用。此外,没有必要精确定义内聚的级别,只要能够识别出低内聚的模块即可。 【例51】一个模块中含有几个子程序为实现一个堆栈,如init_stack()、push()和pop();模块中同时还含有格式化报告数据和定义子程序中用到的所有全局数据和子程序。很难看出堆栈与报告子程序或全局数据部分有什么联系,因此模块的内聚性很差。这些子程序应该按照模块内聚的原则进行重新组织。 耦合性与内聚性是模块独立性的两个定性标准,将软件系统划分模块时,尽量做到高内聚、低耦合,提高模块的独立性,为设计高质量的软件结构奠定基础。 5. 软件设计原则 改进软件设计,提高软件质量需要遵循如下原则。 1) 模块高独立性 设计出软件的初步结构以后,应该进一步分解或合并模块,力求降低耦合并提高内聚。例如,多个模块公有的一个子功能可以独立定义一个模块,由这些模块调用;有时可以通过分解或合并模块以减少控制信息的传递及对全程数据的引用,并降低接口的复杂程度。 2) 模块规模适中 大的模块往往是由于分解不充分,但是进一步分解必须符合问题结构,一般分解后不应该降低模块独立性。过小的模块开销大于有效操作,而且模块数目过多将使系统接口复杂。因此过小的模块有时不值得单独存在,特别是只有一个模块调用它时,通常可以把它合并到上级模块中而不必单独存在。 3) 深度、宽度、扇出和扇入适当 深度表示软件结构中控制的层数,能够粗略地标志一个系统的大小和复杂程度,如图54所示。它和程序长度之间应该有粗略的对应关系,当然这个对应关系是在一定范围内变化的。如果层数过多,则应该考虑是否有许多管理模块过于简单,需要适当合并。宽度是软件结构内同一个层次上的模块总数的最大值。一般宽度越大系统越复杂。对宽度影响最大的因素是模块的扇出。 图54程序结构的有关术语 扇出是一个模块直接调用的模块数目,扇出过大意味着模块过于复杂,需要控制和协调过多的下级模块;扇出过小也不好。经验表明,一个设计得很好的典型系统的平均扇出通常是3或4。扇出太大一般是因为缺乏中间层次,应该适当增加中间层次的控制模块。扇出太小时可以把下级模块进一步分解成若干个子功能模块,或者合并到它的上级模块中去。当然,分解模块或合并模块必须符合问题结构,不能违背模块独立原理。 一个模块的扇入表明有多少个上级模块直接调用它,扇入越大则共享该模块的上级模块数目越多,这是有好处的,但是,不能违背模块独立单纯追求高扇入。 观察大量软件系统后发现,设计得优秀的软件结构通常顶层扇出比较高,中层扇出较少,底层扇入公共的实用模块中。 4) 模块的作用域应该在其控制域之内 模块的作用域定义为受该模块判定影响的所有模块的集合。模块的控制域是这个模块本身以及所有直接或间接从属图55模块的作用域和控制域 于它的模块的集合。例如,在图55中,模块A的控制域是A、B、C、D、E、F模块的集合。 在一个设计得很好的软件系统中,所有受判定影响的模块应该都从属于做出判定的那个模块,最好局限于做出判定的那个模块本身及它的直属下级模块。例如,如果图55中模块A做出的判定只影响模块B,符合这条规则。但是,如果模块A做出的判定同时还影响模块G中的处理过程,这样的结构使得软件难于理解。为了使A中的判定能影响G中的处理过程,通常需要在A中给一个标记设置状态以指示判定的结果,并且应该把这个标记传递给A和G的公共上级模块M,再由M把它传给G。这个标记是控制信息而不是数据,因此将使模块间出现控制耦合。 可以通过修改软件结构使作用域是控制域的子集: 一个方法是把做判定的点往上移,例如,把判定从模块A中移到模块M中;另一个方法是把那些在作用域内但不在控制域内的模块移到控制域内,例如,把模块G移到模块A的下面,成为它的直属下级模块。 5) 模块接口的低复杂度 模块接口复杂是软件发生错误的主要原因之一。应该设计模块接口使得信息传递简单并且和模块的功能一致。 【例52】一元二次方程的根的模块定义为QUAD_ROOT(TBL,X)其中,用数组TBL表示方程的系数,用数组X回送求得的根。这种传递信息的方法不利于对这个模块的理解,不仅在维护期间容易引起混淆,在开发期间也可能发生错误。下面这种接口可能比较简单: QUAD_ROOT(A,B,C,ROOT1,ROOT2)其中,A、B、C是方程的系数,ROOT1和ROOT2是算出的两个根。 接口复杂或者不一致是高耦合或低内聚的原因所致,应该重新分析这个模块的独立性,力争降低模块接口的复杂程度。 6) 单入口、单出口的模块 这条启发式规则表明,在设计软件结构时不要使模块间出现内容耦合。在结构上模块顶部有单入口,模块底部单出口,这样的结构比较容易理解,比较容易维护。 7) 模块功能应可预测 如果一个模块可以当作一个黑盒子,只要输入的数据相同就产生同样的输出,这个模块的功能就是可以预测的。带有内部存储器的模块的功能可能是不可预测的,因为它的输出可能取决于内部存储器(例如,某个标记)的状态。由于内部存储器对于上级模块而言是不可见的,所以这样的模块不易理解,难于测试和维护。 如果一个模块只完成一个单独的子功能,则表现高内聚;但是,如果一个模块任意限制局部数据结构的大小,过分限制在控制流中可以做出的选择或者外部接口的模式,这种模块的功能就过分局限,使用范围也过于狭窄。在使用过程中将不可避免地需要修改功能过分局限的模块,以提高模块的灵活性,扩大它的使用范围;但是,在使用现场修改软件的代价是很高的。 5.2软件设计技术过程 软件设计从工程管理角度可分为总体设计和详细设计两个步骤,从技术角度可分为软件体系结构设计、数据库设计、控制过程设计和用户界面设计四大部分。 5.2.1软件体系结构设计 提及体系结构时,容易引发与建筑物的物理结构的比较。在修建建筑物时,要兼顾考虑其外观与内部的统一。软件体系结构的设计,也具有相同的特征。 1. 软件体系结构设计概述 软件体系结构为软件系统设计提供了一套关于数据、行为、结构的指导性框架,该框架提供了描述系统数据、数据间的关系的静态特征,也对数据的操作、系统控制和通信等活动提供具有动态特征的描述过程。系统的静态特征体现了组织结构,系统的动态特征则体现系统操作流程的拓扑过程,共同构成设计决策的基本指导方针。良好设计的体系结构具有普适性,能满足不同的软件需求。 体系结构设计是软件设计的早期活动,其作用: 提供软件设计师能预期的体系结构描述;数据结构、文件组织、文件结构体现了软件设计的早期抉择,这些抉择将极大地影响后续的软件开发人员,影响软件产品的最后成功。 2. 软件体系结构 随着计算机网络技术和软件技术的发展,软件体系结构和模式也在不断地发生变化,下面介绍3种新型的软件体系结构。 1) 正交软件体系结构 正交软件体系结构由组织层和线索的构件构成。组织层由一组具有相同抽象级别的构件构成。线索是子系统的特例,它是由完成不同层次功能的构件组成(通过相互调用来关联)的,每条线索完成整个系统中相对独立的一部分功能。每条线索的实现与其他线索的实现无关或关联很少,在同一层中的构件之间是不存在相互调用的。 如果线索是相互独立的,即不同线索中的构件之间没有相互调用,这个结构就是完全正交的。从以上定义可以看出,正交软件体系结构是一种以垂直线索构件族为基础的层次化结构,其基本思想是把应用系统的结构按功能的正交相关性,垂直分割为若干条线索(子系统),线索又分为几个层次,每个线索由多个具有不同层次功能和不同抽象级别的构件构成。各线索的相同层次的构件具有相同的抽象级别。 对于大型的和复杂的软件系统,其子线索(一级子线索)还可以划分为更低一级的子线索(二级子线索),形成多级正交结构。正交软件体系结构的框架图如图56所示。 【例53】图56是一个三级线索(高层和最低层之间,有三级线索)、五层结构的正交软件体系结构框架图,在该图中,ABDFK组成了一条线索,ACEJK也是一条线索。因为B、C处于同一层次中,所以不允许互相调用;H、J处于同一层次中,也不允许互相调用。一般第五层是一个物理数据库连接构件或设备构件,供整个系统公用。 图56正交软件体系结构的框架图 在软件进化过程中,系统需求会不断发生变化。在正交软件体系结构中,因线索的正交性,每个需求变动仅影响某条线索,而不会涉及其他线索。这样,就把软件需求的变动局部化了,产生的影响也被限制在一定范围内,因此容易实现。 正交软件体系结构具有结构清晰、易于理解,易修改、可维护性强,可移植性强,重用粒度大等优点。 2) 三层C/S软件体系结构 C/S软件体系结构,即Client/Server(客户机/服务器)软件体系结构,是基于资源不对等,且为实现共享而提出来的。C/S软件体系结构将应用一分为二,服务器(后台)负责数据管理,客户机(前台)完成与用户的交互任务。C/S软件体系结构具有强大的数据操作和事务处理能力,模型简单,易于人们理解和接受。但随着企业规模的日益扩大,软件