首页 理论教育 软件工程:软件设计原理

软件工程:软件设计原理

时间:2023-11-06 理论教育 版权反馈
【摘要】:在进行软件设计过程中将用到抽象、逐步求精、模块化、软件体系结构、控制层次、信息隐藏等方法,并遵循模块独立性原理。由图4-2可以看出,模块数量在M 1~M 2时,软件的总成本最低,是我们期望的。图4-2模块化和软件成本之间的关系2.抽象抽象源于哲学,它是一种解决问题的方法,即忽略事物的一些细节,只关注少数特性的解决问题的方法,这一方法目前已被应用于软件领域。

软件工程:软件设计原理

软件设计是软件开发的一个很重要的阶段,该阶段的产品为软件设计模型及文档,对前期和后期的工作有很大的影响。软件设计的结果如果不能够实现需求阶段定义的需求,则结束设计工作;软件设计的结果还作为实现、测试和维护的依据,所以我们会对设计方案进行论证,以寻找更适合的设计方案,并对设计方案进行优化,使其更好地满足软件需求和各种约束。

软件设计是一种非确定性的过程,不同的系统设计师对相同的需求可以得到不同的设计方案,也不可能期望得到一个完全符合预期的结果。既然软件设计是一个重要的阶段,该阶段又没有确定性的结果,那么我们就期望这个过程按照一定的原理进行,尽量使设计的结果可预期并且具有更好的质量。

在上述软件设计的过程中,在讲具体的软件设计方法之前,首先需要掌握一些软件设计的常用概念,它们会在今后的软件设计过程中不断出现,所以在本节中做了详细介绍。

在进行软件设计过程中将用到抽象、逐步求精、模块化、软件体系结构、控制层次、信息隐藏等方法,并遵循模块独立性原理。

1.模块化

模块化是指软件被划分成独立命名和可独立访问的被称作模块的构成成分,它们集合到一起满足问题的需求。

设C(x)是定义问题x复杂性的函数,E(x)是定义解决问题x所需工作量(以时间计算)的函数。对于问题p 1和p 2,如果C(p1)>C(p2),那么E(p1)>E(p2);还有一个特性:即如果C(p1+p 2)>C(p1)+C(p2),那么E(p1+p 2)>E(p1)+E(p2);这就引出了“分而治之”的结论,这个理论可运用于软件的开发,意味着软件被划分为小的模块,那么开发小模块的工作量会变小。开发单个软件模块所需的工作量(成本)的确随着模块数量的增加会下降,给定同样的需求,更多的模块意味着每个模块的尺寸更小,然而随着模块数量的增加,集成模块所需的工作量(成本)也在增长。

由图4-2可以看出,模块数量在M 1~M 2时,软件的总成本最低,是我们期望的。M 1、M 2具体的数值和软件项目的规模有关,并且在一定程度上还依赖于项目开发者的经验。

图4-2 模块化和软件成本之间的关系

2.抽象

抽象源于哲学,它是一种解决问题的方法,即忽略事物的一些细节,只关注少数特性的解决问题的方法,这一方法目前已被应用于软件领域。开发人员对要解决的问题进行抽象,随着解决方案的提出,再逐渐考虑更多的细节。

“抽象”的心理学观念使人能够集中于某个一般性级别上的问题,而不去考虑无关的底层细节,这种解决问题的方式也可应用于软件领域。

在软件开发过程中,开发人员把待解决的软件问题划分为若干个子问题,这就相当于在原有的问题划分后的子问题的级别上考虑其解决方案。这就是将抽象的思维方式应用于软件开发领域,但软件过程中的每一个步骤都是软件解决方案抽象级别上的求精。在软件分析阶段,软件的解决方案使用问题领域中熟悉的术语来陈述;当进入设计阶段,抽象级别降低,采用软件开发领域的一些术语和工具表示;当进入源代码生成时,进入抽象的最低层次。

根据软件开发过程中,抽象的对象不同,把抽象过程分为三方面:过程抽象、数据抽象和控制抽象。

(1)过程抽象:是对处理业务的过程进行抽象,最终形成函数或方法。例如,针对查询这个业务过程,随着对查询功能的不断分析与设计,查询过程分为:按书名查询,按作者查询,按出版社查询等;再具体点如按书名查询步骤,首先输入关键字,然后进行查询,最后显示查询结果。

(2)数据抽象:它是对系统处理的对象进行抽象,最终形成数据库中的表、表的字段、类以及类的属性。例如,查询“书”,进一步详细定义其属性:书名、作者、出版社、出版日期、ISBN等;书名,进一步定义其长度为50个字符的字符串,这就是数据抽象的过程。

