软件工程之过程与设计

    从实践中来,方觉软件工程的重要性。上篇讲的代码设计不够详尽,本篇从软件工程的角度看下代码设计。

    软件工程是一门涵盖量很广的学科,本文只记录软件工程中重要的两个部分:软件过程与系统设计。内容大多摘至软件工程课本,有想深入了解的朋友,可以直接查阅软件工程相关书籍。

一、软件开发原则:

        1)抽象:以归纳的方式对现实问题进行描述,或者抽离出包括又不局限与现实问题结果的一种方式。

        2)分析设计方法和概念:大多工程师, 讨论他们做哪类工程,就使用哪一个标准概念帮助交流,并决策文档化。分析与设计方法提供给我们不止一种交流媒介,他们允许我们建模,并检查完全性和一致性,而且我们可更容易地从以前的应用需求和设计组件中获取有效内容,这样能提高生产效率和质量。

       用户接口原型(相对与当前开发,即原型图的概念)。原型的意思是建立一个系统的一个小版本,通常只具有有限的功能。其作用: ①帮助有户或顾客确定一个系统的关键需求 ②论证一个设计或方法的可行性

        3)软件体系结构:设计一个系统就是要确定一组满足特定需求的组件,以及各组件间的接口关系。具体的设计方法由设计者自身的喜好,或者是系统所要求的结构和数据所决定。然而每一种设计方法都要涉及某种分解方法:从系统关键元素的顶层描绘开始,然后建立较低层次,看系统的特征和功能将怎样相互适应。

        4)软件过程:应用类型和组织文化的巨大变化使得不可能固定过程本身,因此似乎软件过程不象抽象的模块化那样是软件工程的基础。代替地,建议不同类型的软件需要不同的过程。企业范围应用程序则需要大量的控制,而个体或部门应用可采用快速开发。

        5)复用:在软件开发和维护中,我们常通过应用以前开发中的某些共性来利用跨越应用程序的共同特征。Priteo-Diaz(1991)介绍可应用组件的概念为一种商业资产。公司和组织投次于可应用项目,然后在这些项目一次又一次在后来的项目里使用时获得可以计量的利润。然而,建立一个长期,有效力的应用程序是困难的,因为有几个障碍:

        ①有时建立一个小组件比可应用组件仓储库中搜索更快

        ②使组件足够通用以使未来别的开发者很容易地应用它,这需花额外的时间

        ③难以文档化,不能保证质量和测试

        ④如果一个应用组件失败或者要更新,谁来负责是不清楚的

        ⑤理解和应用别人写的组件是费钱费时的

        ⑥通常在共通性和特殊性之间存在冲突

         6)度量:改良是软件工程研究的推动力:改进过程、资源和方法,以便我们生产和维护更好的产品。软件度量已成为好的软件工程实践的一个关键方面,通过度量我们可以确定软件可以实现到哪里及我们能做什么。另外,这种数量的方法使我们可比较不同项目的进展。在较低层次的抽象中,度量也有助于使过程和产品的特定特征更具可见性。

         7)工具和集成环境

二、软件过程:

