现代软件工程
上QQ阅读APP看书,第一时间看更新

4.4 关于设计的概念

在软件工程历史上产生了一系列基本的软件设计概念。尽管多年来对每一种概念的关注程度不断变化,但它们都经受住了时间的考验。基础的软件设计概念为“使程序正确”提供了必要的框架,每一种概念都为软件设计者提供了应用更加复杂设计方法的基础。每一种方法都可以回答下面的问题。

●使用什么标准将软件分割为独立的构件?

●功能和数据结构细节如何从软件的概念表示中分离出来?

●定义软件设计技术质量的统一标准是什么?

4.4.1 抽象

当考虑某一问题的模块化解决方案时,可以给出许多抽象级。在最高抽象级上,使用问题所处环境的语言以概括性的术语描述解决方案。在较低的抽象级上,将提供更详细的解决方案说明。当力图陈述一种解决方案时,面向问题的术语和面向实现的术语会同时使用。最后,在最低的抽象层次上,解决方案将以一种能直接实现的方式得到陈述。

当开发不同层次的抽象时,软件设计师力图创建过程抽象和数据抽象。过程抽象是指具有明确和有限功能的指令序列,其命名暗示了这些功能,但隐藏了具体的细节。过程抽象的例子如单词“开门”,“开”隐含了一长串的过程性步骤(例如,走到门前,伸出手并抓住把手,转动把手并拉门,从门口走开等)。

数据抽象是描述数据对象的冠名数据集合。在过程抽象“开”的场景下可以定义一个名为door的数据抽象。同任何数据对象一样,door的数据抽象将包含一组描述门的属性(例如,门的类型、转动方向、开门方法、重量和尺寸)。因此,过程抽象“开”将利用数据抽象门的属性中所包含的信息。

4.4.2 体系结构

软件体系结构意指“软件的整体结构和这种结构为系统提供概念完整性的方式”。从最简单的形式来看,体系结构是程序构件(模块)的结构或组织、这些构件交互的形式,以及这些构件所用数据的结构。然而在更广泛的意义上,构件可以概括为表示主要的系统元素及其交互。

软件设计的目标之一是导出系统的体系结构示意图,该示意图作为一个框架来指导更详细的设计活动。一系列的体系结构模式使软件工程师能够解决常见的设计问题。

下面这组属性应该被指定为体系结构设计的一部分。

1)结构特性。体系结构设计表示定义了系统的构件(如模块、对象和过滤器)、构件被封装的方式,以及构件之间相互作用的方式。例如,对象封装了数据和过程,过程操纵数据并通过方法调用进行交互。

2)外部功能特性。体系结构设计描述应当指出设计体系结构如何满足需求。这些需求包括性能需求、能力需求、可靠性需求、安全性需求、可适应性需求,以及其他系统特征需求。

3)相关系统族。体系结构应当能抽取出在一类相似系统开发中经常遇到的重复性模式。本质上,设计应当能够重用体系结构构件。

一旦给出了这些特性的规格说明,体系结构设计就可以用一种或多种模型来表示。结构模型将体系结构表示为程序构件的一个有组织的集合。通过确定类似应用中遇到的可复用的体系结构来设计框架,框架模型可以提高设计抽象级别。动态模型强调程序体系结构的行为方面,指明结构或系统配置作为外部事件的函数将如何变化。过程模型注重系统必须提供的业务或技术流程设计。最后,功能模型可以用来表示系统的功能层次结构。

4.4.3 模式

设计模式描述了在某个特定场景与可能影响模式应用和使用方式的“影响力”中解决某个特定的设计问题的设计结构。

每个设计模式的目的都是提供一个描述,以便设计人员能够确定:①模式是否适用于当前的工作;②模式是否能够复用(因此,节约设计时间);③模式是否能够用于指导开发一个类似的、但是功能或结构不同的模式。

4.4.4 模块化

模块化是关注点分离最常见的表现。软件被划分为独立命名的、可处理的构件,有时被称为模块,把这些构件集成到一起可以满足问题的需求。

有人提出“模块化是软件的单一属性,它使程序能被智能化地管理”。软件工程师难以掌握单块软件(即由一个单独模块构成的大程序)。对于单块大型程序,其控制路径的数量、引用的跨度、变量的数量和整体的复杂度使得理解这样的软件几乎是不可能的。几乎所有的情况下,为了理解起来更容易,都应当将设计划分成许多模块,这样做的结果是,构建软件所需的成本将会随之降低。应该避免不足的模块化或过度的模块化问题。

