第三章 软件设计 一、复习要求 1. 了解软件概要设计的原则和过程。 2. 掌握模块划分的评价准则―模块独立性的判别。 3. 掌握结构化设计方法。 4. 了解Jackson系统开发方法和Jackson程序设计方法。 5. 了解数据设计和文件设计的原则。 6. 掌握常用的详细设计的表达方法。 6. 了解软件设计规格说明和设计评审的主要内容。 二、内容提要 1. 软件设计的过程 一旦软件需求确定之后,就进入开发阶段。开发阶段由三个互相关联的的步骤组成:设计、实现(编码)和测试。每个步骤都按某种方式进行信息变换,最后得到有效的计算机软件。 (1) 软件设计在开发阶段中的重要性 在软件需求分析阶段已经完全弄清楚了软件的各种需求,较好地解决了要让所开发的软件“做什么”的问题,并已在软件需求规格说明和数据要求规格说明中详尽和充分地阐明了这些需求。下一步就要着手实现软件的需求,即要着手解决“怎么做”的问题。 分析模型中的每一个成份都提供了建立设计模型所需的信息。软件设计的信息流如图4.1所示。根据用数据、功能和行为模型表示的软件需求,采用某种设计方法进行数据设计、体系结构设计、接口设计和过程设计。 图4.1 将分析模型转换为软件设计 数据设计将实体―关系图中描述的对象和关系,以及数据词典中描述的详细数据内容转化为数据结构的定义。体系结构设计定义软件系统各主要成份之间的关系。接口设计根据数据流图定义软件内部各成份之间、软件与其它协同系统之间及软件与用户之间的交互机制。过程设计则是把结构成份转换成软件的过程性描述。在编码步骤,根据这种过程性描述,生成源程序代码,然后通过测试最终得到完整有效的软件。 软件设计是开发阶段中最重要的步骤,它是软件开发过程中质量得以保证的关键步骤。设计提供了软件的表示,使得软件的质量评价成为可能。同时,软件设计又是将用户要求准确地转化成为最终的软件产品的唯一途径。另一方面,软件设计是后续开发步骤及软件维护工作的基础。如果没有设计,只能建立一个不稳定的系统,如图4.2所示。只要出现一些小小的变动,就会使得软件垮掉,而且难于测试。  (2) 软件设计的过程 软件设计是一个把软件需求变换成软件表示的过程。最初这种表示只是描绘出可直接反映功能、数据、行为需求的软件的总的框架,然后进一步细化,在此框架中填入细节,把它加工成在程序细节上非常接近于源程序的软件表示。 从工程管理的角度来看,软件设计分两步完成。首先做概要设计,将软件需求转化为数据结构和软件的系统结构,并建立接口。然后是详细设计,即过程设计。通过对结构表示进行细化,得到软件的详细的数据结构和算法。 McGlanghlin给出在将需求转换为设计时判断设计好坏的三条特征: ( 设计必须实现分析模型中描述的所有显式需求,必须满足用户希望的所有隐式需求。 ( 设计必须是可读、可理解的,使得将来易于编程、易于测试、易于维护。 ( 设计应从实现角度出发,给出与数据、功能、行为相关的软件全貌。 以上三点就是软件设计过程的目标。为达到这些目标,必须建立衡量设计的技术标准。 ① 设计出来的结构应是分层结构,从而建立软件成份之间的控制。 ② 设计应当模块化,从逻辑上将软件划分为完成特定功能或子功能的构件。 ③ 设计应当既包含数据抽象,也包含过程抽象。 ④ 设计应当建立具有具有独立功能特征的模块。 ⑤ 设计应当建立能够降低模块与外部环境之间复杂连接的接口。 ⑥ 设计应能根据软件需求分析获取的信息,建立可驱动可重复的方法。 软件设计过程根据基本的设计原则,使用系统化的方法和完全的的设计评审来建立良好的设计。 2. 软件设计的原则 (1) 抽象化 对软件进行模块设计的时候,可以有不同的抽象层次。在最高的抽象层次上,可以使用问题所处环境的语言描述问题的解法。而在较低的抽象层次上,则采用过程化的方法。 ( 过程的抽象 :在软件工程过程中,从系统定义到实现,每进展一步都可以看做是对软件解决方案的抽象化过程的一次细化。在软件计划阶段,软件被当做整个计算机系统中的一个元素来看待。在软件需求分析阶段,用“问题所处环境的为大家所熟悉的术语”来描述软件的解决方法。而在从概要设计到详细设计的过程中,抽象化的层次逐次降低。当产生源程序时到达最低的抽象层次。 ( 数据抽象 :数据抽象与过程抽象一样,允许设计人员在不同层次上描述数据对象的细节。例如,可以定义一个draw数据对象,并将它规定为一个抽象数据类型,用它的构成元素来定义它的内部细节。此时,数据抽象draw本身是由另外一些数据抽象构成。而且在定义draw的抽象数据类型之后,就可以引用它来定义其它数据对象,而不必涉及draw的内部细节。 ( 控制抽象 :与过程抽象和数据抽象一样,控制抽象可以包含一个程序控制机制而无须规定其内部细节。控制抽象的例子就是在操作系统中用以协调某些活动的同步信号。 (2) 自顶向下,逐步细化 Niklaus Wirth提出的设计策略。将软件的体系结构按自顶向下方式,对各个层次的过程细节和数据细节逐层细化,直到用程序设计语言的语句能够实现为止,从而最后确立整个的体系结构。最初的说明只是概念性地描述了系统的功能或信息,但并未提供有关功能的内部实现机制或有关信息的内部结构的任何信息。设计人员对初始说明仔细推敲,进行功能细化或信息细化,给出实现的细节,划分出若干成份。然后再对这些成份,施行同样的细化工作。随着细化工作的逐步展开,设计人员就能得到越来越多的细节。 (3) 模块化 软件系统的层次结构正是模块化的具体体现。就是说,整个软件被划分成若干单独命名和可编址的部分,称之为模块。这些模块可以被组装起来以满足整个问题的需求。 一个大软件,由于其控制路径多、涉及范围广、变量多及其总体复杂性,使其相对于一个较小的软件不容易被人们理解。在解决问题的实践中,如果把两个问题结合起来作为一个问题来处理,其理解复杂性大于这两个问题被分开考虑时的理解复杂性之和。因此,把一个大而复杂的问题分解成一些独立的易于处理的小问题,解决起来就容易得多。 基于上述考虑,把问题/子问题(功能/子功能)的分解与软件开发中的系统/子系统或者系统/模块对应起来,就能够把一个大而复杂的软件系统划分成易于理解的比较单纯的模块结构。所谓“比较单纯”,是指模块和其它模块之间的接口应尽可能独立。 实际上,如果模块是相互独立的,当模块变得越小,每个模块花费的工作量越低;但当模块数增加时,模块间的联系也随之增加,把这些模块联接起来的工作量也随之增加。如图4.3所示。因此,存在一个模块个数M, 它使得总的开发成本达到最小。  图4.3 模块大小、模块数目与费用的关系 (4) 控制层次 控制层次也叫做程序结构,它表明了程序构件(模块)的组织情况。控制层次往往用程序的层次(树形或网状)结构来表示。如图4.4所示。位于最上层根部是顶层模块,它是程序的主模块。与其联系的有若干下属模块,各下属模块还可以进一步引出更下一层的下属模块。模块M是顶层模块,如果算做第0层,则其下属模块A、B和C为第1层,模块D、E、K、L和N是第2层,…,等等。  图4.4 程序的层次结构图示例 ( 程序结构的深度:程序结构的层次数称为结构的深度。结构的深度在一定意义上反映了程序结构的规模和复杂程度。 ( 程序结构的宽度:层次结构中同一层模块的最大模块个数称为结构的宽度。 ( 模块的扇入和扇出:扇出表示一个模块直接调用(或控制)的其它模块数目。扇入则定义为调用(或控制)一个给定模块的模块个数。多扇出意味着需要控制和协调许多下属模块。而多扇入的模块通常是公用模块。 要注意的是,程序结构是软件的过程表示,但并未表明软件的某些过程性特征。比如,进程序列、事件∕决策的顺序或其它的软件动态特性。 (5) 结构划分 程序结构可以按水平方向或垂直方向进行划分。水平划分按主要的程序功能来定义模块结构的各个分支。顶层模块是控制模块,用来协调程序各个功能之间的通信和运行。其下级模块的最简单的水平划分方法是建立三个分支:输入、处理(数据变换)和输出。这种划分的优点是:由于主要的功能相互分离,易于修改、易于扩充,且没有副作用。缺点是:需要通过模块接口传递更多的数据,使程序流的整体控制复杂化。 垂直划分也叫做因子划分。主要用在程序的体系结构中,且工作自顶向下逐层分布:顶层模块执行控制功能,少做实际处理工作,而低层模块是实际输入、计算和输出的具体执行者。这种划分的优点是:对低层模块的修改不太可能引起副作用的传播,而恰恰对计算机程序的修改常常发生在低层的输入、计算或输出模块中。因此,程序的整体控制结构不太可能被修改,便于将来的维护。 (6) 数据结构 数据结构是数据的各个元素之间的逻辑关系的一种表示。数据结构设计应确定数据的组织、存取方式、相关程度、以及信息的不同处理方法。数据结构的组织方法和复杂程度可以灵活多样,但典型的数据结构种类是有限的,它们是构成一些更复杂结构的基本构件块。图4.5表示了这些典型的数据结构。  图4.5 典型的数据结构 标量是最简单的一种数据结构。所谓标量项就是单个的数据元素,例如一个布尔量、整数、实数或一个字符串。可以通过名字对它们进行存取。 若把多个标量项组织成一个表或者顺序邻接为一组时,就形成了顺序向量。顺序向量又称为一维数组。通常可以通过下标及数组名来访问数组中的某一元素。把顺序向量扩展到二维、三维,直至任意维,就形成了n维向量空间。最常见的n维向量空间是二维矩阵。 链表是一种更灵活的数据结构,它把不相邻的标量项、向量或空间结构用拉链指针链接起来,使得它们可以像表一样得到处理。 组合上述基本数据结构可以构成其它数据结构。例如,可以用包含标量项、向量或 n维空间的多重链表来建立分层结构和网络结构。而利用它们又可以实现多种集合的存储。 必须注意,数据结构和程序结构一样,可以在不同的抽象层次上表示。例如,一个栈是一种线性结构的逻辑模型,其特点是只允许在结构的一端进行插入或删除运算。它可以用向量实现,也可以用链表实现。 (7) 软件过程  图4.6 一个模块内的软件过程 程序结构描述了整个程序的控制层次关系和各个部分的接口情况,而图4.6所示的软件过程则着重描述各个模块的处理细节。 软件过程必须提供精确的处理说明,包括事件的顺序、正确的判定点、重复的操作直至数据的组织和结构等等。程序结构与软件过程是有关系的。对每个模块的处理必须指明该模块所在的上下级环境。软件过程遵从程序结构的主从关系,因此它也是层次化的。 (8) 信息隐蔽 如何分解一个软件才能得到最佳的模块组合?为了明确怎样去做,需要了解什么是“信息隐蔽”。由parnas提倡的信息隐蔽是指,每个模块的实现细节对于其它模块来说是隐蔽的。就是说,模块中所包含的信息(包括数据和过程)不允许其它不需要这些信息的模块使用。 通常有效的模块化可以通过定义一组独立的模块来实现,这些模块相互间的通信仅使用对于实现软件功能来说是必要的信息。通过抽象,帮助我们确定组成软件的过程(或信息)实体,通过信息隐蔽,则可定义和实施对模块的过程细节和局部数据结构的存取限制。 由于一个软件系统在整个软件生存期内要经过多次修改,所以在划分模块时要采取措施,使得大多数过程和数据对软件的其它部分是隐蔽的。这样,在将来修改软件时偶然引入错误所造成的影响就可以局限在一个或几个模块内部,不致波及到软件的其它部分。 3. 软件体系结构 软件体系结构的三要素是程序构件(模块)的层次结构、构件之间交互的方式,以及数据的结构。软件设计的一个目标是建立软件的体系结构表示。将这个表示当作一个框架,从事更详细的设计活动。Shaw和Garlan提出了在软件体系结构设计中应保持的几个性质: ( 结构 :体系结构设计应当定义系统的构件,以及这些构件打包的方式和相互交互的方式。如将对象打包以封装数据和操纵数据的处理,并通过相关操作的调用来进行交互。 ( 附属的功能 :体系结构设计应当描述设计出来的体系结构如何实现对功能、性能、可靠性、安全性、适应性,以及其它的系统需求。 ( 可复用 :体系结构设计应当描述为一种可复用的模式,以便在以后类似的系统族的设计中使用它们。此外,设计应能复用体系结构中的构造块。 表4.1列出可能的软件构件,表4.2列出可能的构件间的连接方式。 表4.1 软件构件分类 构 件  特 点 和 示 例  纯计算构件 具有简单的输入∕输出关系,没有运行状态的变化。例如,数值计算、过滤器(Filters)、转换器(Transformers)等。  存储构件 存放共享的、永久性的、结构化的数据。例如,数据库、文件、符号表、超文本等。  管理构件 执行的操作与运行状态紧密耦合。例如,抽象数据类型(ADT)、面向对象系统中的对象、许多服务器(Servers)等。  控制构件 管理其它构件运行的时间、时机及次序。例如,调度器、同步器等。  链接构件 在实体之间传递信息。例如,通信机制、用户界面等。   表4.2 构件之间的连接方式 连 接  特 点 与 示 例  过程调用 在某一个执行路径中传递执行指针。例如,普通过程调用(同一个命名空间)、远程过程调用(不同的命名空间)。  数据流 相互独立的处理通过数据流进行交互,在得到数据的同时被赋予控制权限。例如,UNIX系统中的管道(pipes)。  间接激活 处理是因事件的发生而激活的,在处理之间没有直接的交互。例如,事件驱动系统、自动垃圾回收等。  消息传递 相互独立的处理之间有明确的交互,通过显式的离散方式的数据传递。这种传递可以是同步的,也可以是异步的。例如,TCP∕IP。  共享数据 构件们通过同一个数据空间进行并发的操作。例如,多用户数据库、数据黑板系统。   软件系统的体系结构经历了一个由低级到高级的发展过程。其间出现过下列体系结构。 (1) 数据流系统 ( 批处理 ( 管道及过滤器 这种结构中的每一个组成成份都有一套输入和输出数据,都依输入数据―处理―输出结果的方式工作。进行数据变换的构件叫做过滤器,把数据从一个过滤器的输出导入到另一个过滤器的输入,就叫做管道。在这种系统中,各个过滤器必须是相互独立的,每一个过滤器对它的上游或下游的过滤器的情况是不知道的,也不能做任何假设。如果要求最终的输出结果与各个过滤器的执行次序相关,就是一个数据流方式的体系结构。这种结构的优点是:数据流程设计明确,直接支持复用,系统容易维护和升级,可以进行某些性能分析(如流量、死锁等),容易支持并行计算。缺点是:容易导致把系统变成为简单的批处理作业。每一个过滤器都要考虑相似的数据检验和处理,不能很好地支持交互式操作和反馈。对数据的格式不能做过多的约定,使得每个过滤器的实现会更复杂。 (2) 调用―返回系统 ( 主程序∕子程序 ( 层次结构 在层次结构中,每一层都只与上下相邻的两层通信。每一层在利用下层基础服务的条件下,为上层提供服务。最典型的例子就是各种虚拟机、X-window以及OSI-ISO的7层网络协议。这种结构的优点是:提供逐步抽象的编程支持,支持复用及系统升级。缺点是:不是所有的系统都适合于建成层次结构,不能提供最佳性能。 ( 面向对象的系统 在这种系统中,数据和其相关的基本操作被封装在一起。系统的构件是对象。对象具有诸如封装、隐蔽、继承等良好的特性。对象必须自己维护其数据的一致性。这种结构的优点是:将具体的实现部分隐蔽在对象中,使得代码之间的独立性很好,有利于将复杂的系统分解为相互操纵的子任务。缺点是:对象间进行一般的调用时必须知道对方的标识。如果一个对象的标识发生变化,所有显式调用这个对象操作的地方都要修改。对象之间的同步等还缺乏现成的机制。 (3) 独立构件系统 ( 进程间通信 ( 事件驱动 这种结构的特点是事件的发出者不必知道,也不应假设对该事件的具体处理过程。它的优点是:提供了强大的可复用性支持。系统的重配置也很容易。缺点是:软件系统中的构件在很大程度上依靠操作系统的调用。如果一个事件的处理需要多个进程处理,活动进程的激活次序是不能保证的。经过事件传递数据也较受限制。事件处理中的逻辑处理实际上带有时态性质,与一般的逻辑处理不一样。 (4) 虚拟机 ( 解释器 解释器的目的是实现一个虚拟机。这是一类比较复杂的体系结构,但是,如果进行结构分析的话,就会发现看似不同的系统有着十分相似的结构。 ( 推理系统 ( 过程控制 这种结构要比较外界变量与目标常数的差异,经过控制策略的计算,反馈信号,控制需要监测和控制的过程。 (5) 数据为中心的系统 ( 数据库 ( 超文本系统 ( 数据黑板系统 这种结构的特点是:有两种构件:一是被共享的结构化数据,保存了所有的运行状态;二是所有访问这些数据的独立的进程。如果是因为输入的数据而引起对共享数据的操作,那么这种控制策略下的体系结构就叫做数据库。如果是由共享数据的当前状态触发相应的处理进程,那么这种体系结构就叫做数据黑板。许多表面上看起来是其它种类的体系结构,可以同时归入这种体系结构。 (6) 其它 ( 分布式处理的系统 ( 特定领域的软件体系结构 (7) 演进与综合 一般来讲,一个新系统的原型最开始出现时,往往以批处理方式进行组合;在进一步的应用分析过程中,将逐步对交互控制方式、实时的反馈、集成化等方面提出需求,因而系统将逐步向以数据为中心的系统过渡,中间还可能会经过一种或几种演化形态。 4. 有效的模块设计 模块化方法带来了许多好处。一方面,模块化设计降低了系统的复杂性,使得系统容易修改; 另一方面,推动了系统各个部分的并行开发,从而提高了软件的生产效率。 (1) 模块 模块又称构件,在传统的方法中指用一个名字就可调用的一段程序。类似于高级语言中的过程、函数等。它一般具有如下三个基本属性: ( 功能:即指该模块实现什么功能,做什么事情。 ( 逻辑:即描述模块内部怎么做。 ( 状态:即该模块使用时的环境和条件。 在描述一个模块时,还必须按模块的外部特性与内部特性分别描述。模块的外部特性是指模块的模块名、参数表、以及给程序以至整个系统造成的影响。而模块的内部特性则是指完成其功能的程序代码和仅供该模块内部使用的数据。 对于模块的外部环境(例如需要调用这个模块的上级模块)来说,只需要了解这个模块的外部特性足够了,不必了解它的内部特性。而软件设计阶段,通常是先确定模块的外部特性,然后再确定它的内部特性。 (2) 模块独立性 所谓模块的独立性,是指软件系统中每个模块只涉及软件要求的具体的子功能,而和软件系统中其它的模块的接口是简单的。例如,若一个模块只具有单一的功能且与其它模块没有太多的联系,那么,我们则称此模块具有模块独立性。 一般采用两个准则度量模块独立性。即模块间的耦合和模块的内聚。 (3) 内聚性 内聚是模块功能强度(一个模块内部各个元素彼此结合的紧密程度)的度量。一个内聚程度高的模块(在理想情况下)应当只做一件事。一般模块的内聚性分为七种类型。  在上面的关系中可以看到,位于高端的几种内聚类型最好,位于中段的几种内聚类型是可以接受的,但位于低端的内聚类型很不好,一般不能使用。因此,人们总是希望一个模块的内聚类型向高的方向靠。模块的内聚在系统的模块化设计中是一个关键的因素。 ( 巧合内聚(偶然内聚):当几个模块内凑巧有一些程序段代码相同,又没有明确表现出独立的功能,把这些代码独立出来建立的模块即为巧合内聚模块。它是内聚程度最低的模块。缺点是模块的内容不易理解,不易修改和维护。 ( 逻辑内聚 :这种模块把几种相关的功能组合在一起,每次被调用时,由传送给模块的控制型参数来确定该模块应执行哪一种功能。逻辑内聚模块比巧合内聚模块的内聚程度要高。因为它表明了各部分之间在功能上的相关关系。 ( 时间内聚(经典内聚):这种模块大多为多功能模块,但要求模块的各个功能必须在同一时间段内执行。例如初始化模块和终止模块。时间内聚模块比逻辑内聚模块的内聚程度又稍高一些。在一般情形下,各部分可以以任意的顺序执行,所以它的内部逻辑更简单。 ( 过程内聚 :使用流程图做为工具设计程序的时侯,常常通过流程图来确定模块划分。把流程图中的某一部分划出组成模块,就得到过程内聚模块。这类模块的内聚程度比时间内聚模块的内聚程度更强一些。 ( 通信内聚 :如果一个模块内各功能部分都使用了相同的输入数据,或产生了相同的输出数据,则称之为通信内聚模块。通常,通信内聚模块是通过数据流图来定义的。如图4.7所示。 ( 信息内聚(顺序内聚):这种模块完成多个功能,各个功能都在同一数据结构上操作,每一项功能有一个唯一的入口点。例如,图4.8所示的模块具有4个功能,由于模块的所有功能都是基于同一个数据结构(符号表),因此,它是一个信息内聚的模块。 信息内聚模块可以看成是多个功能内聚模块的组合,并且达到信息的隐蔽。即把某个数据结构、资源或设备隐蔽在一个模块内,不为别的模块所知晓。当把程序某些方面细节隐藏在一个模块中时,就增加了模块的独立性。 ( 功能内聚 :一个模块中各个部分都是为完成一项具体功能而协同工作,紧密联系,不可分割的。则称该模块为功能内聚模块。功能内聚模块时内聚性最强的模块。 (4) 耦合性 耦合是模块之间的相对独立性(互相连接的紧密程度)的度量。它取决于各个模块之间接口的复杂程度、调用模块的方式以及哪些信息通过接口。 一般模块之间可能的连接方式有七种,构成耦合性的七种类型。  ( 内容耦合 :如果一个模块直接访问另一个模块的内部数据;或者一个模块不通过正常入口转到另一模块内部;或者两个模块有一部分程序代码重迭;或者一个模块有多个入口,则两个模块之间就发生了内容耦合。在内容耦合的情形,被访问模块的任何变更,或者用不同的编译器对它再编译,都会造成程序出错。这种耦合是模块独立性最弱的耦合。 ( 公共耦合 :若一组模块都访问同一个公共数据环境,则它们之间的耦合就称为公共耦合。公共的数据环境可以是全局数据结构、共享的通信区、内存的公共覆盖区等。 公共耦合的复杂程度随耦合模块的个数增加而显著增加。如图4.9所示,若只是两个模块之间有公共数据环境,则公共耦合有两种情况:松散公共耦合和紧密公共耦合。只有在模块之间共享的数据很多,且通过参数表传递不方便时,才使用公共耦合。 ( 外部耦合 :一组模块都访问同一全局简单变量而不是同一全局数据结构,而且不是通过参数表传递该全局变量的信息,则称之为外部耦合。外部耦合引起的问题类似于公共耦合,区别在于在外部耦合中不存在依赖于一个数据结构内部各项的物理安排。  图4.9 公共耦合 ( 控制耦合 :如果一个模块通过传送开关、标志、名字等控制信息,明显地控制选择另一模块的功能,就是控制耦合。如图4.10所示。这种耦合的实质是在单一接口上选择多功能模块中的某项功能。因此,对被控制模块的任何修改,都会影响控制模块。另外,控制耦合也意味着控制模块必须知道被控制模块内部的一些逻辑关系,这些都会降低模块的独立性。 ( 标记耦合 :如果一组模块通过参数表传递记录信息,就是标记耦合。事实上,这组模块共享了某一数据结构的子结构,而不是简单变量。这要求这些模块都必须清楚该记录的结构,并按结构要求对记录进行操作。 ( 数据耦合 :如果一个模块访问另一个模块时,彼此之间是通过数据参数(不是控制参数、公共数据结构或外部变量)来交换输入、输出信息的,则称这种耦合为数据耦合。数据耦合是松散的耦合,模块之间的独立性比较强。 ( 非直接耦合 :如果两个模块之间没有直接关系,它们之间的联系完全是通过主模块的控制和调用来实现的,这就是非直接耦合。这种耦合的模块独立性最强。 实际上,开始时两个模块之间的耦合不只是一种类型,而是多种类型的混合。这就要求设计人员进行分析、比较,逐步加以改进,以提高模块的独立性。 模块之间的连接越紧密,联系越多,耦合性就越高,而其模块独立性就越弱。一个模块内部各个元素之间的联系越紧密,则它的内聚性就越高,相对地,它与其它模块之间的耦合性就会减低,而模块独立性就越强。因此,模块独立性比较强的模块应是高内聚低耦合的模块。 5. 结构化设计方法(Structured Design, SD) 从系统设计的角度出发,软件设计方法可以分为三大类。第一类是根据系统的数据流进行设计,称为面向数据流的设计或者过程驱动的设计,以结构化设计方法为代表。第二类是根据系统的数据结构进行设计,称为面向数据结构的设计或者数据驱动的设计,以LCP(程序逻辑构造)方法、Jackson系统开发方法和数据结构化系统开发(DSSD)方法为代表。第三类设计方法即面向对象的设计。 结构化设计方法是基于模块化、自顶向下细化、结构化程序设计等程序设计技术基础上发展起来的。该方法实施的要点是:① 建立数据流的类型。② 指明流的边界。③ 将数据流图映射到程序结构。④ 用“因子化”方法定义控制的层次结构。⑤ 用设计测量和一些启发式规则对结构进行细化。 (1) 在系统结构图(SC)中的模块 在系统结构图中不能再分解的底层模块为原子模块。如果一个软件系统的全部实际加工(数据计算或处理)都由底层的原子模块来完成,而其它所有非原子模块仅仅执行控制或协调功能,这样的系统就是完全因子分解的系统。如果系统结构图是完全因子分解的,就是最好的系统。一般地,在系统结构图中有4种类型的模块: ( 传入模块 :从下属模块取得数据,经过某些处理,再将其传送给上级模块。 ( 传出模块 :从上级模块获得数据,进行某些处理,再将其传送给下属模块。 ( 变换模块 :即加工模块。它从上级模块取得数据,进行特定的处理,转换成其它形式,再传送回上级模块。大多数计算模块(原子模块)属于这一类。 ( 协调模块 :对所有下属模块进行协调和管理的模块。 在系统的输入/输出部分或数据加工部分可以找到这样的模块。在一个好的系统结构图中,协调模块应在较高层出现。 在实际系统中,有些模块属于上述某一类型,还有一些模块是上述各种类型的组合。  图4.11 系统结构图的四种模块类型 (2) 变换流与变换型系统结构 变换型数据处理问题的工作过程大致分为三步,即取得数据,变换数据和给出数据。如图4.12所示。这三步反映了变换型问题数据流的基本思想。其中,变换数据是数据处理过程的核心工作,而取得数据只不过是为它做准备,给出数据则是对变换后的数据进行后处理工作。  图4.12 变换型数据流 变换型系统结构图如图4.13所示,相应于取得数据、变换数据、给出数据,系统的结构图由输入、中心变换和输出等三部分组成。  图4.13 变换型的系统结构图 (3) 事务流与事务型系统结构图 事务型数据处理问题的工作机理是接受一项事务,根据事务处理的特点和性质,选择分派一个适当的处理单元,然后给出结果。我们把完成选择分派任务的部分叫做事务处理中心。或分派部件。这种事务型数据处理问题的数据流图如图4.14所示。其中,输入数据流在事务中心T处做出选择,激活某一种事务处理加工。D1─D4 是并列的供选择的事务处理加工。 事务型数据流图所对应的系统结构图就是事务型系统结构图。如图4.15所示。   图4.15 事务型系统结构图 在事务型系统结构图中,事务中心模块按所接受的事务的类型,选择某一个事务处理模块执行。各个事务处理模块是并列的,依赖于一定的选择条件,分别完成不同的事务处理工作。每个事务处理模块可能要调用若干个操作模块,而操作模块又可能调用若干个细节模块。不同的事务处理模块可以共享一些操作模块。同样,不同的操作模块又可以共享一些细节模块。 事务型系统结构图在数据处理中经常遇到,但是更多的是变换型与事务型系统结构图的结合。例如,变换型系统结构中的某个变换模块本身又具有事务型的特点。 (4) 变换映射 变换映射是体系结构设计的一种策略。运用变换映射方法建立初始的变换型系统结构图,然后对它做进一步的改进,最后得到系统的最终结构图。设计的步骤如下。 步骤1:复审基本系统模型(0层数据流图和支持信息)。评估系统规格说明和软件需求规格说明。 步骤2:复审和细化软件的数据流图。重画数据流图时, 可以从物理输入到物理输出,或者相反. 还可以从顶层加工框开始,逐层向下。 步骤3:确定数据流图中含有变换流特征还是含有事务流特征。通常,系统的信息流总能表示为变换型,但其中也可能遇到明显的事务流特征,这时可采用变换型为主,在局部范围采用事务型的设计方法。 步骤4:区分输入流、输出流和中心变换部分,即标明流的边界。不同的设计人员可能选择不同的流边界,这将导致不同的系统结构图。 步骤5:进行一级“因子化”分解,设计顶层和第一层模块。 首先设计主模块,用程序名字为它命名,将它画在与中心变换相对应的位置上。做为系统的顶层,它调用下层模块,完成系统所要做的各项工作。系统结构第一层的设计方针:为每一个逻辑输入设计一个输入模块,它为主模块提供数据;为每一个逻辑输出设计一个输出模块,它将主模块提供的数据输出;为中心变换设计一个变换模块,它将逻辑输入转换成逻辑输出。第一层模块与主模块之间传送的数据应与数据流图相对应。 步骤6:进行二级“因子化”分解,设计中、下层模块。这一步工作是自顶向下,逐层细化,为每一个输入模块、输出模块、变换模块设计它们的从属模块。 输入模块要向调用它的上级模块提供数据,因而它必须有两个下属模块:一个是接收数据;另一个是把这些数据变换成它的上级模块所需的数据。输出模块是从调用它的上级模块接收数据,用以输出,因而也应当有两个下属模块:一个是将上级模块提供的数据变换成输出的形式;另一个是将它们输出。中心变换模块的下层模块没有通用的设计方法,一般应参照数据流图的中心变换部分和功能分解的原则来考虑如何对中心变换模块进行分解。 步骤7:利用一些启发式原则来改进系统的初始结构图,直到得到符合要求的结构图为止。这些启发式原则大致有: ① 模块功能的完善化。一个完整的功能模块,不仅应能完成指定的功能,而且还应当能够告诉使用者完成任务的状态,以及不能完成的原因。 ② 消除重复功能,改善软件结构。在系统的初始结构图得出之后,应当审查分析这个结构图。如果发现几个模块的功能有相似之处,可以加以改进。 ③ 模块的作用范围应在控制范围之内。模块的控制范围包括它本身及其所有的从属模块。模块的作用范围是指模块内一个判定的作用范围,凡是受这个判定影响的所有模块都属于这个判定的作用范围。如果一个判定的作用范围包含在这个判定所在模块的控制范围之内,则这种结构是简单的,否则,它的结构是不简单的。 ④ 尽可能减少高扇出结构,经验证明,一个设计得很好的软件模块结构,通常上层扇出比较高,中层扇出较少,底层扇入到有高扇入的公用模块中。 ⑤ 避免或减少使用病态联接。应限制使用如下三种病态联接:直接病态联接(内容耦合)、公共数据域病态联接(公共耦合)和通过通信模块联接。 ⑥ 模块的大小要适中。限制模块的大小是减少复杂性的手段之一,因而要求把模块的大小限制在一定的范围之内。通常规定其语句行数在50~100左右,最多不超过500行。 ⑦ 设计功能可预测的模块,但要避免过分受限制的模块。 一个功能可预测的模块不论内部处理细节如何,但对相同的输入数据,总能产生同样的结果。但是,如果模块内部蕴藏有一些特殊的鲜为人知的功能时,这个模块就可能是不可预测的。对于这种模块,如果调用者不小心使用,其结果将不可预测。调用者无法控制这个模块的执行,或者不能预知将会引起什么后果,最终会造成混乱。 为了能够适应将来的变更,软件模块中局部数据结构的大小应当是可控制的,调用者可以通过模块接口上的参数表或一些预定义外部参数来规定或改变局部数据结构的大小。另外,控制流的选择对于调用者来说,应当是可预测的。而与外界的接口应当是灵活的,也可以用改变某些参数的值来调整接口的信息,以适应未来的变更。 ⑧ 软件包应满足设计约束和可移植性。 (5) 事务映射 在很多应用中,存在某种作业数据流,它可以引发一个或多个处理。这种数据流就叫做事务。与变换映射类似,事务映射也是从分析数据流图开始,自顶向下,逐步分解,建立系统结构图。所不同的是由数据流图映射成的系统结构图不同。 步骤1:复审基本系统模型。 步骤2:复审和细化软件的数据流图。 步骤3:确定数据流图中含有变换流特征还是含有事务流特征。以上三步与变换映射中的相应工作相同。 步骤4:识别事务中心和每一条操作路径上的流特征。事务中心通常位于几条操作路径的起始点上,可以从数据流图上直接找出来。输入路径必须与其它所有操作路径区分开来。 步骤5:将数据流图映射到事务型系统结构图上。事务流应映射到包含一个输入分支和一个分类事务处理分支的程序结构上。输入分支结构的开发与变换流的方法类似。分类事务处理分支结构包含一个调度模块,它调度和控制下属的操作模块。 步骤6:“因子化”分解和细化该事务结构和每一条操作路径的结构。每一条操作路径的数据流图由它自己的信息流特征,可以是变换流也可以是事务流。与每一条操作路径相关的子结构可以依照前面介绍的设计步骤进行开发。 步骤7:利用一些启发式原则来改进系统的初始结构图。 (6) 注意“黑箱”技术的使用。 在设计当前模块时, 先把这个模块的所有下层模块定义成“黑箱”,并在系统设计中利用它们,暂时不考虑它们的内部结构和实现方法。在这一步定义好的“黑箱”,由于已确定了它的功能和输入、输出,在下一步就可以对它们进行设计和加工。这样,又会导致更多的“黑箱”。最后,全部“黑箱”的内容和结构应完全被确定。这就是我们所说的自顶向下,逐步求精的过程。使用黑箱技术的主要好处是使设计人员可以只关心当前的有关问题,暂时不必考虑进一步的琐碎的次要的细节,待进一步分解时才去关心它们的内部细节与结构。 6. Jackson系统开发方法 Jackson方法是一种典型的面向数据结构的分析与设计方法。早期的Jackson方法用于小系统的设计,称之为Jackson结构程序设计方法,简称JSP方法。它是按输入、输出和内部信息的数据结构进行软件设计的,即把数据结构的描述映射成程序结构描述。若数据结构有重复性,则对应程序一定有循环控制结构;若数据结构具有选择性,则对应程序一定需要有判定控制结构,以此揭示数据结构和程序结构之间的内在关系,设计出反映数据结构的程序结构。JSP方法的的三步曲是信息 → 数据结构 → 程序结构,这三步曲减少了设计决策上的盲目性。但是,当把JSP方法用于大系统设计时,就会出现大量复杂的难以对付的结构冲突。因此,促使M.J.Jackson提出了JSD方法,即Jackson系统开发方法。 (1) JSD方法的步骤 Jackson系统开发方法把分析的重点放在构造与系统相关联的现实世界,并建立现实世界的信息域的模型上。它实际上是支持软件分析与设计的一组连续的技术步骤。而且,JSD方法的最终目标是生成软件的过程性描述,没有特别考虑程序模块化结构,模块只是作为过程的副产品而出现,没有特别强调模块独立性。使用JSD方法的步骤如下: ① 实体动作分析:从问题的简单描述中,选出软件系统要产生和运用的实体(人、物或组织),以及现实世界作用于实体上的动作(事件)。实体从名词中选出,动作从动词中选出。当然,只有与问题求解直接有关的实体和动作才能被选出做进一步的分析。 ② 实体结构分析:把作用于实体的动作或由实体执行的动作,按时间发生的先后次序排序,并用一个层状的Jackson结构图表示。 ③ 定义初始模型:把实体和动作表示成一个过程性的模型,定义模型与现实世界的联系。模型系统的规格说明可用系统规格说明图SSD来表示。 ④ 功能描述:详细说明与已定义的动作相对应的功能。 ⑤ 决定系统时间特性:对进程调度特性进行评价和说明。 ⑥ 实现:设计组成系统的硬件和软件。 JSD方法的前三步属于需求分析阶段,后三步属于软件设计阶段。 (2) Jackson程序设计方法JSP JSP的本质就是“问题应当被分解为可以用三种结构形式表示的构件的层次结构。” Jackson所说的“结构形式”就是指顺序、选择和重复,实际上,它们就是过程性构造,并成为结构化程序设计方法基础。 ① 数据结构表示法 Jackson提出的数据结构表示有三种基本的构造类型,如图4.16所示。图(a)是顺序结构,即数据结构A由B、C、D三个成分组成且按B、C、D顺序排列。图(b)是选择结构,即数据结构A或者由B组成,或者由C组成,二者必具其一。可选择的数据(子结构或数据项)加“°”表示。在图(c)中表示的是重复结构,即数据结构A由多个B子结构组成,子结构用“*”加以标记。  图4.16 Jackson数据结构图 三种基本结构可以组合,形成更复杂的结构体系。如图4.17所示。这种数据结构图可以同样方便地应用于输入、输出和数据库结构。 下面用一个信用卡记账的例子具体说明。信用卡记账系统的输入数据结构是两个实际的账册,它们对应的两个输入文件如图4.18所示。 两个实际输入账册都按顾客号码进行登录,所以两个输入文件也是以顾客号码组织记录的。在支付账册中,每个顾客号码行上要登记支付金额、支付日期。则在支付文件中的每个记录中也对应有支付金额(AMT)和支付日期(DATE)项,另外还有标识这个记录的顾客号码(CNO)项。在支付账册中是以顾客号码进行排序的,顾客号码相同的支付记录在支付账册中排列在一起,构成关于该顾客的顾客号码组,在支付文件中也有与之对应的顾客号码组。在顾客主账册中,每一个顾客号码行上登记了顾客号码(CNO)和结余(BAL),给出某一位顾客的支付能力情况。与之对应的顾客主文件每个记录也有这两项。两个输入文件的内容是一致的。对应这两个输入文件的输入数据结构画在文件图示的左侧。   图4.18 信用卡记账系统的输入 图4.19(a) 给出了信用卡记账系统的输出记账报告,在报告中隐含了一个“店方总计”层次。报告中其它部分是顾客信息,因此,隐含了一个与“店方总计”同一层次的“顾客数据”子结构。图4.19(b) 给出了根据输入和输出数据结构的对应关系建立的输出文件。   图4.19 信用卡记账系统的输出 从层次性的输入和(或)输出数据结构可以直接推导出程序或进程的过程性表示。例如从图4.19(b) 所示的输出文件的Jackson数据结构图所导出的程序结构如图4.20所示。  图4.20 从输出数据结构导出的程序结构 最后,利用Jackson给出的三种图解来表示程序或进程的执行逻辑。这种图解类似于程序设计语言,实际上它是一种伪码表示。三种基本控制结构的图解如图4.21所示。  图4.21 三种基本控制结构的图解 在给出程序的过程性描述时,还需要添加一些必要的可执行操作。例如,打开文件、读文件结束符、读表头数据项、读行数据项、打印符号行、打印数字行、关闭文件等等。把它们分配到程序或进程结构的适当位置,以得到一个完整的过程性描述。对于上例中“处理顾客数据”部分,给出过程性描述如下: PROCESS_CUST_DATA seq open PAY_FILE; open CUST_M_FILE; {分别打开支付文件和顾客主文件} PROCESS_CNO_GROUP iter until eof: PAY_FILE; {处理顾客号码组} read PAY_FILE; {读支付文件一个记录} PROCESS_CNO; {读顾客主文件一个记录,找老结余} PROCESS_PAY_RECORD iter until end: CNO_GROUP; {处理顾客号码组中每个支付记录} write report line; {写出报告行} compute total payments; {计算总支付额} read PAY_FILE; {读支付文件下一个记录} PROCESS_PAY_RECORD end; {一位顾客数据处理完} COMPUTE_CUST_TOTAL; {计算顾客总数} COMPUTE_BALANCE seq {计算结余} PROCESS_OLD_BALANCE; {处理老结余} COMPUTE_NEW_BALANCE; {计算新结余) write report line; {写出报告行} COMPUTE_BALANCE end; {计算结余完毕} PROCESS_CNO_GROUP end; {支付文件处理完成} PROCESS_CUST_DATA end ; {Substructure PROCESS_CUST_DATA} 用JSP方法得到的程序或进程结构图,一般都需要求精和优化。因为这种方法是从输入输出数据结构导出程序结构图,因此有些中间处理过程在结构图中反映不出来。在求精过程中,可以对结构图进行改进和细化,使之完整和易于实现。 (3) 结构冲突 Jackson把结构冲突定义为三种类型。 ① 顺序冲突 顺序冲突是指在输入数据结构和输出数据结构中,输入数据与输出数据的顺序冲突。 例如,仓库存放多种零件P1,P2,…,每个零件的每次变动(收或发)都有一张卡片做记录。库存管理系统的输入数据结构是一叠卡片组成的文件。文件包括许多零件组,每个零件组又包括许多卡片(变动记录),每张卡片又分别可以是“收”或“发”,图4.22(b)。输出数据结构是月报表,见图4.22(a)。表中列出每种零件的净变化,一种零件的净变化占一行,见图4.22(c)。月报表来自输入文件,所以它们之间有很好的对应性。  图4.22 顺序冲突的例子 月报表每一行的内容来自输入文件的每一个零件组,行数与零件组个数相同,排列顺序一致(均按零件号递增次序排列)。这样输入数据与输出数据在内容、数量、顺序上的对应性找到了,也就等于理解了用户所需加工要求,因而很容易导出对应的程序结构。 如果卡片不是按零件组分组,而是按“发”或“收”的日期排列,这样输入数据结构与输出数据结构就找不到上述的对应关系。这种情形就是“顺序冲突”。 ② 边界冲突 在输入与输出数据结构中,虽然输入数据与输出数据的顺序相同,但分解不一样。 ③ 多重穿插冲突 在输入与输出数据结构中,输入数据与输出数据的顺序相同,而且处于同一程序段中,就象绳索穿绕一样。这些数据并行运行,同时在输入文件中重叠在一起。为了解决上述的结构冲突,Jackson提出了定义中间数据结构来解决冲突的方法。它可归纳成以下4点: ( 利用数据结构图来定义输入数据结构的特性; ( 将输入数据结构的元素分解,构造成中间数据结构; ( 描述输出数据结构的特性; ( 根据中间数据结构,构造输出数据结构. 这就需要两套程序结构,一个是把输入数据结构转换成中间数据结构;另一个是把中间数据结构转换成输出数据结构。Myers提出了一种叫做“程序变换”或“多道穿插”的方法,用以解决冲突。 即设计两个子程序,它们既能单独执行,又能并发执行。这样,中间数据结构也可以不要,而使用一个单记录缓冲区来代替它。第一个子程序读入输入记录,把它写到缓冲区中,然后开始等待,直到这个缓冲区变空,然后再把读入的下一个输入记录写到缓冲区中。第二个子程序在等待,待到一个记录被放入缓冲区中之后,它开始读它并进行处理,之后它再等待,直到下一个记录再放入缓冲区后,再读它并进行处理。 例如,用卡片按行输入一个矩阵文件,要求按列打印该矩阵,图4.23(a)表明这个例子的输入、输出数据结构。从图中可见,卡片文件和打印文件之间存在着对应关系,但行和列的内容不符,行选择和列选择的次序不同。因此,没有办法从这个结构图上构造出相应的程序结构。这就是所谓的结构冲突。  图4.23 解决冲突的例子 对于这个问题,只能把输入和输出的处理分开来解决。办法有两种。其一是先读入全部卡片,然后再输出,其程序结构图如图4.23(b)所示。 其二是更一般的方法: 构造两个程序结构,分别处理读入和打印,中间有一个排序,如图4.23(c)所示。 7. 数据设计和文件设计的原则 (1) 数据设计的原则 R.S.Pressman把数据设计的过程概括成以下两步: ① 为在需求分析阶段所确定的数据对象选择逻辑表示,需要对不同结构进行算法分析,以便选择一个最有效的设计方案。 ② 确定对逻辑数据结构所必需的那些操作的程序模块(软件包),以便限制或确定各个数据设计决策的影响范围。 无论采取什么样的设计方法,如果数据设计得好,往往能产生很好的软件系统结构,具有很强的模块独立性和较低的程序复杂性。 Pressman提出了一组原则,用来定义和设计数据。 ( 用于软件的系统化方法也适用于数据。应当考虑几种不同的数据组织方案,还应当分析数据设计给软件设计带来的影响。 ( 要确定所有的数据结构和在每种数据结构上施加的操作。对于涉及到软件中若干个功能的实现处理的复杂数据结构,可以为它定义一个抽象数据类型。 ( 应当建立一个数据词典并用它来定义数据和软件的设计。 ( 低层数据设计的决策应推迟到设计过程的后期进行。可以将逐步细化的方法用于数据设计。在需求分析时确定总体数据组织,在概要设计阶段加以细化,而在详细设计阶段才规定具体的细节。 ( 数据结构的表示只限于那些必须直接使用该数据结构内数据的模块才能知道。此原则就是信息隐蔽和与此相关的耦合性原则,把数据对象的逻辑形式与物理形式分开。 ( 数据结构应当设计成为可复用的。建立一个存有各种可复用的数据结构模型的构件库,以减少数据定义和设计的工作量。 ( 软件设计和程序设计语言应当支持抽象数据类型的定义和实现。如果没有直接定义某种复杂数据结构的手段,这种结构的设计和实现往往是很困难的。 以上原则可适用于软件工程的定义阶段和开发阶段。“清晰的信息定义是软件开发成功的关键”。 (2) 文件设计的过程 文件设计指数据存储文件设计,其主要工作就是根据使用要求、处理方式、存储的信息量、数据的活动性,以及所能提供的设备条件等,来确定文件类别,选择文件媒体,决定文件组织方法,设计文件记录格式,并估算文件的容量。 文件设计的过程主要分为两个阶段。第一个阶段是文件的逻辑设计,主要在概要设计阶段实施。它包括: ① 整理必须的数据元素。在软件设计中所使用的数据,有长期的,有短期的,还有临时的。它们都可以存放在文件中,在需要时对它们进行访问。因此首先必须整理应存储的数据元素,给它们一个易于理解的名字,指明其类型和位数,以及其内容涵义。 ② 分析数据间的关系。分析在业务处理中哪些数据元素是同时使用的。把同时使用次数多的数据元素归纳成一个文件进行管理。分析数据元素的内容,研究数据元素与数据元素之间的逻辑关系,根据分析,弄清数据元素的含义及其属性。 ③ 确定文件的逻辑设计。根据数据关联性分析,明确哪些数据元素应当归于一组进行管理,把应当归于一组的数据元素进行统一布局,产生文件的逻辑设计。 第二个阶段是文件的物理设计,主要在软件的详细设计阶段实施。主要工作有: ④ 理解文件的特性。针对文件的逻辑规格说明,进一步研究从业务处理的观点来看所要求的一些特性,包括文件的使用率、追加率和删除率,以及保护和保密等。 ⑤ 确定文件的存储媒体。选择文件的存储媒体,应当考虑以下一些因素。 ( 数据量:根据处理数据量,估算需要媒体的数量。数据量大的文件可选用磁带、磁盘或光盘做为存储媒体,数据量小的文件可采用软盘做为存储媒体。 ( 处理方式:处理方式有联机处理和批处理。对于联机处理, 多选用直接存取设备,如磁盘等;对于批处理,选用任何一种存储媒体都可以。 ( 存取时间和处理时间:批处理对于时间没有严格的要求, 因此对存储媒体也没有特殊的要求。实时处理最好选用直接存取媒体,如磁盘等,以满足响应时间的要求。 ( 数据结构:根据文件的数据结构,选用能实现其结构的合适媒体及相应的存取方法。例如,顺序文件可选用磁带或光盘,而索引文件和散列文件则必须选用磁盘。 ( 操作要求:对于数据量大,执行时较少要求用户干预的文件,应当选用磁带媒体;而对于频繁交互的文件,应当选用磁盘媒体。 ( 费用要求:在满足上述要求的基础上,应当尽量选用价格低的媒体。 ⑥ 确定文件的组织方式。根据文件的特性,来确定文件的组织方式。常用的文件组织方式有:顺序文件(按记录的加入先后次序排列、按记录关键码的升序或降序排列、按记录的使用频率排列);直接存取文件(无关键码直接存取文件、带关键码直接存取文件、桶式直接存取文件);索引顺序文件(B+树);分区文件;虚拟存储文件;倒排文件等。 ⑦ 确定文件的记录格式。确定了文件的组织方式之后,需要进一步确定文件记录中各数据项以及它们在记录中的物理安排。考虑设计记录的布局时,应当注意以下几点: ( 记录的长度:设计记录的长度要确保能满足需要,还要考虑使用设备的制约和效率,尽可能与读写单位匹配,并尽可能减少处理过程中内外存的交换次数。 ( 数据项的顺序:对于可变长记录,应在记录的开头记入长度信息;对于关键码,应尽量按级别高低,顺序配置;联系较密切的数据项,应归纳在一起进行配置。 ( 数据项的属性:属性相同的数据项,应尽量归纳在一起配置;数据项应按双字长,全字长,半字长和字节的属性,顺序配置。 ( 预留空间:考虑到将来可能的变更或扩充,应当预先留下一些空闲空间。 ( 子数据项:可把一个数据项分成几个子数据项,每一个子数据项也可以做为单独的项来使用。 ⑧ 估算存取时间和存储容量(不要求) 8. 过程设计 从软件开发的工程化观点来看,在使用程序设计语言编制程序以前,需要对所采用算法的逻辑关系进行分析,设计出全部必要的过程细节,并给予清晰的表达,使之成为编码的依据。这就是过程设计的任务。 过程设计也叫做详细设计或程序设计,它不同于编码或编程。在过程设计阶段,要决定各个模块的实现算法,并精确地表达这些算法。前者涉及所开发项目的具体要求和对每个模块规定的功能。以及算法的设计和评价,后者需要给出适当的算法描述,为此应提供过程设计的表达工具。 表达过程规格说明的工具叫做详细设计工具,它可以分为三类:图形工具、表格工具和语言工具。 (1) 程序流程图 程序流程图独立于任何一种程序设计语言,比较直观、清晰,易于学习掌握。但流程图也存在一些严重的缺点。例如流程图所使用的符号不够规范,常常使用一些习惯性用法。特别是表示程序控制流程的箭头可以不受任何约束,随意转移控制。这些现象显然是与软件工程化的要求相背离的。为了消除这些缺点,应对流程图所使用的符号做出严格的定义,不允许人们随心所欲地画出各种不规范的流程图。例如,为使用流程图描述结构化程序,必须限制流程图只能使用图4.24所给出的五种基本控制结构。  图4.24 流程图的基本控制结构 任何复杂的程序流程图都应由这五种基本控制结构组合或嵌套而成。作为上述五种控制结构相互组合和嵌套的实例,图4.25给出一个程序的流程图。图中增加了一些虚线构成的框,目的是便于理解控制结构的嵌套关系。显然,这个流程图所描述的程序是结构化的。   图4.25 嵌套构成的流程图实例 (2) N-S图 Nassi和Shneiderman 提出了一种符合结构化程序设计原则的图形描述工具,叫做盒图,也叫做N-S图。为表示五种基本控制结构,在N-S图中规定了五种图形构件。参看图4.26。  图4.26 N-S图的五种基本控制结构 为说明N-S图的使用,仍用图4.25给出的实例,将它用如图4.27所示的N-S图表示。 如前所述,任何一个N-S图,都是前面介绍的五种基本控制结构相互组合与嵌套的结果。当问题很复杂时,N-S图可能很大。  图4.27 N-S图的实例 (3) PAD PAD是Problem Analysis Diagram的缩写,它是日本日立公司提出,由程序流程图演化来的,用结构化程序设计思想表现程序逻辑结构的图形工具。现在已为ISO认可。 PAD也设置了五种基本控制结构的图式,并允许递归使用。  图4.28 PAD的基本控制结构 做为PAD应用的实例,图4.29给出了图4.25程序的PAD表示。PAD所描述程序的层次关系表现在纵线上。每条纵线表示了一个层次。把PAD图从左到右展开。随着程序层次的增加,PAD逐渐向右展开。 PAD的执行顺序从最左主干线的上端的结点开始,自上而下依次执行。 每遇到判断或循环,就自左而右进入下一层,从表示下一层的纵线上端开始执行,直到该纵线下端,再返回上一层的纵线的转入处。如此继续,直到执行到主干线的下端为止。  图4.29 PAD实例 (4) 判定表 当算法中包含多重嵌套的条件选择时,用程序流程图、N-S图或PAD都不易清楚地描述。然而,判定表却能清晰地表达复杂的条件组合与应做动作之间的对应关系。仍然使用图4.16的例子。为了能适应判定表条件取值只能是“T”和“F”的情形,对原图稍微做了些改动,把多分支判断改为两分支判断,但整个图逻辑没有改变。见图4.30。   图4.30 不包含多分支结构的流程图实例 与图4.30表示的流程图对应的判定表如图4.31所示。在表的右上半部分中列出所有条件,“T”表示该条件取值为真,“F”表示该条件取值为假,空白表示这个条件无论取何值对动作的选择不产生影响。在判定表右下半部分中列出所有的处理,画“Y”表示要做这个动作,空白表示不做这个动作。判定表右半部的每一列实质上是一条规则,规定了与特定条件取值组合相对应的动作。  图4.31 反映程序逻辑的判定表 判定表的优点是能够简洁,无二义性地描述所有的处理规则。但判定表表示的是静态逻辑,是在某种条件取值组合情况下可能的结果,它不能表达加工的顺序,也不能表达循环结构,因此判定表不能成为一种通用的设计工具。 (5) PDL ( Program Design Language ) PDL是一种用于描述功能模块的算法设计和加工细节的语言。称为设计程序用语言。它是一种伪码。一般地,伪码的语法规则分为“外语法”和“内语法”。外语法应当符合一般程序设计语言常用语句的语法规则;而内语法可以用英语中一些简单的句子、短语和通用的数学符号,来描述程序应执行的功能。 PDL就是这样一种伪码。它具有严格的关键字外语法,用于定义控制结构和数据结构,同时它的表示实际操作和条件的内语法又是灵活自由的,可使用自然语言的词汇。下面举一个例子,来看PDL的使用。   PROCEDURE spellcheck IS 查找错拼的单词 BEGIN split document into single words 把整个文档分离成单词 lood up words in dictionary 在字典中查这些单词 display words which are not in dictionary 显示字典中查不到的单词 create a new dictionary 造一新字典 END spellcheck 从上例可以看到,PDL 语言具有正文格式,很像一个高级语言。人们可以很方便地使用计算机完成PDL的书写和编辑工作。 PDL作为一种用于描述程序逻辑设计的语言,具有以下特点: ( 有固定的关键字外语法,提供全部结构化控制结构、数据说明和模块特征。属于外语法的关键字是有限的词汇集,它们能对PDL正文进行结构分割,使之变得易于理解。为了区别关键字,规定关键字一律大写,其它单词一律小写。 ( 内语法使用自然语言来描述处理特性。内语法比较灵活,只要写清楚就可以,不必考虑语法错,以利于人们可把主要精力放在描述算法的逻辑上。 ( 有数据说明机制,包括简单的(如标量和数组)与复杂的(如链表和层次结构)的数据结构。 ( 有子程序定义与调用机制,用以表达各种方式的接口说明。 使用PDL语言,可以做到逐步求精:从比较概括和抽象的PDL程序起,逐步写出更详细的更精确的描述。 (6) HIPO图 ( Hierarchy plus Input Process Output ) HIPO最初只用做文档编写的格式要求,随后发展成比较有名的软件设计手段。HIPO图采用功能框图和PDL来描述程序逻辑,它由两部分组成:可视目录表和IPO图。可视目录表给出程序的层次关系,IPO图则为程序各部分提供具体的工作细节。 ① 可视目录表 :由体系框图、图例、描述说明三部分组成。 ( 体系框图。又称层次图(H图),是可视目录表的主体,用它表明各个功能的隶属关系。它是自顶向下逐层分解得到的,是一个树形结构。它的顶层是整个系统的名称和系统的概括功能说明;第二层把系统的功能展开,分成了几个框;第二层功能进一步分解,就得到了第三层、第四层,…,直到最后一层。每个框内都应有一个名字,用以标识它的功能。还应有一个编号,以记录它所在的层次及在该层次的位置。 ( 图例。每一套HIPO图都应当有一个图例,即图形符号说明。附上图例,不管人们在什么时侯阅读它都能对其符号的意义一目了然。 ( 描述说明。它是对层次图中每一框的补充说明,在必须说明时才用,所以它是可选的。描述说明可以使用自然语言。 例如,应用HIPO法对盘存/销售系统进行分析。得到如图4.32所示的工作流程图。   图4.32 盘存/销售系统工作流程图 分析此工作流程图,可得如图4.33所示的可视目录表。图4.33(a)是系统的层次图,图4.33(b)是后面IPO图的图例,图4.33(c)是描述说明。 ② IPO图 :IPO图为层次图中每一功能框详细地指明输入、处理及输出。通常,IPO图有固定的格式,图中处理操作部分总是列在中间,输入和输出部分分别在其左边和右边。由于某些细节很难在一张IPO图中表达清楚,常常把IPO图又分为两部分,简单概括的称为概要IPO图,细致具体一些的称为详细IPO图。   图4.33 盘存/销售系统的可视目录表 概要IPO图用于表达对一个系统,或对其中某一个子系统功能的概略表达,指明在完成某一功能框规定的功能时需要哪些输入,哪些操作和哪些输出。图4.34是表示销售/盘存系统第二层的对应于H图上的1.1.0框的概要IPO图。  图4.34 对应H图上1.1.0框的概要IPO图 在概要IPO图中,没有指明输入―处理―输出三者之间的关系, 用它来进行下一步的设计是不可能的。故需要使用详细IPO 图以指明输入―处理―输出三者之间的关系,其图形与概要IPO图一样,但输入、输出最好用具体的介质和设备类型的图形表示。图4.35是销售/盘存系统中对应于1.1.2框的一张详细IPO图。  图4.35 对应于H图1.1.2框的详细IPO图 ③ 利用HIPO进行迭代式细化设计 在软件设计时,解决设计问题通常需要经历一个认识逐步发展的过程,并且对一些问题还要经过反复的考虑才可能达到比较满意的设计效果。我们称此为迭代式细化设计。HIPO能很好地适应这一要求。图4.36是利用HIPO进行迭代式细化设计的示意图。从图中可看到,把可视目录表和IPO图结合起来,反复交替地使用它们,可使得设计工作逐步深化,最终取得完满的设计结果。其实这正是自顶向下,逐步求精的结构化程序设计思想。   图4.36 利用HIPO进行迭代式细化设计 HIPO有自己的特点。首先,这一图形表达方法容易看懂。其次,HIPO的适用范围很广,绝不限于详细设计。事实上,画可视目录表就是与概要设计密切相关的工作。如果利用它仅仅表达软件要达到的功能,则是需求分析中描述需求的很好的工具。因为HIPO是在开发过程中的表达工具,所以它又是开发文档的编制工具。开发完成后,HIPO图就是很好的文档,而不必在设计完成以后,专门补写文档。 9. 设计规格说明与设计评审 软件设计规格说明的大纲如表4.3所示。每一个编号的段落描述了设计模型的不同侧面。在设计人员细化他们的软件设计时,就可以逐步完成各章节内容的编写。 表4.3 软件设计规格说明的大纲 Ⅰ. 工作范围 A. 系统目标 B. 运行环境 C. 主要软件需求 D. 设计约束∕限制  Ⅱ. 体系结构设计 A. 数据流与控制流复审 B. 导出的程序结构 C. 功能与程序交叉索引  Ⅲ. 数据设计 A. 数据对象与形成的数据结构 B. 文件和数据库结构 ⅰ文件的逻辑结构 ⅱ 文件逻辑记录描述 ⅲ 访问方式 C. 全局数据 D. 文件∕数据与程序交叉索引  Ⅳ. 接口设计 A. 人机界面规格说明 B. 人机界面设计规则 C. 外部接口设计 ⅰ外部数据接口 ⅱ 外部系统或设备接口 D. 内部接口设计规则  Ⅴ.(每个模块的)过程设计 A. 处理与算法描述 B. 接口描述 C. 设计语言(或其它)描述 D. 使用的模块 E. 内部程序逻辑描述 F. 注释∕约束∕限制  Ⅵ. 运行设计 A. 运行模块组合 B. 运行控制规则 C. 运行时间安排  Ⅶ. 出错处理设计 A. 出错处理信息 B. 出错处理对策 ⅰ设置后备 ⅱ 性能降级 ⅲ 恢复和再启动  Ⅷ. 安全保密设计  Ⅸ. 需求∕设计交叉索引  Ⅹ. 测试部分 A. 测试方针 B. 集成策略 C. 特殊考虑  Ⅺ. 特殊注解  Ⅻ. 附录   软件设计的最终目标是要取得最佳方案。“最佳”是指在所有候选方案中,就节省开发费用,降低资源消耗,缩短开发时间的条件,选择能够赢得较高的生产率、较高的可靠性和可维护性的方案。在整个设计的过程中,各个时期的设计结果需要经过一系列的设计质量的评审,以便及时发现和及时解决在软件设计中出现的问题,防止把问题遗留到开发的后期阶段,造成后患。 设计评审的内容包括: ·可追溯性:即分析该软件的系统结构、子系统结构,确认该软件设计是否复盖了所有已确定的软件需求,软件每一成分是否可追溯到某一项需求。 ·接口:即分析软件各部分之间的联系,确认该软件的内部接口与外部接口是否已经明确定义。模块是否满足高内聚和低耦合的要求。模块作用范围是否在其控制范围之内。 ·风险:即确认该软件设计在现有技术条件下和预算范围内是否能按时实现。 ·实用性:即确认该软件设计对于需求的解决方案是否实用。 ·技术清晰度:即确认该软件设计是否以一种易于翻译成代码的形式表达。 ·可维护性:从软件维护的角度出发,确认该软件设计是否考虑了方便未来的维护。 ·质量:即确认该软件设计是否表现出良好的质量特征。 ·各种选择方案:看是否考虑过其它方案,比较各种选择方案的标准是什么。 ·限制:评估对该软件的限制是否现实,是否与需求一致。 ·其它具体问题:对于文档、可测试性、设计过程,……,等等进行评估。 在这里需要特别注意:软件系统的一些外部特性的设计,例如软件的功能、一部分性能、以及用户的使用特性等,在软件需求分析阶段就已经开始。这些问题的解决,多少带有一些“怎么做”的性质,因此有人称之为软件的外部设计。 三、例题分析 【例1】从下列有关系统结构图的叙述中选出正确的叙述。 (1) 系统结构图中反映的是程序中数据流的情况。 (2) 系统结构图是精确表达程序结构的图形表示法。因此,有时也可将系统结构当作程序流程图使用。 (3) 一个模块的多个下属模块在系统结构图中所处的左右位置是无关紧要的。 (4) 在系统结构图中,上级模块与其下属模块之间的调用关系用有向线段表示。这时,使用斜的线段和水平、垂直的线段具有相同的含义。 答案: (4) 分析:系统结构图反映的是系统中模块的调用关系和层次关系,谁调用谁,有一个先后次序(时序)关系。所以系统结构图既不同于数据流图,也不同与程序流程图。数据流图仅描述数据在系统中如何流动,如何处理和存储,它不考虑时序关系。图中的有向线段表示了数据流。程序流程图描述程序中控制流的情况,即程序中处理的执行顺序和执行序列所依赖的条件,图中的有向线段(流线)表示的是控制流,从一个处理走到下一个处理。但在系统结构图中的有向线段表示调用时程序的控制从调用模块移到被调用模块,并隐含了当调用结束时控制将交回给调用模块。 如果一个模块有多个下属模块,这些下属模块的左右位置可能与它们的调用次序有关。例如,在用结构化设计方法依据数据流图建立起来的变换型系统结构图中,主模块的所有下属模块按逻辑输入、中心变换、逻辑输出的次序自左向右一字排开,左右位置不是无关紧要的。所以只有最后的一个叙述是正确的。 【例2】软件的开发工作经过需求分析阶段,进入( A )以后,就开始着手解决“怎么做”的问题。常用的软件设计方法有( B )、( C )、( D )和( E )等方法。 供选择的答案: A ( B.① 程序设计 ② 设计阶段 ③ 总体设计 ④ 定义阶段 ⑤ SD方法 ⑥ SP方法 C. ① Jackson方法 ② 瀑布法 ③ 快速原型法 ④ 回溯法 D ( E. ① LCP(Wanier)方法 ② 递归法 ③ Parnas方法 ④ 自下而上修正 ⑤ 逐步求精法 ⑥ 检测校正法 答案:A. ②, B. ⑤, C. ①, D. ①, E. ③。其中,D与E的答案可互换。 分析:进入设计阶段之后,就开始着手解决“怎么做”的问题。一般把设计阶段的工作分成两步:即概要设计和详细设计。在概要设计阶段应着重解决实现需求的程序模块划分问题,在详细设计阶段则要决定每个模块的具体算法。 常见的软件概要设计方法有三大类: ( 以数据流图为基础构造模块结构的结构化设计方法(SD); ( 以数据结构为基础构造模块结构的Jackson方法和LCP(Wanier)逻辑构造方法; ( 以对象、类、继承和通信为基础的面向对象设计方法(OOD)。 此外,以信息隐蔽为原则的Parnas方法虽然没有给出系统化的设计方法,但它提出了一组原则,要求预先估计未来生存周期中可能发生的种种情况,并采取相应措施以提高软件系统的可维护性和可靠性。 这里对面向数据结构的Jackson方法和LCP方法再多说几句。 Jackson方法是一种典型的面向面向数据结构开发软件的方法。它的基本思想是首先根据实际问题,给出处理问题所需要和产生的数据结构,一旦搞清了问题的输入∕输出数据结构,就可以以简单的方式直接导出程序的处理结构,然后应用Jackson的描述符号,将这个处理结构转换为程序的过程性描述。 LCP方法是另一种面向数据结构的方法,它也要先给出用Wanier图表示的处理问题所需要和产生的数据结构,再在Wanier图上直接将数据结构转换为加工处理的形式化表示,最后生成描述加工过程的伪代码,进行验证和优化。 【例3】请将下述有关模块独立性的各种模块之间的耦合,按其耦合度从低到高排列起来。 ① 内容耦合 ② 控制耦合 ③ 非直接耦合 ④ 标记耦合 ⑤ 数据耦合 ⑥ 外部耦合 ⑦ 公共耦合 答案:③、⑤、④、②、⑥、⑦、① 分析:参看“内容提要”中有关模块独立性的介绍。 【例4】请将下述有关模块独立性的各种模块内聚,按其内聚度(强度)从高到低排列起来。 ① 巧合内聚 ② 时间内聚 ③ 功能内聚 ④ 通信内聚 ⑤ 逻辑内聚 ⑥ 信息内聚 ⑦ 过程内聚 答案: ③、⑥、④、⑦、②、⑤、① 分析:在状态―迁移图中,由一个状态和一个事件所确定的下一状态可能会有多个。实际会迁移到哪一个状态,是由更详细的内部状态和更详细的事件信息来决定的,此时在状态―迁移图中可能需要使用加进判断框和处理框的记法。状态―迁移图的优点:第一,状态之间的关系能够直观地捕捉到,这样用眼睛就能看到是否所有可能的状态迁移都已纳入图中,是否存在不必要的状态等。第二,由于状态―迁移图的单纯性,能够机械地分析许多情况,可很容易地建立分析工具。 【例5】在结构化分析方法中,用实体―关系图表达系统中的对象及其关系。在实体―关系图中,表达对象的实例之间的关联有三种类型: 一对一联系、( )联系、多对多联系。 供选择的答案: A. 多对一 B. 一对多 答案:B 分析:使用实体―关系图,可以建立系统中各个数据对象及对象之间的关系。对象的实例间的关联称为“基数”,共有3种类型的基数:一对一,一对多,多对多。它反映了现实世界中实体之间的联系,多对一的情况可以归入一对多的关联中去。 【例6】 软件需求分析的任务不应包括( A )。进行需求分析可使用多种工具,但( B )是不适用的。在需求分析中,分析员要从用户那里解决的最重要的问题是( C )。需求规格说明书的内容不应当包括( D )。该文档在软件开发中具有重要的作用,但其作用不应当包括( E )。 供选择的答案: A. ① 问题分析 ② 信息域分析 ③ 结构化程序设计 ④ 确定逻辑模型 B. ① 数据流图 ② 判定表 ③ PAD图 ④ 数据词典 C. ① 要让软件做什么 ② 要给该软件提供哪些信息 ③ 要求软件工作效率如何 ④ 要让软件具有什么样的结构 D. ① 对重要功能的描述 ② 对算法的详细过程性描述 ③ 软件确认准则 ④ 软件的性能 E. ① 软件设计的依据 ② 用户和开发人员对软件要“做什么”的共同理解 ③ 软件验收的依据 ④ 软件可行性分析的依据 答案:A. ③ B. ③ C. ① D. ② E. ④ 分析:软件需求分析的任务是通过与用户的合作,了解用户对待开发系统的要求;根据对用户要求的系统所在的信息域的调查、分析,确定系统的逻辑模型;并对求解的问题做适当的分解,使之适合于计算机求解。需求分析的结果是软件需求规格说明书。 结构化程序设计是在详细设计和编码阶段所采用的技术,而不是需求分析阶段要采用的技术。在需求分析阶段,分析人员可以用数据流图描述系统的数据流的变换和流向,用数据词典定义在数据流图中出现的数据流、数据文件、加工或处理,用判定表表示复杂条件和动作组合的情况。但PAD图是在详细设计阶段使用的描述加工逻辑的工具,不适用于需求分析。此外,软件需求分析阶段只确定软件系统要“做什么”,完成对重要功能、性能、确认准则的描述,至于“怎么做”由后续的设计阶段完成,对算法的详细过程性描述也是在设计阶段给出。软件可行性分析应在需求分析之前,所以需求分析规格说明不能成为可行性分析的依据。 【例7】原型化方法是用户和软件开发人员之间进行的一种交互过程,适用于( A )系统。它从用户界面的开发入手,首先形成( B ),用户( C ),并就( D )提出意见,它是一种( E )型的设计过程。 供选择的答案: A. ① 需求不确定性高的 ② 需求确定的 ③ 管理信息 ④ 决策支持 B. ① 用户界面使用手册 ② 用户界面需求分析说明书 ③ 系统界面原型 ④ 完善的用户界面 C. ① 改进用户界面的设计 ② 阅读文档资料 ③ 模拟用户界面的运行 ④ 运行用户界面原型 D. ① 同意什么和不同意什么 ② 使用和不使用哪一种编程语言 ③ 程序的结构 ④ 执行速度是否满足要求 E. ① 自外向内 ② 自顶向下 ③ 自内向外 ④ 自底向上 答案:A. ①, B. ③, C. ④, D. ①, E. ① 分析:通常,原型是指模拟某种产品的原始模型。在软件开发中,原型是软件的一个早期可运行的版本,它反映最终系统的部分重要特性。 使用原型的原型化方法特别适用于需求不确定性较高的软件系统的开发。它的基本思想是根据用户给出的基本需求,通过快速实现构造出一个小型的可执行的模型,满足用户的基本要求,这就是系统界面原型。让用户计算机上实际运行这个用户界面原型,在试用的过程中得到亲身感受和受到启发,做出反应和评价,提出同意什么和不同意什么。然后开发者根据用户的意见对原型加以改进。随着不断试验、纠错、使用、评价和修改,获得新的原型版本,如此周而复始,逐步减少分析和通信中的误解,弥补不足之处,进一步确定各种需求细节,适应需求的变更,从而提高了最终产品的质量。它是一种自外向内型的设计过程。 四、习题 【3-1】逐步求精、分层过程与抽象等概念之间的相互关系如何? 【3-2】完成良好的软件设计应遵循哪些原则? 【3-3】如何理解模块独立性?用什么指标来衡量模块独立性? 【3-4】模块独立性与信息隐蔽(反映模块化有效程度的属性)有何关系? 【3-5】模块的内聚性程度与该模块在分层结构中的位置有关系吗?说明你的论据。 【3-6】耦合性的概念和软件的可移植性有什么关系?请举例说明你的论述。 【3-7】从供选择的答案中选出正确的答案填入下列叙述中的( ) 内。 模块内聚性用于衡量模块内部各成份之间彼此结合的紧密程度。 (1) 一组语句在程序中多处出现,为了节省内存空间把这些语句放在一个模块中,该模块的内聚性是( A )的。 (2) 将几个逻辑上相似的成分放在同一个模块中,通过模块入口处的一个判断决定执行哪一个功能。该模块的内聚性是( B )的。 (3) 模块中所有成分引用共同的数据,该模块的内聚性是( C )的。 (4) 模块内的某成份的输出是另一些成份的输入,该模块的内聚性是( D )的。 (5) 模块中所有成份结合起来完全一项任务,该模块的内聚性是( E )的。它具有简明的外部界面,由它构成的软件易于理解、测试和维护。 供选择的答案: A ( E: ① 功能内聚 ② 信息内聚 ③ 通信内聚 ④ 过程内聚 ⑤ 巧合内聚 ⑥ 时间内聚 ⑦ 逻辑内聚 【3-8】从供选择的答案中选出正确的答案填入下面的( )中。 块间联系和块内联系是评价程序模块结构质量的重要标准。联系的方式、共用信息的作用、共用信息的数量和接口的( A )等因素决定了块间联系的大小。在块内联系中,( B )的块内联系最强。 SD方法的总的原则是使每个模块执行( C )功能,模块间传送( D )参数,模块通过( E )语句调用其它模块,而且模块间传送的参数应尽量( F )。 此外,SD方法还提出了判定的作用范围和模块的控制范围等概念。SD方法认为,( G )应该是( H )的子集。 供选择的答案: A: ① 友好性 ② 健壮性 ③ 简单性 ④ 安全性 B: ① 巧合内聚 ② 功能内聚 ③ 通信内聚 ④ 信息内聚 C: ① 一个 ② 多个 D: ① 数据型 ② 控制型 ③ 混合型 E: ① 直接引用 ② 标准调用 ③ 中断 ④ 宏调用 F: ① 少 ② 多 G ( H:① 作用范围 ② 控制范围 【3-9】从供选择的答案中选出应该填入下列关于软件设计的叙述的( )内的正确答案。 在众多的设计方法中,SD方法是最受人注意的,也是最广泛应用的一种,这种方法可以同分析阶段的( A )方法及编程阶段的( B )方法前后衔接,SD方法是考虑如何建立一个结构良好的程序结构,它提出了评价模块结构质量的两个具体标准——块间联系和块内联系。SD方法的最终目标是( C ),用于表示模块间调用关系的图叫( D )。 另一种比较著名的设计方法是以信息隐蔽为原则划分模块,这种方法叫( E )方法。 供选择的答案: A ( B:① Jackson ② SA ③ SC ④ Parnas ⑤ SP C: ① 块间联系大,块内联系大 ② 块间联系大,块内联系小 ③ 块间联系小,块内联系大 ④ 块间联系小,块内联系小 D: ① PAD ② HCP ③ SC ④ SADT ⑤ HIPO ⑥ NS E: ① Jackson ② Parnas ③ Turing ④ Wirth ⑤ Dijkstra 【3-10】递归模块(即自己调用自己的模块)的概念如何能够与本章所介绍的设计原理与方法相适应? 【3-11】举例说明你对概要设计与详细设计的理解。有不需要概要设计的情况吗? 【3-12】软件详细设计工具可分为三类,即:图示工具、设计语言和表格工具。图示工具中,( A )简单而应用广泛、( B )表示法中,每一个处理过程用一个盒子表示,盒子可以嵌套。( C )可以纵横延伸,图形的空间效果好。 ( D ) 是一种设计和描述程序的语言,它是一种面向( E )的语言。 供选择的答案: A ( C:① NS图 ② 流程图 ③ HIPO图 ④ PAD图 D: ① C ② PDL ③ RPOLOG ④ PASCAL E: ① 人 ② 机器 ③ 数据结构 ④ 对象 【3-13】如何用PDL语言来实施逐步求精的设计原理? 【3-14】从供选择的答案中选出应该填入下列关于软件设计的叙述的( )内的正确答案。 在完成软件概要设计,并编写出相关文档之后,应当组织对概要设计工作的评审。评审的内容包括: 分析该软件的系统结构、子系统结构,确认该软件设计是否覆盖了所有已确定的软件需求,软件每一成份是否可( A )到某一项需求。分析软件各部分之间的联系,确认该软件的内部接口与外部接口是否已经明确定义。模块是否满足( B )和( C )的要求。模块( D )是否在其( E )之内。 供选择的答案 A: ① 覆盖 ② 演化 ③ 追溯 ④ 等同 ⑤ 连接 B: ① 多功能 ② 高内聚 ③ 高耦合 ④ 高效率 ⑤ 可读性 C: ① 多入口 ② 低内聚 ③ 低耦合 ④ 低复杂度 ⑤ 低强度 D ( E:① 作用范围 ② 高内聚 ③ 低内聚 ④ 取值范围 ⑤ 控制范围 五、习题解答 【3-1】“自顶向下,逐步求精”是Niklaus Wirth提出的设计策略:即将软件的体系结构按自顶向下方式,对各个层次的过程细节和数据细节逐层细化,直到用程序设计语言的语句能够实现为止,从而最后确立整个的体系结构。 这样的结构实际就是一个模块的分层结构,即分层的过程。在实施时,采用抽象化的方法,自顶向下,给出不同的抽象层次。在最高的抽象层次上,可以使用问题所处环境的语言概括地描述问题的解法。而在较低的抽象层次上,则采用过程化的方法。在描述问题的解法时,我们可以配合使用面向问题的术语和面向现实的术语。但最后在最低的抽象层次上,我们应使用能够直接实现的方式来描述这个解法。 【3-2】软件设计既是过程又是模型。设计过程是一系列迭代的步骤,使设计人员能够描述被开发软件的方方面面。设计模型体现了自顶向下、逐步细化的思想,首先构造事物的整体,再逐步细化,引导人们构造各种细节。为了给软件设计人员提供一些指导,1995年Davis提出了一系列软件设计的原则如下,其中有些修改和补充: ·设计过程不应受“隧道视野”的限制。一位好的设计者应当考虑一些替代的手段。根据问题的要求,可以用基本的设计概念,如抽象、逐步求精、模块化、软件体系结构、控制层次、结构分解、数据结构、软件过程、信息隐蔽等,来决定完成工作的资源。 ·设计应能追溯到分析模型。由于设计模型中每一单个成份常常可追溯到多个需求上,因此有必要对设计模型如何满足需求进行追踪。 ·设计不应当从头做起。系统是使用一系列设计模式构造起来的,很多模式很可能以前就遇到过。这些模式通常被称为可复用的设计构件。可以使用它们代替那些从头开始做的方法。时间短暂而资源有限!设计时间应当投入到表示真正的新思想和集成那些已有的设计模式上去。 ·设计应当缩短软件和现实世界中问题的“智力差距”,就是说,软件设计的结果应尽可能模拟问题领域的结构。 ·设计应具有一致性和集成性。如果一个设计从整体上看像是一个人完成的,那它就是一致的。在设计工作开始之前,设计小组应当定义风格和格式的规则,如果仔细定义了设计构件之间的接口,则该设计就是集成的。 ·使用上述的基本的设计概念,将设计构造得便于将来的修改。 ·应将设计构造得即使遇到异常的数据、事件或操作条件,也能平滑地、轻松地降级。设计良好的计算机程序应当永不“彻底停工”,它应能适应异常的条件,并且当它必须中止处理时也能以从容的方式结束。 ·设计不是编码,编码也不是设计。即使在建立程序构件的详细的过程设计时,设计模型的抽象级别也比源代码要高。在编码级别上作出的唯一设计决策是描述如何将过程性设计转换为程序代码的小的实现细节。 ·在开始着手设计时就应当能够评估质量,而不是在事情完成之后。利用上述的基本的设计概念和已有的设计方法,可以帮助设计者评估质量。 ·应当坚持设计评审以减少概念上(语义上)的错误。有时人们在设计评审时倾向于注重细节,只见树木不见森林。在关注设计模型的语法之前,设计者应能确保设计的主要概念上的成份(的遗漏、含糊、不一致)都已检查过。 【3-3】如果两个模块互相独立,那么对其中一个模块进行编码、测试或修改时可以完全不考虑另一个模块对它的影响。因此,用模块独立性作为衡量模块结构是否容易编码、容易测试、容易修改的标准是合适的。但是,在一个系统的模块结构中没有哪两个模块可以完全独立,所以,要力争模块之间尽量独立,以得到一个质量良好的模块结构。 一般采用两个准则度量模块独立性。即模块间的耦合和模块的内聚。模块间的耦合是模块之间的相对独立性(互相连接的紧密程度)的度量。模块之间的连接越紧密,联系越多,耦合性就越高,而其模块独立性就越弱。内聚是模块功能强度(一个模块内部各个成份彼此结合的紧密程度)的度量。一个模块内部各个成份之间的联系越紧密,则它的内聚性就越高,相对地,它与其它模块之间的耦合性就会减低,而模块独立性就越强。因此,模块独立性比较强的模块应是高内聚低耦合的模块。 一般模块之间可能的连接方式有七种,构成耦合性的七种类型。它们之间的关系为  低耦合的情形有非直接耦合、数据耦合和标记耦合,它们都是比较好的模块间的连接。特点是模块间的接口简单、规范。中度耦合的情形有控制耦合,它通过参数表传递控制参数。相对高的耦合情形有外部耦合和公共耦合,它们都是通过全局数据传递模块间的信息,不是说它们一定“坏”,但一定要注意使用这类耦合可能产生的后果,特别要防范这种后果。 一般模块的内聚性分为七种类型,它们的关系如下图所示。  在上面的关系中可以看到,位于高端的几种内聚类型最好,位于中段的几种内聚类型是可以接受的,但位于低端的内聚类型很不好,一般不能使用。因此,人们总是希望一个模块的内聚类型向高的方向靠。模块的内聚在系统的模块化设计中是一个关键的因素。 内聚和耦合是相互关联的。在程序结构中各模块的内聚程度越高,模块间的耦合程度就越低。但这也不是绝对的。我们的目标是力求增加模块的内聚,尽量减少模块间的耦合,但增加内聚比减少耦合更重要,应当把更多的注意力集中到提高模块的内聚程度上来。 【3-4】所谓“模块独立性”是指软件系统中每个模块只涉及软件要求的具体的子功能,而和软件系统中其它的模块的接口是简单的。所谓的“信息隐蔽”是指每个模块的实现细节对于其它模块来说是隐蔽的。也就是说,模块中所包含的信息(包括数据和过程)不允许其它不需要这些信息的模块使用。 如果软件系统做到了信息隐蔽,即定义和实施了对模块的过程细节和局部数据结构的存取限制,那么这些模块相互间的接口就是简单的。这组模块的独立性就比较强。事实上,衡量模块独立性的一个准则就是模块内聚,达到信息隐蔽的模块是信息内聚模块,它是高内聚情形,模块独立性当然很强了。 一个对象的抽象数据类型,就是信息隐蔽的示例。例如,对于栈stack, 可以定义它的操作makenull(置空栈)、push(进栈)、pop(退栈)、gettop(取栈顶)和empty(判栈空)。这些操作所依赖的数据结构是什么样的? 它们是如何实现的? 都被封装在其实现模块中。软件的其它部分可以直接使用这些操作,不必关心它的实现细节。一旦实现栈stack的模块里内部过程或局部数据结构发生改变,只要它相关操作的调用形式不变, 则软件中其它所有使用这个栈stack的部分都可以不修改。 这样的模块结构具有很强的模块独立性。 【3-5】模块的内聚性与该模块在分层模块结构中的位置无关。事实上,一个好的模块化的程序系统,它所有的模块可以都是功能内聚的,即每一个模块就只干了一件事。用结构化设计方法建立起来的模块结构中的每一个模块都符合这个要求。把讨论范围再拓宽点,在纯面向对象范型的软件系统中,整个系统看作是一个类,它的子类可以看作是系统的子系统或高层模块,它们还可以有子类,……,这就形成一个类的层次结构。类的构造可以看成是一个抽象数据类型,实际上是信息内聚的。所以整个系统中从上到下,所有模块(对象类)都是信息内聚的模块。 【3-6】所谓“耦合性”是指模块之间联系的紧密程度的一种度量,而软件的“可移植性”是指将一个软件系统从一个计算机系统或环境移植到另一个计算机系统或环境中运行时所需工作量的大小。可移植性是用一组子特性,包括简明性、模块独立性、通用性、可扩充性、硬件独立性和软件系统独立性等,来衡量的。如果一个软件具有可移植性,它必然耦合性低,这样模块独立性要强。例如,有一个图形处理软件,它应具有二维几何图形处理、三维几何图形处理、图形显示、外设控制、数据库管理、用户界面控制、设计分析等模块。如果这些模块之间都是通过参数表来传递信息,那么它们之间的的耦合就是数据耦合或标记耦合等,都是低耦合。将来如果想要把它们移植到另一个外部环境中,这些模块容易修改(功能内聚),且接口清晰,修改可局部化。反言之,如果这些模块都是功能内聚或信息内聚的模块,模块之间的耦合都是低耦合,也对可移植性有促进。但不能讲具有低耦合性模块结构的软件一定具有可移植性,因为是否具有可移植性还有其它因素的影响。 【3-7】 A. ⑤, B. ⑦, C. ③, D. ④, E. ① 【3-8】 A. ③, B. ②, C. ①, D. ①, E. ②, F. ①, G. ①, H. ② 模块之间的耦合(块间联系)和模块的内聚(块内联系)是评价程序模块结构质量的重要标准。联系的方式、共用信息的作用、共用信息的数量和接口的简单性等因素决定了块间联系的大小。在块内联系中,以功能内聚模块的块内联系最强。 SD方法的总的原则是使每个模块只做一件事,就是说,只执行一个功能。模块之间尽可能传送简单的数据型参数。模块要通过标准调用语句调用其它模块,不要直接引用另一个模块内部的数据。同时模块之间传送的参数应尽量少。此外,SD方法还提出了判定的作用范围和模块的控制范围等概念。SD方法认为,模块的作用范围应该是其控制范围的子集。 【3-9】 A. ②, B. ⑤, C. ③, D. ③, E. ② 结构化设计方法(SD)是一种应用非常广泛的软件设计方法,它以结构化分析方法(SA)得到的数据流图和数据词典为依据,建立软件的模块结构,然后对每一个模块用结构化程序设计(SP)方法设计它的内部逻辑。这几种方法是前后衔接的。用SD方法建立的模块结构用模块间的耦合(块间联系)和模块的内聚(块内联系)来度量,要求一个好的模块结构应满足高内聚,低耦合。在SD方法中表示模块间调用关系的图叫做系统结构图(SC)。 另一种著名的设计方法是以信息隐蔽为原则划分模块,这种方法是Parnas提出来的,叫做Parnas方法。 【3-10】递归过程在求解复杂的大型问题时非常有效。常用的求解问题的方法,一种叫做“分而治之”的策略和“回溯”的策略,都可以用递归方法来解决。所谓“分而治之”的方法即是把大而复杂的问题化为规模稍小的子问题,用同样方法求解。如果分解后的子问题能够直接解决,就直接解出,然后再回推得到原来问题的解。所谓“回溯”方法就是如果一个大的问题在求解过程中从某一步出发有可选的多种解决方案,先选择一种解决方案,试探求解。如果求解失败,撤消原来的选择,再选一种解决方案,试探求解,……。如果用某一方案求解成功,则退回上一步并报告这一步求解成功;如果所有可选方案都试过,都求解失败,则退回上一步并报告求解失败。 软件设计过程中的“自顶向下,逐层分解”的做法与上述求解问题的策略是一致的。如果分解出来的子问题(子功能、子模块)相互独立性比较强,这种分解可以降低模块的复杂性,做到模块化。所以,只要分解出来的子问题与原来问题满足递归的情况,用递归方法建立模块结构也是可行的。 【3-11】软件设计是一个把软件需求变换成软件表示的过程。最初这种表示只是描绘出软件的总的框架,然后进一步细化,在此框架中填入细节,把它加工成在程序细节上非常接近于源程序的软件表示。正因为如此,所以从工程管理的角度来看,软件设计分两步完成。首先做概要设计,将软件需求转化为数据结构和软件的系统结构。然后是详细设计,即过程设计。通过对结构表示进行细化,得到软件的详细的数据结构和算法。 由于概要设计建立起整个系统的体系结构框架,并给出了系统中的全局数据结构和数据库接口,人机接口,与其它硬、软件的接口。此外还从系统全局的角度,考虑处理方式、运行方式、容错方式、以及系统维护等方面的问题,并给出了度量和评价软件质量的方法,所以它奠定了整个系统实现的基础。没有概要设计,直接考虑程序设计,就不能从全局把握软件系统的结构和质量,实现活动处于一种无序状态,程序结构划分不合理,导致系统处于一种不稳定的状态,稍一做改动就会失败。所以,不能没有概要设计。 【3-12】A. ②, B. ①, C. ④, D. ②, E. ① 【3-13】使用PDL语言,可以做到逐步求精:从比较概括和抽象的PDL程序起,逐步写出更详细的更精确的描述。下面举一个例子。   PROCEDURE spellcheck IS 查找错拼的单词 BEGIN split document into single words 把整个文档分离成单词 lood up words in dictionary 在字典中查这些单词 display words which are not in dictionary 显示字典中查不到的单词 create a new dictionary 造一新字典 END spellcheck 这个例子只是搭起一个处理问题的框架。为进一步表明查找拼错的单词的4个步骤如何实现,可以对它每一步进行细化: PROCEDURE spellcheck BEGIN --* split document into single words LOOP get next word add word to word list in sortorder EXIT WHEN all words processed END LOOP --* look up words in dictionary LOOP get word from word list IF word not in dictionary THEN --* display words not in dictionary display word,prompt on user terminal IF user response says word OK THEN add word to good word list ELSE add word to bad word list ENDIF ENDIF EXIT WHEN all words processed END LOOP --* create a new words dictionary dictionary:=merge dictionary and good word list END spellcheck 【3-14】A. ③, B. ②, C. ③, D. ①, E. ⑤