软件开发模型(摘至百科):

       1边做边改型遗憾的是,许多产品都是使用"边做边改"模型来开发的。在这种模型中,既没有规格说明,也没有经过设计,软件随着客户的需要一次又一次地不断被修改。

        在这个模型中,开发人员拿到项目立即根据需求编写程序,调试通过后生成软件的第一个版本。在提供给用户使用后,如果程序出现错误,或者用户提出新的要求,开发人员重新修改代码,直到用户满意为止。

        这是一种类似作坊的开发方式,对编写几百行的小程序来说还不错,但这种方法对任何规模的开发来说都是不能令人满意的,其主要问题在于:

        1缺少规划和设计环节,软件的结构随着不断的修改越来越糟,导致无法继续修改;

        2忽略需求环节,给软件开发带来很大的风险;

        3没有考虑测试和程序的可维护性,也没有任何文档,软件的维护十分困难。

       2、瀑布模型:1970Winston Royce提出了著名的"瀑布模型",直到80年代早期,它一直是唯一被广泛采用的软件开发模型。瀑布模型将软件生命周期划分为制定计划、需求分析软件设计、程序编写、软件测试和运行维护等六个基本活动,并且规定了它们自上而下、相互衔接的固定次序,如同瀑布流水,逐级下落。

        在瀑布模型中,软件开发的各项活动严格按照线性方式进行,当前活动接受上一项活动的工作结果,实施完成所需的工作内容。当前活动的工作结果需要进行验证,如果验证通过,则该结果作为下一项活动的输入,继续进行下一项活动,否则返回修改。

        瀑布模型强调文档的作用,并要求每个阶段都要仔细验证。但是,这种模型的线性过程太理想化,已不再适合现代的软件开发模式,几乎被业界抛弃,其主要问题在于:

        1各个阶段的划分完全固定,阶段之间产生大量的文档,极大地增加了工作量;

        2由于开发模型是线性的,用户只有等到整个过程的末期才能见到开发成果,从而增加了开发的风险;

        3早期的错误可能要等到开发后期的测试阶段才能发现,进而带来严重的后果。

        我们应该认识到,"线性"是人们最容易掌握并能熟练应用的思想方法。当人们碰到一个复杂的"非线性"问题时,总是千方百计地将其分解或转化为一系列简单的线性问题,然后逐个解决。一个软件系统的整体可能是复杂的,而单个子程序总是简单的,可以用线性的方式来实现,否则干活就太累了。线性是一种简洁,简洁就是美。当我们领会了线性的精神,就不要再呆板地套用线性模型的外表,而应该用活它。例如增量模型实质就是分段的线性模型,螺旋模型则是接连的弯曲了的线性模型,在其它模型中也能够找到线性模型的影子。

       3、快速原型模型:快速原型模型的第一步是建造一个快速原型,实现客户或未来的用户与系统的交互,用户或客户对原型进行评价,进一步细化待开发软件的需求。

        通过逐步调整原型使其满足客户的要求,开发人员可以确定客户的真正需求是什么;第二步则在第一步的基础上开发客户满意的软件产品。

        显然,快速原型方法可以克服瀑布模型的缺点,减少由于软件需求不明确带来的开发风险,具有显著的效果。

        快速原型的关键在于尽可能快速地建造出软件原型,一旦确定了客户的真正需求,所建造的原型将被丢弃。因此,原型系统的内部结构并不重要,重要的是必须迅速建立原型,随之迅速修改原型,以反映客户的需求。

       4、增量模型:又称演化模型。与建造大厦相同,软件也是一步一步建造起来的。在增量模型中,软件被作为一系列的增量构件来设计、实现、集成和测试,每一个构件是由多种相互作用的模块所形成的提供特定功能的代码片段构成。

        增量模型在各个阶段并不交付一个可运行的完整产品,而是交付满足客户需求的一个子集的可运行产品。整个产品被分解成若干个构件,开发人员逐个构件地交付产品,这样做的好处是软件开发可以较好地适应变化,客户可以不断地看到所开发的软件,从而降低开发风险。但是,增量模型也存在以下缺陷:

        1由于各个构件是逐渐并入已有的软件体系结构中的,所以加入构件必须不破坏已构造好的系统部分,这需要软件具备开放式的体系结构。

        2在开发过程中,需求的变化是不可避免的。增量模型的灵活性可以使其适应这种变化的能力大大优于瀑布模型快速原型模型,但也很容易退化为边做边改模型,从而使软件过程的控制失去整体性。

        在使用增量模型时,第一个增量往往是实现基本需求的核心产品。核心产品交付用户使用后,经过评价形成下一个增量的开发计划,它包括对核心产品的修改和一些新功能的发布。这个过程在每个增量发布后不断重复,直到产生最终的完善产品。

        例如,使用增量模型开发字处理软件。可以考虑,第一个增量发布基本的文件管理、编辑和文档生成功能,第二个增量发布更加完善的编辑和文档生成功能,第三个增量实现拼写和文法检查功能,第四个增量完成高级的页面布局功能。

       5、螺旋模型:1988年,Barry Boehm正式发表了软件系统开发的"螺旋模型",它将瀑布模型和快速原型模型结合起来,强调了其他模型所忽视的风险分析,特别适合于大型复杂的系统。

        螺旋模型沿着螺线进行若干次迭代:

        1制定计划:确定软件目标,选定实施方案,弄清项目开发的限制条件;

        2风险分析:分析评估所选方案,考虑如何识别和消除风险;

        3实施工程:实施软件开发和验证;

        4客户评估:评价开发工作,提出修正建议,制定下一步计划。

        螺旋模型由风险驱动,强调可选方案和约束条件从而支持软件的重用,有助于将软件质量作为特殊目标融入产品开发之中。但是,螺旋模型也有一定的限制条件,具体如下:

        1螺旋模型强调风险分析,但要求许多客户接受和相信这种分析,并做出相关反应是不容易的,因此,这种模型往往适应于内部的大规模软件开发。

        2如果执行风险分析将大大影响项目的利润,那么进行风险分析毫无意义,因此,螺旋模型只适合于大规模软件项目。

        3软件开发人员应该擅长寻找可能的风险,准确地分析风险,否则将会带来更大的风险

        一个阶段首先是确定该阶段的目标,完成这些目标的选择方案及其约束条件,然后从风险角度分析方案的开发策略,努力排除各种潜在的风险,有时需要通过建造原型来完成。如果某些风险不能排除,该方案立即终止,否则启动下一个开发步骤。最后,评价该阶段的结果,并设计下一个阶段。

       6、演化模型:演化模型是一种全局的软件(或产品)生存周期模型。属于迭代开发方法。

        该模型可以表示为:第一次迭代(需求->设计->实现->测试->集成)->反馈->第二次迭代(需求->设计->实现->测试->集成)->反馈->……

        即根据用户的基本需求,通过快速分析构造出该软件的一个初始可运行版本,这个初始的软件通常称之为原型,然后根据用户在使用原型的过程中提出的意见和建议对原型进行改进,获得原型的新版本。重复这一过程,最终可得到令用户满意的软件产品。采用演化模型的开发过程,实际上就是从初始的原型逐步演化成最终软件产品的过程。演化模型特别适用于对软件需求缺乏准确认识的情况。

       7、喷泉模型:(也称面向对象的生存期模型, OO模型)喷泉模型与传统的结构化生存期比较,具有更多的增量和迭代性质,生存期的各个阶段可以相互重叠和多次反复,而且在项目的整个生存期中还可以嵌入子生存期。就像水喷上去又可以落下来,可以落在中间,也可以落在最底部。

       8、智能模型:智能模型拥有一组工具(如数据查询、报表生成、数据处理、屏幕定义、代码生成、高层图形功能及电子表格等),每个工具都能使开发人员在高层次上定义软件的某些特性,并把开发人员定义的这些软件自动地生成为源代码

        这种方法需要四代语言(4GL)的支持。4GL不同于三代语言,其主要特征是用户界面极端友好,即使没有受过训练的非专业程序员,也能用它编写程序;它是一种声明式、交互式和非过程性编程语言。4GL还具有高效的程序代码、智能缺省假设、完备的数据库和应用程序生成器。市场上流行的4GL(如Foxpro等)都不同程度地具有上述特征。但4GL主要限于事务信息系统的中、小型应用程序的开发。

       8、混合模型:过程开发模型又叫混合模型(hybrid model),或元模型(meta-model,把几种不同模型组合成一种混合模型,它允许一个项目能沿着最有效的路径发展,这就是过程开发模型(或混合模型)。实际上,一些软件开发单位都是使用几种不同的开发方法组成他们自己的混合模型。

       10RAD模型:快速应用开发(RAD)模型是一个增量型的软件开发过程模型。强调极短的开发周期。RAD模型是瀑布模型的一个高速变种,通过大量使用可复用构件,采用基于构件的建造方法赢得快速开发。如果需求理解得好且约束了项目的范围,随后是数据建模、过程建模、应用生成、测试及反复。

        RAD模型各个活动期所要完成的任务如下:

        1)业务建模:以什么信息驱动业务过程运作?要生成什么信息?谁生成它?信息流的去向是哪里由谁处理?可以辅之以数据流图。

        2)数据建模:为支持业务过程的数据流找数据对象集合,定义数据对象属性,与其他数据对象关系构成数据模型,可辅之以E-R图。

        3)过程建模:使数据对象在信息流中完成各业务功能。创建过程以描述数据对象的增加、修改、删除、查找,即细化数据流图中的处理框。

        4)应用程序生成:利用第四代语言(4GL)写出处理程序,重用已有构件或创建新的可重用构件,利用环境提供的工具自动生成并构造出整个应用系统。

        5)测试与交付,由于大量重用,一般只做系统测试,但新创建的构件还是要测试的。

        每个软件开发组织应该选择适合于该组织的软件开发模型,并且应该随着当前正在开发的特定产品特性而变化,以减小所选模型的缺点,充分利用其优点,下表列出了几种常见模型的优缺点。

        1)瀑布模型 文档驱动系统可能不满足客户的需求

        2)快速原型模型 关注满足客户需求可能导致系统设计差、效率低,难于维护

        3)增量模型 开发早期反馈及时,易于维护需要开放式体系结构,可能会导致效率低下

        4)螺旋模型 风险驱动风险分析人员需要有经验且经过充分训练