模块化设计(以及由其产生的程序)使开发工作更易于规划;可以定义和交付软件增量;更容易实施变更;能够更有效地开展测试和调试;可以进行长期维护而没有严重的副作用。

4.4.5 信息隐蔽

信息隐蔽原则建议模块应该“具有的特征是:每个模块对其他所有模块都隐蔽自己的设计决策”。换句话说,模块应该规定并设计成为在模块中包含的信息(算法和数据)不被不需要这些信息的其他模块访问。

隐蔽意味着通过定义一系列独立的模块可以得到有效的模块化,独立模块相互之间只交流实现软件功能所必需的那些信息。抽象有助于定义构成软件的过程(或信息)实体。隐蔽定义并加强了对模块内过程细节的访问约束和对模块所使用的任何局部数据结构的访问约束。

将信息隐蔽作为模块化系统的一个设计标准,在测试和随后的软件维护过程中需要进行修改时,可提供最大的益处。由于大多数数据和过程对软件的其他部分是隐蔽的,因此,在修改过程中不小心引入的错误不会传播到软件的其他地方。

4.4.6 功能独立

功能独立的概念是关注点分离、模块化、抽象和信息隐蔽概念的直接产物。通过开发具有“专一”功能和“避免”与其他模块过多交互的模块,可以实现功能独立。换句话说,软件设计时应使每个模块仅涉及需求的某个特定子集,并且当从程序结构的其他部分观察时,每个模块只有一个简单的接口。

具有有效模块化(也就是独立模块)的软件更容易开发,这是因为功能被分隔而且接口被简化(考虑由一个团队进行开发时的结果)。独立模块更容易维护(和测试),因为修改设计或修改代码所引起的副作用被限制,减少了错误扩散,而且模块复用也成为可能。概括地说,功能独立是良好设计的关键,而设计又是软件质量的关键。

独立性可以通过两条定性的标准进行评估:内聚性和耦合性。

内聚性显示了某个模块相关功能的强度,是信息隐蔽概念的自然扩展。一个内聚的模块执行一个独立的任务,与程序的其他部分构件只需要很少的交互。简单地说,一个内聚的模块应该(理想情况下)只完成一件事情。即使人们总是争取高内聚性(即专一性),一个软件构件执行多项功能也经常是必要的和可取的。然而,为了实现良好的设计,应该避免“分裂型”构件(执行多个无关功能的构件)。

耦合性显示了模块间的相互依赖性,它表明软件结构中多个模块之间的相互连接性。耦合性依赖于模块之间的接口复杂性、引用或进入模块所在的点及什么数据通过接口进行传递。在软件设计中,应当尽力得到最低可能的耦合。模块间简单的连接性使得软件易于理解并减少“涟漪效果”的倾向。当在某个地方发生错误并传播到整个系统时,就会引起“涟漪效果”。

4.4.7 求精

逐步求精是一种自顶向下的设计策略。通过连续精化过程细节层次来实现程序的开发,通过逐步分解功能的宏观陈述(过程抽象)直至形成程序设计语言的语句来进行层次开发。求精实际上是一个细化的过程。该过程从高抽象级上定义的功能陈述(或信息描述)开始,概念性地描述了功能或信息,但是没有提供有关功能内部的工作或信息内部的结构。可以在原始陈述上进行细化,随着每个精化(细化)的持续进行,将提供越来越多的细节。

抽象和精化是互补的概念。抽象能够明确说明内部过程和数据,但对“外部使用者”隐藏了低层细节;精化有助于在设计过程中揭示低层细节。这两个概念均有助于设计人员在设计演化中构造出完整的设计模型。

4.4.8 方面

在开始进行需求分析时,一组“关注点”就出现了。这些关注点“包括需求、用例、特征、数据结构、服务质量问题、变量、知识产权边界、合作、模式及合同”。理想情况下,可以按某种方式组织需求模型,该方式允许分离每个关注点(需求),使得能够独立考虑每个关注点(即需求)。然而实际上,某些关注点跨越了整个系统,从而很难进行分割。