(3)控制抽象:是程序控制机制内部细节的设计。例如,模块之间的控制信息,模块内部的控制信息。

3.逐步求精

逐步求精是由Niklaus Wirth最初提出的一种自顶向下设计策略,系统是通过过程细节的连续的层次精化开发的,层次结构通过逐步地分解功能的宏观声明直至形成程序设计语言的语句而开发。逐步求精实际是一个详细描述的过程,首先是一种初始的声明,然后随着后续的开发工作提供越来越多的细节。

逐步求精的思想应用于软件开发的整个过程。在需求分析阶段,对于功能的调研从系统的目的和范围入手,不断细化系统的功能,直到将用户要求的功能完全描述,写入需求规格说明书,并保证其完整、一致、没有二义性

在设计阶段,我们先要从系统的体系结构入手,根据需求确定整体的框架结构,再考虑在该框架下实现需求规格说明书中的全部需求,然后可根据需求的分配设计各个模块的接口,再按照模块分配的功能设计实现该模块的算法

在实现阶段也是同样道理,我们可以利用辅助工具生成部分代码框架,在此基础上,再添加更多的代码。

通过上述描述能够看出,在软件的开发过程中,每个阶段都是遵从逐步求精的思想从整体到局部一步一步完成的。

逐步求精和抽象是互补的概念,随着软件的开发过程逐步求精是越来越精化,而抽象是越来越具体的。

4.信息隐藏

信息隐藏的原则就是说模块应该设计成其包含的信息(过程和数据)对不需要这些信息的其他模块是不可访问的,或者是不可随意访问的。有效的模块化是将系统划分为若干个模块,模块与模块之间进行通信完成指定的功能,隐藏就是要求模块与模块之间通信时,只交流必要的信息,这样加强了对模块内部过程细节或模块使用的任何局部数据结构的访问约束。

模块的独立性就是靠信息隐藏实现的,为后期的软件测试和维护提供了极大的方便。一旦在进行测试或者维护时发现问题,那么对模块的变更不会影响或者至少很少影响其他模块,不会将影响扩大并传播。

5.控制层次

控制层次也称为“程序结构”,它代表了程序构件(模块)的组织,并暗示控制的层次结构。一般有四个特征:深度、宽度、扇入和扇出。其中深度和宽度是针对整个控制层次说的;扇入和扇出是针对一个模块而言的。

(1)深度:定义为控制层次的层数,或者说是控制级别的数量。

(2)宽度:定义为控制层次的跨度。

(3)扇入:指明有多少个模块直接控制一个给定的模块。

(4)扇出:指明被一个模块直接控制的其他模块的数量。

首先要了解深度和宽度的概念。深度是指控制层次的层数,或者说是控制级别的数量;宽度是指控制层次的跨度。这两个特征是从系统的整体角度来衡量控制层次设计是否合理的。如图4-3,该系统结构的深度应该是5,宽度是7,我们进行设计时应该使设计结果的软件控制层次呈现顶细,中间鼓,底比中间要收拢的形状,图4-3基本就是这样的形状。

扇入和扇出是从一个模块的角度提出的系统特征。某个模块的扇入是指有多少个模块直接调用该模块。如图4-3,R模块的扇入是4,L模块的扇入是1;某个模块的扇出是指被该模块直接调用的其他模块的数量。如图4-3,H模块的扇出是4,D模块的扇出是2。

图4-3 控制层次的深度、宽度、扇入和扇出

如果扇出过高的话,那么执行模块就会太复杂。在典型的系统中,平均的扇出数是3或4,但这不是必须机械遵循的准则。一般较好的系统形态,高层的模块有较大的扇出,在底层的模块则有较大的扇入。

在系统的控制结构中,某一层的模块中的判定或者条件在系统中产生的结果会影响到其他层的某个处理或数据,这样该处理就是条件依赖于那个判定。因此在控制结构中产生另外两个概念:作用范围和控制范围。

(1)作用范围:一个模块的作用范围是指条件依赖于这个模块的全部模块。即使一个模块全部处理中只有一小部分为这个判定所影响,整个模块也被认为在作用范围中。

(2)控制范围:一个模块的控制范围是指模块本身和它的全部子模块。

作用范围和控制范围相关的设计原则:对于任何判定,作用范围应该是这个判定所在模块的控制范围的一个子集。通常我们通过把判定节点在结构中上移来达到这个原则。换句话说,受该判定影响的所有模块应该都是该判定模块的子模块,最理想的情况是,把作用范围限制在该判定本身所在模块以及与它直接相连的子模块中,但实际上在系统中可能会有如图4-4所示的四种情况:图4-4(a)中作用范围不属于控制范围,这种情况是不好的设计,因为B2中的判定影响了模块A,这种影响是通过模块B、模块Y传递过去的,造成模块B2和模块A耦合性增加。图4-4(b)中作用范围在控制范围以内,但是判定所在模块距离它的作用范围模块较远,造成沿途模块会引入错误的可能性增强。图4-4(c)中控制范围较好地包含了作用范围,判定所在模块距离依赖于该判定的模块远近适当。最理想的设计应为图4-4(d)所示,图中作用范围和控制范围是最理想的关系。