三、系统设计

       1、什么是设计

        设计是将一个实际问题转换成相应的解决办法的主动过程。所谓设计也可以是对一种解决办法的描述。

       2、总体设计和技术设计

        设计者必须同时满足用户和系统建立者的要求才能将实际需求转换为可以运行的系统。用户要清楚系统要做什么,而系统建立者要清楚系统是怎样运作的,基于这样的原因,设计实际上是一个两部分的迭代过程。总体设计(或系统设计):告诉用户系统具体将要做什么。一旦用户同意了这个总体设计,我们会将这个总体设计转换为更加详细的文档。技术设计:让系统建设者了解要解决用户的问题所需要的硬件和软件。确定设计的过程是一个迭代的过程,这是因为设计者对于需求的理解、提出解决办法、测试办法的可行性和为程序员提供设计文档始终处于一个反复的过程中。

        一个优秀的总体设计应该包含以下特征:

        1)它是由用户语言书写的

        2)不包括用户不熟悉的专业词汇

        3)它描述系统功能

        4)独立于实现过程

        5)与需求分析文档相关联

        与总体设计相比,技术设计主要描述系统的硬件配置、软件需要、人机界面、输入和输出,和网络体系结构等。也就是说,技术设计是系统说明的一个技术层面上的描述,它至少应该包括以下方面:

        1)主要的硬件组成的描述和它们的功能

        2)软件组成的层次和功能

        3)数据结构和数据流

        3、分解和模块化

        设计一个系统就是要确定一组满足特定需求的组件,以及各组件间的接口关系。具体的 设计方法由设计者自身的喜好、或是系统所要求的结构或数据所决定。然而每一种设计方法都要涉及某种分解方法:从系统关键元素的顶层描绘开始,然后建立较低层次,看系统的特征和功能将怎样相互适应。

        Wasserman(1995)提出的确定设计的五种方法:

        1)模块化分解:在把功能分配到各个组成部分(component)的基础上进行构造。设计者开始于功能的顶层描述上,然后在较低的层次上说明各组成部分是如何组织的以及各部分是如何关联的

        2)面向数据的分解:这种设计是基于外部数据结构的。顶层描述总体的数据结构,而底层描述所包含的数据元素及它们之间的关系这些细节。

        3)面向事件的分解:这种设计是基于系统必须处理的事件和有关事件是怎样改变系统状态的信息。顶层描述是将各种各样的状态分类,而较低层次是描述状态转换是怎样发生的。

        4)有外向内的设计:这种黑盒方法是基于用户对系统的输入。也就是说,顶层描述列出所有可能的用户输入,而较低层次的描述是对于用户的这些输入(及可能产生的输出),系统要做什么的描述。

        5)面向对象的设计:这种设计将对象类和它们之间的相互关系关联起来。顶层是对每一个对象类型的描述,而在较低层次上是对象的属性、行为的描述和对象是怎样和另一个对象相关联的解释。

       4、体系结构风格和策略

        Shaw 和 Garlan(1996)建议一个软件的设计从软件的体系结构开始。他们区分了三个设计层次:体系结构、代码设计、和可执行的设计。

         1)体系结构将在需求规格说明中规定的系统性能和将要实现它们的系统组件相联系起来。组件通常即模块,而体系结构是描述它们之间相互联系的,此外,体系结构定义了从子系统中建造系统的操作符。

         2)代码设计涉及到算法和数据结构,其组件是设计语言基本要素,例如:数字、字符、指针和控制线程。依次地,原始的操作符还包括语言的算术和数据操作基本元素,和组成机制(例如:数组、文件和过程)。

         3)可执行的设计是在一个更低的层次上说明代码设计的。它讨论的是内存分配、数据格式、位模式等问题。

        自上而下的方法是很有用的,通常先是体系结构的设计,然后是代码设计,最后是可执行的设计。然而,一些研究表明,开发者们总是在当他们对解决办法有更深入的了解时,在几个分解层面上来回反复。比如:一组开发人员开始时决定系统采用表驱动的形式,但是在建立了部分功能的原型后发现,这种表驱动不能满足实时相应的需求,于是他们决定重新设计系统为数组形式,来代替原来的表的形式。类似地,设计者在开发系统的某个方面的时候,他们可能通过和测试者或是程序员的交流,而改变一开始的一些设计来加强系统的维护性等。这样,随着设计者理解的加深和不断的创造性,注定他们要在体系结构、代码设计和可执行的设计者三个层面上来回的反复。

        软件的体系结构的风格,在结合它所有组件的时候,涉及它的组件、连接器和约束条件。Shaw和 Garlan(1996)曾经指出通常有7 种常用风格:管道和过滤器,对象,隐式请求,层次化,知识库,解释程序和过程控制。理解他们的特征有助于我们确定对于一个给定的系统那一种风格才是最适合的。

        1)管道和过滤器

        在一个管道--过滤器系统中,组件是由管道和过滤器组成的。所谓管道就是传送数据流的,包括输入和输出。所谓过滤器就是完成数据从输入到输出的转换的。这种类型的系统中,过滤器是独立的,每一个过滤器不知道系统其他的过滤器的功能。此外,系统输出的正确性不依赖于过滤器的顺序。如果你使用这样一个管道--过滤器系统,那么无论你什么时候编译一个程序,过滤器都是以一个线性的顺序工作:词法分析、句法分析、语义分析和代码生成。

        管道--过滤器系统所具有的几个重要的特征是:

        ①设计者可以把整个系统对输入输出的影响看成是过滤器的组成部分。

        ②既然任意两个过滤器可以连接在一起,那么它们可以很容易地应用于其它系统。

        ③系统发展演变很容易,因为新的过滤器可以很容易地加进来,而旧的过滤器也可以很容易地删减下去。

        ④由于过滤器的独立性,设计者可以模拟系统行为和分析系统特性,比如吞吐量。

        ⑤允许过滤器的并行执行。

        然而,管道和过滤器也有一些缺点,首先,他们鼓励批量处理,且不适合处理交互的应用程序。其次,当两个数据流是相关的时候,系统必须维持它们之间的通讯。第三,过滤器的独立性意味着一些过滤器必须要复制一些由其他过滤器完成的准备功能,这样会影响性能,并使代码十分复杂。

        2)面向对象的设计

        需求可以通过对象和他们的抽象类型组织起来。这种设计也可以围绕抽象的数据类型来建立它的组件。也就是说,每一个组件是一种抽象数据类型的实例。因此,一个基于对象的设计必须具备两个重要的特征:一个对象必须保持数据表示的完整性,还有就是数据表示对于其他对象是隐藏的。

        与管道--过滤器系统不同的是,一个对象必须能识别其他对象以便于它们之间的通讯,而过滤器是完全独立的。对象间的这种依赖意味着一个对象的改变将影响到所有和它有联系的其他组件的改变。

        3)隐式请求

        隐式请求的设计模型是事件驱动的。基于广播的概念。它不同于直接调用一个过程,而是由一个组件宣布一个或多个事件要发生了。然后,其他组件将一个过程与这些事件联系起来(称这样的过程为注册过程),再由系统调用这些注册过程。在这种类型的系统中,数据交换是通过储存库中的共享数据完成的。这种类型的设计也经常用于包交换网络中或是基于施动者的系统中,或用于数据库中以保持一致性,还用于用户界面中以便分开数据和管理数据的应用程序。

        下面举例说明事件驱动:假如在一个调试程序中,发现用户漏掉了一个变量,这时系统宣布“漏掉变量”这个事件,同时调用文本编辑器(这个文本编辑器已经注册到这个事件中),让用户在漏掉的那个变量的哪一行进行编辑。

        在这种风格的设计中,当一个组件或是系统宣布有一个事件发生时,它并不知道哪一个组件将会受到这个事件的影响。基于这样的原因,隐式请求的系统中通常还有一些显式请求。

        这种风格的设计对于从其他系统中重用设计组件是特别有用的。因为,任何组件都可以注册到这个事件中。一个重用的组件也可以被加载到系统中,进行注册,且独立于其他组件。类似地,当系统发展或是升级的时候,老的组件可以很容易地被删除,新的组件也可以很容易地加进来。

        4)层次化

        层次是分级的,每一层都为它外面的一层提供服务,而且对于它里面的一层来说是一个客户。在一些系统中,每一层都有权进入某些或其他所有层;而在另一些系统中,一个指定的层次只有进入相邻层的权利。这个设计还要包括关于各层间是如何通讯的协议。

        在设计中,用户可以访问系统的不同层次,这完全依赖于需求说明中的要求。比如:如果用户不需要知道有关加密策略的问题,那么他只需访问最外层,提供用户名和密码即可。

        这种层次化的体系结构最大的好处就是抽象概念。也就是说,每一个层次都被认为是一个可扩展的抽象级,设计者可以用这些层次将一个问题分解成一系列更抽象的步骤。此外,由于各层次间通讯的限制,当需求改变的时候,很容易增加或修改一个层次。通常,这样的改变只影响到相邻的两个层次。同样,重用一个层次也是非常简单的,只需要在相邻层此间做一些改变。

        然而,另一方面,像这样层次化地建立一个系统并不是很容易。

        5)知识库

        在知识库中有两种类型的组件:中央数据存储型和集合型组件,通过对它们的操作实现信息的存储、检索和更新。设计这类知识库的难点在于怎样实现两种类型的组件的相互作用。在传统数据库中,处理方法是以输入流的形式,触发进程执行。而在黑板法中,是中央存控制进程的触发。

        很多其他的系统也是依据这种数据库组织的:可重用构件库,大型数据库,搜索引擎等。这种体系结构的好处就是开放性;销售商很容易获得数据表示,可以开发很多工具来访问知识库。但是这个特点也造成了它的一个不利就是,共享数据必须以所有知识源都接受的形式存在,即使知识源本身是极端不同的。

        6)解释程序(翻译机)

        一个解释程序就是将伪码转换成实际能执行的代码。它不仅可以用于将程序语言转换成机器代码,还可以用于将任意一种形式编码转换成更明白有用的形式。一个解释程序由四部分组成:

        ①一个用于存放要被解释的伪码的存储器

        ②一个解释引擎,用于转化伪码和模拟它所代表的程序的

        ③解释引擎目前的状态

        ④被模拟的程序目前的状态

        虽然这种设计风格只适用于特殊的一类问题,但是我们还是要介绍它,因为它与其它的设计方法有显著的不同。

        7)过程控制

         Shaw和Garlan(1996)指出过程控制系统与基于功能或基于对象的设计非常不同,它是以在设计中所出现的组件的种类,及它们之间的关系为特征的。过程控制系统的目标就是将过程输出的指定特性维持在某个指定值左右。

         当设计一个过程控制系统时,有很多问题需要讨论:要监控哪个变量,要使用什么传感器,要怎样标定它们,以及要怎样处理检测和控制的时钟。此外,Shaw 和 Garlan 推荐此体系结构将控制循环分成三个部分:

        ①计算元素,从控制策略中将过程分开。这个过程的定义应该包括改变过程变量的机制。而控制算法应该解释出如何决定什么时候和怎样做这些改变。

        ②数据元素,包括过程变量(输入、控制和操纵),设定点,和所用的传感器。

        ③控制循环策略:开环或闭环,反馈或前馈。

         这种体系结构风格的一大优点就是它将功能和对外部干扰的反映割裂开。

        8)其它设计风格

         还有一些其他体系风格的设计方案。分布式系统体系结构处理的是一组系统如何进行相互作用的情况。他们通常是根据它们所配置的拓扑结构进行描述的。

         一个流行的分布式系统体系结构就是客户机/服务器形式,由客户机系统提出一个服务的请求,然后由服务器系统响应这个请求。在这种体系结构中,服务器端并不知道有多少客户机将会对它提出请求,也不知道客户机的标识。但是客户机知道服务器的位置,它通过一个过程调用来向服务器发送信息。这种客户机/服务器系统的优点是用户仅在他们需要信息时得到信息。此外,这种设计还详述了表示细节,使得不同的客户机可以以不同的方式察看数据。但是,缺点是这种系统通常需要更复杂的安全保证,系统管理和应用程序的开发,所以他们需要更多的资源去实现和支持。Sidebar5.1描述的是世界杯客户机/服务器系统的实现。

         还有一种特殊领域体系结构,它是利用一些特殊的应用领域,比如航空电子学和自动化制造。这些体系结构利用了应用领域所提供的共同特征,使得一些基本的假定和关系不用再被表示。其长远目标是创建可以被直接执行的设计,或者至少也可以利用那些可重用的组件。

         还有一些体系结构是结合了几种不同风格设计的一些方面,来用于特定的子系统。比如说在管道—过滤器系统中,第一层的分解是按管道—过滤器的形式组织的,而在每一个过滤器的设计可能又采用了不同的体系结构方案。

         对于每种情况而言,没有一种设计风格或方法可以说是最佳的。事实上,当新方法和技术出现的时候,我们必须将它们和现有的方法进行比较。

         5、关于设计的几个问题

        1) 模块化和抽象层次

        很早的时候我们就注意到,模块性是一个好的设计所应具备的特征。在模块化设计中,每一个组件都被清楚地定义输入和输出,及确定的用途。也就是说,独立地测试一个组件是否完成所要求的功能是很容易的。因此,在设计软件时要尽可能地使用模块化的思想。

        抽象层次,是能够帮助我们更好的理解问题,它在较抽象的高层次上隐藏了功能的细节,而在较低的抽象层次上是问题解决办法的详细描述。这样的抽象层次可以改善设计。

        2)共同设计

        大部分的项目,是由一组人员共同工作进行设计的,通常不同的人被分配整个设计中的不同的部分。但是一些问题是必须注意的,包括谁最适合系统设计的某一部分,怎样写文档才能使得每一个组员都能清楚其他人的设计,怎样协调设计组件使他们像一个统一的整体。设计人员们必须清楚的是哪些原因可能导致设计失败(Sidebar5.3),并用集体的力量克服它。共同设计时,一个主要的问题就是设计人员个人经验、理解力和喜好的差别。另一个就是,人们有时候在团队中的行为有别于他们个人的行为方式。

         例子:Guindon,Krasner和Curtis(1987)研究了 19个项目设计者的习惯,得出了引起设计过程失败的主要原因。他们发现主要有三类:缺乏知识,认识的局限,和两者的结合。

        ①缺乏专业化的设计策略。

        ②缺乏一个有关设计过程中导致不佳的资源分配的各种设计行为的元模式。

        ③导致多种解决办法中的不佳选择的不好的优化法。

        ④在定义一种解决方案时要考虑到所有确定的或是猜想的约束条件的困难。

        ⑤用测试用例或是步骤进行智力模拟的困难。

        ⑥在继续追踪和返回已经延迟的解决办法的自问题上的困难。

        ⑦扩展或合并从单个的子问题中得出的解决办法和整个解决方案的困难。

        3)设计用户界面

        4)文化问题

        5)用户喜好

        6)决定用户界面特征的方针

        7)并发性

        8)设计模式和重用

        通常,我们现在设计和建立的系统和我们以前建立的那些系统在很多方面有相似之处。或者我们可以设计和建立一系列的有相似功能的应用程序,他们可以在不同的环境中运行。如果我们利用系统的这些共同特征,我们就不用每次都从头开发了。

        要识别出这些共同点的一个通用的方法就是查看设计模式。这样当我们进行下一个类似的系统的开发的时候,就可以复用这些模式,还有代码,测试和相关的文档。

        一个设计模式可以命名,抽象,识别出一个通常的设计结构的关键方面,这有助于建立一个可复用的设计。设计模式可以识别出包含的类和实例,及他们的作用,合作关系和责任的分布。

         6、好的设计所具备的特征

        当我们设计系统时,我们要保证嵌入一些优良的特性。高质量的设计应该具备哪些导致优良产品的特征,比如:易于理解、易于实现、易于测试、易于修改和从需求规格的正确转换。可修改性是特别重要的,因为需求的修改有时候要导致整个设计的修改。

        1)组件独立性

        在整个设计中,抽象和信息隐藏允许我们检验与其它组件相关联的组件。Sidebar5.4描述的就是在我们的设计中那些组件控制的问题需要被考虑。这样,不仅容易理解一个组件是怎样工作的,更容易修改一个独立的组件。同样地,当发现系统错误的时候,独立的组件可以帮助我们定位原因。我们使用耦合和内聚这两个概念来识别和测定一个组件独立的程度。

        ①耦合:如果我们说两个组件是高耦合的,那么就是说他们之间有很强的依赖性。而松散耦合的组件间虽然有联系,但是程度比较弱。而无耦合组件间根本没有相互联系,他们是完全独立。

        ②内聚:是指一个组件内部的粘合程度。一个组件的内聚程度越高,它内部元素互相联系的就愈紧密。换句话说,如果一个组件内的所有元素都是为了完成统一任务的话,那么这个组件是内聚的。

        2)异常识别和处理

        我们应该进行防御性设计,预言出可能导致系统问题的情况。

        防御设计并不容易。需求规格说明只是告诉我们系统假定要做的事儿,并没有告诉我们系统没有被假定要做的事儿。

        所谓异常,是指那些和我们真正想要系统作的事情相反的情况。在设计中我们将包括异常处理部分,这样系统可以处理每一个异常,而不会降低系统的特性。

        3)故障预防和故障容错

        我们软件安全并不是想当然的事情。在设计中,应该尽量预料到故障,并且处理他们,才能最大程度地保证安全,减少混乱。目标就是通过在设计中加入故障预防和故障处理尽量避免故障的出现。我们已经知道如何使用异常处理来解决我们可以事先预料到的不经常出现的情况,同样地,我们也可以防范组件中故障的出现,包括由其他组件、系统和界面带来的故障。

        处理方法:

         ①主动发现故障。如果我们设计系统时,只是等待系统出现失效的问题再解决,那么就是被动发现故障。然而,如果我们定期检查系统,或是事先预料可能会发生的问题,那就是主动发现故障。

         另一个主动发现故障的方法是使用冗余设计,将两个处理的结果对比然后判断他们是否相同。比如:一个计算的程序可以将所有的行加起来,然后将所有的列加起来,以保证总和是相同的。有的系统使用多个计算机,执行完全相同的设计。理论上,如果两个功能相等的系统由两个不同的设计组在不同的时间使用不同的技术开发,那么相同的故障同时发生的可能性就很小。这就是所谓的n-方案编程,但是由于设计者们开发时都遵循类似的模式和技术,使得它的可靠性并不像前面说的那样。

         ②故障改正。一旦发现故障,它就必须立即被改正。故障改正是系统对故障存在的补偿通常,故障改正稳定故障所造成的损害,也修改产品以消除故障。这样我们的设计必须包括故障的处理策略。通常,当故障影响系统时我们终止系统运行或简单地记录下来存在的问题,和当时的系统状态,以后再稳定所造成的损害。

         ③故障容错。改正一各故障有的时候太昂贵、有风险或是不便的。取而代之,我们可以将故障带来的损坏降低到最低程度,然后使它几乎不给用户带来干扰。故障容错是将一个故障所引起的损害分隔开来,这在很多情况下很便利,甚至是用户所希望的。一些故障容错的策略取决于预测故障位置和失效时间的能力。为了使系统能一直工作,设计者们必须能够猜测出什么可能出现问题。一些故障很容易预测,但是一些复杂的系统就比较难于分析了。同时,比较复杂的系统更可能存有大的故障。而且实现故障容错的代码可能自身还包含一些问题。这样,也要有故障容错策略用于分离可能的故障区域,而不仅是预测出故障就行了。

        7、改进设计的手段方法

        对于特定的需求,我们可以使用相同的手段。状态表,数据流图,SADT图等可以用来表达怎样设计而不是需求是什么的记法。开始设计时,我们可以用到上一节中为设计质量所建立的目标的特征,满足这些目标的设计通常就容易建立、测试、改正和维护了,所以我们要尽可能早地将特性加入到系统中去。

        1)减少复杂度

         设计时,我们要再没有改变解决办法本质的前提下,尽可能简化结构。随着设计的高质量特性被逐步转化通过开发,软件和硬件就更容易设计、构造和维护了。

        2)合同设计

        Meyer(199 2a)提出了一个设计方法,称为合同设计,是为了保证一个和设计满足它的规格说明的。他一开始将一个软件系统视为一组通讯组件,他们的交互是基于规格说明的精确定义的。而这些规格说明就称为合同,用于管理每一个组件是怎样和其他组件系统进行交互的。这样的规格说明不能用于保证一个组件的正确性,但是它是进行测试和验证的基础。

        一个描述类的特征的断言称为类不变量,这些特征总是应用于类,甚至当发生改变的时候。所以,当要改动的时候,有必要测试这些改变的正确性。

        类和断言可以和随后的执行相比较,在数学中证明他们两个是一致的。此外,断言提供了测试的基础:测试者可以决定每一个的结果。另外,当设计改变的时候,可以检查断言是否削弱或增强了。

        3)原型设计

        用户经常提出一些要求,而我们并不能确定我们是否能够实现。通过可行的原型,我们可以发现一开始提出的解决办法是否能够真的解决我们手边的问题。这样,原型可以促进开发人员之间的,还有和用户之间的交流。通过这种方式,我们可以在编码之前就解决很多问题,而且还可以避免测试中的一些问题。

        4)故障树分析法

        我们已经知道有关故障识别、改正和容错的重要性。这里有几种手段是用于识别故障和定位它们的。故障树分析法,最初是由美国Minuteman导弹程序发展的,可以帮助我们找出可能导致失效的情况。在这个意义上,这个名字可能起错了,因为我们不是要分析故障,而是要找出失效可能的原因。我们建立故障树是要展示从结果到原因的逻辑路径。然后,根据我们所采用的设计策略,这些树要用于支持故障改正或容错。通过识别可能的失效问题开始我们的分析。我们可以用一组指示词来帮助我们理解系统是怎样背离一开始的初衷的。

        割集是割集树的叶结点的集合。如图5.20,左边是一个故障树,G1是第一个逻辑门,它是或门,因此在割集树中被分成G2,G3。依次地分解成右图的割集树。在这个图中,割集就是{A1,A3},{A1,A4},{A2,A3},{A2,A4},{A4,A5}.含义是,只有当Ai,Aj都发生的时候{Ai,Aj}才发生。这样我们就可以推断出所有可能的原因了。

        到目前为止,应用于任何系统、硬件或是软件的概念我们已经描述过了,软件故障树分析的设计分解可以更精确,因为我们可以根据它的次序、决定和迭代进行任何形式的设计。这样每一种次序、决定和迭代结构都可以转换成都可以转换成相等的割集树表示和一个很大的割集。下一步,通过假设一些失效可能发生要详细检查这个设计,发现可能产生它的事件集合。如果没有发现。我们就假设这个失效不会发生。

        一旦我们在设计中发现失效点,我们就要重新设计以减小系统的脆弱性。当在设计中发现故障的时候,我们有如下选择:

        ①去掉它

        ②增加一个组件或状况以防止导致这个故障的可能的输入

        ③增加一个可以恢复失效所引起的损害的组件

        虽然第一个选择是最佳的,但是它总是不可能的。

        故障树通常用于计算给定的效率可能发生的可能性。但是也有一些缺点。首先,构造图要花费一定的时间。其次,很多系统都有很多依赖,所以很难发现不一致性,就像很难将注意力集中在设计的最关键的部分一样。此外,每一个失效必需的先决条件的数量和种类都不少,而且不总那么容易就能确定,同时也没有度量方法可以帮助我们将他们整理好。然而,研究人员们仍在继续寻找能够自动建立树和分析过程的方法。在美国和加拿大,故障树分析法被用于关键的航空和核应用,在这些领域里,失效的危险率值得花费相当的努力去建立和评估故障树。

四、软件质量

软件工程之过程与设计_第1张图片

McCall-的质量模型

你可能感兴趣的:(软件工程之过程与设计)