当开始进行设计时,需求被精化为模块设计表示。考虑两个需求:A和B。“如果已经选择了一种软件分解(精化),在这种分解中,如果不考虑需求A的话,需求B就不能得到满足”,那么需求A横切需求B。

方面是一个横切关注点的表示,因此标识方面很重要,以便在开始求精和模块化的时候,设计能够很好地适应这些方面。在理想情况下,一个方面作为一个独立的模块(构件)进行实施,而不是作为“分散的”或者和许多构件“纠缠的”软件片断进行实施。为了做到这一点,设计体系结构应当支持定义一个方面,该方面即一个模块,该模块能够使该关注点经过它横切的所有其他关注点而得到实施。

4.4.9 重构

很多敏捷方法都建议一种重要的设计活动——重构,即一种重新组织的技术,可以简化构件的设计(或代码)而无须改变其功能或行为。“重构是使用这样一种方式改变软件系统的过程:不改变代码(设计)的外部行为而是改进其内部结构。”

当重构软件时,检查现有设计的冗余性、没有使用的设计元素、低效的或不必要的算法、拙劣的或不恰当的数据结构,以及其他设计不足,修改这些不足以获得更好的设计。例如,第一次设计迭代可能得到一个构件,表现出很低的内聚性(即,执行3个功能但是相互之间仅有有限的联系)。在仔细思考之后,设计人员可以决定将构件重构为3个独立的构件,每个构件都表现出较高的内聚性。其结果是软件更容易集成、测试和维护。

4.4.10 设计类

面向对象(object-oriented,OO)范型已经广泛地应用于现代软件工程。面向对象的需求模型定义了一组分析类,每一个分析类都描述问题域中的某些元素,这些元素关注用户可见的问题方面。分析类的抽象级相对较高。

在设计模式演化时,必须定义一组设计类:①通过提供设计细节精化分析类,这些设计细节将促成类的实现;②实现支持业务解决方案的软件基础设施。

有5种类型的设计类,每一种都表示了设计体系结构的一个不同层次。

●用户接口类:定义人-机交互(Human-Computer Interaction,HCI)所必需的所有抽象。在很多情况下,HCI出现在隐喻的环境(如支票簿、订单表格和传真机)中,而接口的设计类可能是这种隐喻元素的可视化表示。

●业务域类:通常是早期定义的分析类的精化。这些类识别实现某些业务域元素所必需的属性和服务(方法)。

●过程类:实现完整的管理业务域类所必需的低层业务抽象。

●持久类:表示将在软件执行之外持续存在的数据存储(如数据库)。

●系统类:实现软件管理和控制功能,使得系统能够运行,并在其计算环境内与外界通信。

随着体系结构的形成,每个分析类转化为设计表示,抽象级就降低了。也就是说,分析类使用业务域的专门用语描述对象(以及对象所用的相关服务);设计类更多地表现技术细节,用于指导实现。应该评审每个设计类,以确保设计类是“组织良好的”。

组织良好的设计类具有以下4个特征。

1)完整性与充分性。设计类应该完整地封装所有可以合理预见到的(根据对类名的理解)存在于类中的属性和方法。例如,为视频编辑软件定义的Scene类,只有当它包含与创建视频场景相关的所有合理的属性和方法时,才是完整的。充分性确保设计类只包含那些“对实现这个类的目的足够”的方法,不多也不少。与一个设计类相关的方法应该关注实现类的某一个服务。例如,视频编辑软件的VideoClip类,可能用属性start-point和end-point指定剪辑的起点和终点(注意,加载到系统的原始视频可能比要用的部分长)。方法setStartPoint()和setEndPoint()为剪辑提供了设置起点和终点的唯一手段。

2)高内聚性。一个内聚的设计类具有小的、集中的职责集合,并且专注于使用属性和方法来实现那些职责。例如,视频编辑软件的VideoClip类可能包含一组用于编辑视频剪辑的方法。只要每个方法只关注与视频剪辑相关的属性,内聚性就得以维持。

3)低耦合性。在设计模型内,设计类之间相互协作是必然的。但是,协作应该保持在一个可以接受的最小范围内。如果设计模型高度耦合(每个设计类和其他所有设计类都有协作关系),那么系统就难以实现和测试,并且维护也很费力。通常,一个子系统内的设计类对其他子系统中的类应仅有有限的了解。该限制被称为Demeter定律[1],该定律建议一个方法应该只向周边类中的方法发送消息。