软件工程第9章构件级设计主要内容什么是构件设计基于类的构件实施构件级设计设计传统构件基于构件的开发小结构件级设计一套完整的软件构件是在体系结构设计过程中定义的。但是没有在接近代码的抽象级上表示内部数据结构和每个构件的处理细节。构件级设计定义了数据结构、算法、接口特征和分配给每个软件构件的通信机制。构件级设计必须能够在建造软件之前就确定该软件是否可以工作。为了保证设计的正确性,以及与早期设计表示(即数据、体系结构和接口设计)的一致性,构件级设计需要以一种可以评审设计细节的方式来表示软件。它提供了一种评估数据结构、接口和算法是否能够工作的方法。构件级设计数据、体系结构和接口的设计表示构成了构件级设计的基础。每个构件的类定义或者处理叙述都转化为一种详细设计,该设计采用图形或基于文本的形式来详细说明内部的数据结构、局部接口细节和处理逻辑。设计符号包括UML图和一些辅助表示。通过使用一系列结构化编程结构来说明过程设计。通常的做法是采用现有的可复用软件构件,而不是开发新的构件。构件级设计建模每个构件的设计都以图形的、图表的或基于文本的方式表示,这是构件级设计阶段产生的主要工作产品。采用设计走查或审查机制。对设计执行检查以确定数据结构、接口、处理顺序和逻辑条件等是否都正确,并且给出早期设计中与构件相关的数据或控制变换。构件级设计体系结构设计第一次迭代完成之后,就应该开始构件级设计。在这个阶段,全部的数据和软件的程序结构都已经建立起来,其目的是把设计模型转化为运行软件。但是现有设计模型的抽象层次相对较高,而可运行程序的抽象层次相对较低。这种转化具有挑战性,因为可能会在软件过程后期阶段引入难于发现和改正的微小错误。构件级设计用编程语言表示构件级设计是可以的。其实,程序的创建是以体系结构设计模型作为指南的。一种可供考虑的方法是使用某些能够容易转化为代码的中间表示来表示构件级设计。无论采用何种机制来表示构件级设计,定义的数据结构、接口和算法应该遵守各种已经精心规定好的设计指导准则,以避免在过程设计演化中犯错误。什么是构件通常来讲,构件是计算机软件中的一个模块化的构造块。OMGUML规范将构件定义为“系统中模块化的、可部署的和可替换的部件,该部件封装了实现并暴露一系列接口”。构件存在于软件体系结构中,因而构件在完成所建系统的需求和目标中起重要作用。由于构件驻留于软件体系结构的内部,它们必须与其他的构件和存在于软件边界以外的实体进行通信和合作。面向对象的观点在面向对象软件工程环境中,构件包括一个协作类集合。构件中的每一个类都被详细阐述,包括所有的属性和与其实现相关的操作。作为细节设计的一部分,所有与其他设计类相互通信协作的接口必须予以定义。为了完成这些,设计师从分析模型开始,详细描述分析类(该类与问题域相关)和基础类(该类为问题域提供了支持性服务)。面向对象的观点考虑为一个高级印刷车间构建软件。软件的目的是为了收集前台的客户需求,对印刷业务进行定价,然后把印刷任务交给自动生产设备。在需求工程中得到了一个被称为PrintJob的分析类,如图9-1所示。PrintJob有两个接口:computeJob具有对任务进行定价的功能,initiateJob能够把任务传给生产设备。面向对象的观点图9-1设计构件的细化面向对象的观点构件级设计将由此开始。必须对PrintJob构件的细节进行细化,以提供指导实现的充分信息。通过不断补充作为构件PrintJob的类的全部属性和操作,来逐步细化最初的分析类。对于体系结构设计组成部分的每一个构件都要实施细化。细化一旦完成,要对每一个属性、每一个操作和每一个接口进行更进一步的细化。适合每个属性的数据结构必须予以详细说明。另外还要说明实现与操作相关的处理逻辑的算法细节。最后是实现接口所需机制的设计。对于面向对象软件,还包含对实现系统内部对象间消息通信机制的描述。传统观点在传统软件工程环境中,一个构件就是程序的一个功能要素,程序由处理逻辑及实现处理逻辑所需的内部数据结构以及能够保证构件被调用和实现数据传递的接口构成。传统构件也被称为模块,作为软件体系结构的一部分,它承担如下三个重要角色之一:(1)控制构件,协调问题域中所有其他构件的调用;(2)问题域构件,完成部分或全部用户的需求;(3)基础设施构件,负责完成问题域中所需相关处理的功能。传统观点与面向对象的构件相类似,传统的软件构件也来自于分析模型。不同的是在该种情况下,是以分析模型中的数据流要素作为导出构件的基础。数据流图最低层的每个变换都被映射为某一层次上的模块。一般来讲,控制构件位于层次结构顶层附近,而问题域构件则倾向位于层次结构的底层。为了获得有效的模块化,在构件细化过程中采用了功能独立性的设计概念。传统观点再来考虑为一个高级影印中心构建软件。在分析模型建立过程中导出一组数据流图。假设这些数据流图已经被映射到图9-2中所显示的体系结构中。图中每个方框都表示一个软件构件。传统观点图9-2一个传统系统的结构图传统观点在构件级设计中,图9-2中的每个模块都要被细化。需要明确定义模块的接口,即每个经过接口的数据或控制对象都需要明确加以说明。定义模块内部使用的数据结构,采用逐步求精方法设计完成模块中相关功能的算法。有时候需要用状态图表示模块行为。传统观点考虑ComputePageCost模块。该模块的目的在于根据用户提供的规格说明来计算每页的印刷费用。为了实现该功能需要以下数据:文档的页数,文档的印刷份数,单面或者双面印刷,颜色,纸张大小。这些数据通过该模块的接口传递给ComputePageCost。ComputePageCost根据任务量和复杂度,使用这些数据来决定一页的费用——这是一个通过接口将所有数据传递给模块的功能。每一页的费用与任务的大小成反比,与任务的复杂度成正比。传统观点图9-3给出了使用UML建模符号描述的构件级设计。其中ComputePageCost模块通过调用getJobData模块和数据库接口accessCostDB来访问数据。接着,对ComputePageCost模块进一步细化,给出算法和接口的细节描述。其中算法的细节可以由图中显示的伪代码或者UML活动图来表示。接口被表示为一组输入和输出的数据对象或者数据项的集合。设计细化的过程需要一直进行下去,直到能够提供指导构件构造的足够细节为止。传统观点图9-3ComputePageCost的构件级设计过程相关的观点上面提及的关于构件级设计的面向对象观点和传统观点,都假定从头开始设计构件。即设计者必须根据从分析模型中导出的规格说明创建新构件。当然,还存在另外一种方法。在过去的10年间,软件工程已经开始强调使用已有构件或设计模式来构造系统的必要性。实际上,软件工程师在设计过程中可以使用已经过验证的设计或代码级构件目录。当软件体系结构设计完后,就可以从目录中选出构件或者设计模式,并用于组装体系结构。由于这些构件是根据复用思想来创建的,所以其接口的完整描述、要实现的功能和需要的通信与协作等对于设计者来说都是可以得到的。设计基于类的构件构件级设计利用了分析模型开发的信息和体系结构模型表示的信息。当选择了面向对象软件工程方法后,构件级设计主要关注分析类的细化和基础类的定义和精化。这些类的属性、操作和接口的详细描述是开始构建活动之前所需的设计细节。基本设计原则有四种适用于构件级设计的基本设计原则,这些原则在使用面向对象软件工程方法时被广泛采用。使用这些原则的根本动机在于,使得产生的设计在发生变更时能够适应变更并且减少副作用的传播。开关原则(OCP):模块应该对外延具有开放性,对修改具有封闭性。简单地说,设计者应该采用一种无需对构件自身内部做修改就可以进行扩展的方式来说明构件。SAFEHOME实例[50]0SAFEHOME实例[51]图9-4遵循OCP原则基本设计原则Liskov替换原则(LSP):“子类可以替换它们的基类”。最早提出该原则的[LIS88]建议,将子类传递给构件来代替基类时,使用基类的构件应该仍然能够正确完成其功能。LSP原则要求源自基类的任何子类必须遵守基类与使用该基类的构件之间的隐含约定。基本设计原则依赖倒置原则(DIP):“依赖于抽象,而非具体实现”。抽象可以比较容易地对设计进行扩展,又不会导致大的混乱。构件依赖的具体构件越多,其扩展起来就越困难。接口分离原则(ISP):“多个用户专用接口比一个通用接口要好”。多个客户构件使用一个服务器类提供的操作的实例很多。ISP原则建议设计者应该为每一个主要的客户类型都设计一个特定的接口。只有那些与特定客户类型相关的操作,才应该出现在该客户的接口说明中。如果多个客户要求相同的操作,则这些操作应该在每一个特定的接口中都加以说明。基本设计原则假设FloorPlan类用在SafeHome的安全和监视功能中。对于安全功能,FloorPlan只有在配置活动中使用,并且使用placeDevice()、showDevice()、groupDevice()和removeDevice()等操作实现在建筑平面图中放置、显示、分组和删除传感器。SafeHome监视功能除了需要这4个有关安全的操作之外,还需要特殊的操作showFOV()和showDeviceID()来管理摄像头。因此,ISP建议为来自SafeHome功能的两个客户端构件定义特殊的接口。安全接口应该只包括placeDevice()、showDevice()、groupDevice和removeDevice()4种操作。监视接口应包括placeDevice()、showDevice()、groupDevice和removeDevice()、showFOV()和showDeviceID()6种操作。基本设计原则尽管构件级设计原则提供了有益的指导,但构件自身不能够独立存在。在很多情况下,单独的构件或者类被组织到子系统或包中。[Mar00]中给出了在构件级设计中可以应用的另外一些打包原则。基本设计原则发布复用等价性原则:”复用的粒度就是发布的粒度”。当类或构件被设计用以复用时,在可复用实体的开发者和使用者之间就建立了一种隐含的约定关系。开发者承诺建立一个发布控制系统,用来支持和维护实体的各种老版本,同时用户缓慢地将其升级到最新版本。明智的方法是将可复用的类分组打包成能够管理和控制的包作为一个更新的版本,而不是对每个类分别进行升级。基本设计原则共同封装原则:”一同变更的类应该合在一起”。类应该根据其内聚性进行打包。即当类被打包成设计的一部分时,它们应该处理相同的功能或者行为域。当域的一些特征必须变更时,只有那些包中的类才有可能需要修改。这样可以进行更加有效的变更控制和发布管理。基本设计原则共同复用原则:”不能一起复用的类不能被分到一组”。当包中的一个或者多个类变更时,包的发布版本数量也会发生。所有那些依赖于已经发生变更的包的类或者包,都必须升级到最新的版本,并且都需要进行测试以保证新发布的版本能够无故障运转。如果类没有根据内聚性进行分组,那么这个包中与其他类无关联的类有可能会发生变更,而这往往会导致进行没有必要的集成和测试。因此,只有那些一起被复用的类才应该包含在一个包中。构件级设计指导方针构件。对那些已经被确定为体系结构模型一部分的构件应该建立命名约定,并对其做进一步的细化和精化,使其成为构件级模型的一部分。体系结构构件的名字来源于问题域,并且应该能够被查看体系结构模型的所有共利益者理解。另一方面,基础构件或者细化后的构件级类应该以能够反映其实现意义的名称来命名。在详细设计层面使用构造型帮助识别构件的特性也很有价值。例如,infrastructure可以用来标识基础构件;database可以用来标识服务于一个或多个设计类或者整个系统的数据库;table可以用来标识数据库中的表。构件级设计指导方针接口。接口提供关于通信和协作的重要信息。然而接口表示的随意性会使构件图趋于复杂化。[AMB02]建议:(1)当构件图变得复杂时,在