因为模块一定通过某个数据或者参数来影响作用范围内的模块,如果该模块不在控制范围内,则增加了耦合度,不利于发展良好的系统结构,通常这种情况都是通过上移判定点来满足的。(www.xing528.com)

图4-4 作用范围和控制范围

6.模块独立性

系统是由若干个模块组成的,每个模块具有一定的功能,它们相互联系共同完成整个系统的功能,因此模块之间必然有着这样那样的关系或者依赖。模块之间的这种联系越多,就越会增大测试和维护的难度。所以我们在进行软件设计的时候,还要遵循模块独立性原理,希望模块之间的联系越少越好,即模块的独立性越强越好,相互之间的接口越简单越好。那么如何来衡量模块独立性的强弱呢?这就涉及两个概念:耦合度和内聚度,它们从不同的角度来衡量模块独立性的强弱。

(1)耦合度。耦合度是指模块与模块之间联系的强弱程度。它们之间的联系越多,模块的耦合度就越强,独立性就越弱。模块之间的联系体现在相互调用时需要互相了解的程度。如果一个模块需要调用另外一个模块来完成它的功能,那么调用模块需要了解被调用模块的信息越多,它们之间的联系就越多。因此,如果被调用模块发生变化,对调用模块产生影响的可能性就大,造成调用模块也要随之变化。我们在进行软件设计的时候,就希望模块之间的耦合度越低越好,因为这样的话,模块之间的联系就少,便于后续的实现、测试和维护。

在这里根据耦合度的强弱,将其分为7个等级:

非直接耦合:两个模块是不同模块的从属模块,相互之间无直接关联,因而没有耦合发生,称为非直接耦合。图4-5中A、B之间属于非直接耦合。非直接耦合是耦合度最低的,但是一个软件产品的模块之间不可能都是非直接耦合,否则就无法构成一个整体。

图4-5 各种耦合关系的一个实例

此外的6种耦合都属于模块之间有一定的关联和依赖。

①数据耦合:模块与模块之间需要通过常规的参数表访问,数据通过该列表传递,传递的数据是简单类型的,这种耦合称为数据耦合。图4-5中A、E之间属于数据耦合,在显示学生的详细信息时需要提供该生的学号。

②标记耦合:当模块与模块之间传递的参数是数据结构的一部分时,这种耦合是标记耦合。它是数据耦合的变体,两者都属于低级别耦合。图4-5中A、D之间属于标记耦合,对学生进行排序时需要提供待排序的学生序列。

③控制耦合:调用模块与被调用模块之间传递的信息对于被调用模块的执行路径有决定作用,此种耦合属于控制耦合。图4-5中B、F之间为控制耦合,因为学生选课时提交的系别和年级对学生可选择的课程的类别和详细信息都是不同的,也就是B传递给F的信息,系别和年级对F调用I、J和K中的哪一个模块起到决定作用。控制耦合是中级别的耦合度。

④外部耦合:当模块连接到软件外部环境上时会发生的偶合关系,具有相对较高的耦合度。

⑤公共耦合:多个模块都访问一块全局数据区中的数据项(一个磁盘文件、一个全局可访问的内存区),这种耦合程度就是公共耦合。E、G和H三个模块是公共耦合,它们共同访问同一个数据库文件

⑥内容耦合:一个模块访问另一个模块边界中的数据或控制,这种耦合是内容耦合,也是最强的耦合。

如果发生下列情形,两个模块之间就发生了内容耦合:

①一个模块直接访问另一个模块的内部数据;

②一个模块不通过正常入口转到另一模块内部;

③两个模块有一部分程序代码重叠。

可以使用适当的消除方法来消除或者减弱模块之间的耦合度,具体做法:

①将公共的数据区进行分割,降低多个模块因共享该数据区而产生的耦合;

②将模块之间的连接方式尽量标准化:只用调用,不直接引用;只传递必要信息,无冗余;

③引入缓冲区;

④减少公共区,使其局部化;

⑤输入/输出尽量在少量模块间进行,不要分散在全系统;

⑥参数确定的越晚,就越容易修改,越灵活。

(2)内聚度。内聚度是模块所执行任务的整体统一性的度量,是指模块内部组成部分之间联系的紧密程度,它与耦合度是相对应的。在一个理想的系统中,每一个模块应该是执行一个单一明确的任务,但是实际中一个模块可能完成一些结合在一起的、有一定相关性的功能,或者几个模块一起完成一个或一组功能。一般模块功能的相关性强,我们就认为将其转换为代码时,代码也是高度相关的,换句话说,不在同一个模块中的代码其功能相关性是很小的,所以要尽力减少模块之间连接数和模块之间的耦合度,以保证模块的独立性。

模块的内聚度按照其程度也分为7个级别,按顺序内聚性依次变强。

①偶然内聚。设计者随意决定将没有关系的几个任务组合在一个模块中,该模块的内聚程度就是偶然内聚,一般来说这样的模块是没有任何意义的。

例如,为了节省空间,将多个模块中重复出现的语句提取出来,组成一个新的模块,图4-6(a)就是偶然内聚的例子,模块M1、M2和M3中都出现了一部分同样的语句,为了节省空间,将这部分语句单独构成一个新的模块T。这样的模块存在的问题是模块不易取名、含义不易理解、难以测试、重用性差且更不易修改。由于这样的模块内部的语句相关性很低,又包含多个任务,往往修改的可能性很大,因此会为后续工作造成很坏的影响,应尽量避免偶然性内聚的发生。

矫正方法:因为它执行多个任务,可以考虑将模块分成更小的模块,每个小模块执行一个操作,或者是将模块中的语句放回它们各自出现的地方。

②逻辑内聚。把逻辑上相似的功能结合到一个模块中,该模块的内聚程度就是逻辑内聚。一般来说这种逻辑相似体现在:第一,使用统一动词但针对不同的对象,有相同的代码段;第二,起始于某多路开关,以后转向不同的代码段,但各代码段间联系很少。如图4-6(b),这个小例子的模块实现的功能是为四个年级的学生显示各自的课程体系结构,但是各自的课程体系差别很大,它们只是逻辑上类似(都是显示课程体系)。这种聚合现象带来一些设计问题:增加了开关量、不易理解、不易维护、效率低。因此也要尽量避免使用这种内聚。

可以将它内部不相干的功能分离成更多的小模块,实现各自的功能,将功能逻辑相同的代码段提出来,单独做一个模块,然后在被分离出去的模块中调用。

③时间性聚合。在某一时间同时执行的任务放在同一模块中,该模块的内聚度就是时间性内聚。如,初始化模块,集中了初始化功能的模块,图4-6(c)就是时间内聚的一个小例子。

④过程性聚合。模块中各个处理任务相关,并且是按照特定次序执行,这样的模块的聚合度就是过程性聚合。一般来说,这种聚合情况往往发生在程序流程图中,以及相邻的处理功能聚合成的模块中。例如,接收用户的输入信息并对其进行格式化编辑,图4-6(d)就是这种聚合的一个例子。

⑤通信性内聚。模块中各个功能需要用到同样的数据,而将其放于一个模块中,则称之为通信性内聚模块。如图4-6(e),模块中包含对选课信息的修改和删除,它们由于是对相同的数据对象进行处理,因此放在一个模块中,这种聚合要比过程性内聚强,但是由于模块中各部分使用相同的数据对象,会降低模块的执行效率。

⑥信息性内聚。模块中各功能任务利用相同的输入或产生相同的输出。由于它可能包含几个功能或只是某个功能的一部分,所以内聚性不是最高的。例如图4-6(f)中的模块结构,该模块中的任务用到的是相同的输入,就是学生的选课信息,然后对其进行不同的操作,打印出了信息中的不同部分。

⑦功能性内聚。一个模块中各个部分都是完成某一具体功能必不可少的组成部分,或者说该模块中所有部分都是为了完成一项具体功能而协同工作,紧密联系,不可分割,则称该模块为功能内聚模块,它是最强的一种内聚,这样内聚形式的模块易于实现,易于测试、修改和维护。

模块独立性原理是软件设计的一条基本原理。在进行设计的时候,我们希望模块本身是高内聚的,模块之间是低耦合的,而两者之中,从广泛的实践来说聚合度显得更加重要,因为,耦合以及传递的特定数据项的数量可以很好地表示模块的内聚程度,执行一个单一独立的任务的模块,往往是高内聚的,所有的内部代码使用同样的数据项。低内聚的模块往往有高耦合以及相互之间松散关系的任务,通常是对不同的数据对象进行的操作,需要上层模块传递相互关系不大的数据项。

图4-6 各种逻辑内聚示例

免责声明:以上内容源自网络,版权归原作者所有,如有侵犯您的原创版权请告知,我们将尽快删除相关内容。

我要反馈