软件体系结构——第九章<构件设计>
一、用例设计
用例设计(Use-Case Design)的目标
-
利用交互图改进用例实现——完善处理细节
-
改进对设计类的操作需求——明确类的职责
-
改进对子系统和接口的操作需求——完善对外提供服务的公共接口
输入——设计的来源
- 用例分析的结果(用例实现)、设计元素
输出——设计的结果
- 用例实现(设计)——对“输入”完善后的结果
用例设计步骤
-
利用构架设计中所定义的设计元素取代分析类,重新确定参与交互的对象——结合设计元素,定义设计对象间的交互(交互图)
-
利用这些设计对象重新绘制交互图,并遵循相应的设计原则和模式,完成职责分配过程——可利用子系统简化交互图
-
递增地并入可适用的构架机制:引入所需的设计机制和设计模式,调整和完善交互图——描述与持久化、遗留系统 等相关的行为
-
检查细化用例事件流(验证、处理)的实现细节
-
最后,评价类和子系统 ,并补充和完善
应用交互图如何进行职责分配?
利用设计元素,进行类的职责分配,完成用例实现的交互图(顺序图或通信图)
需要遵循相关的设计原则和模式
-
职责分配模式:通用职责分配软件模式。牵涉的主要内容如下:
- 领域专家、创建者、高内聚、低耦合、控制者、多态、纯虚构、中介、受保护变化(不要和陌生人讲话)
-
单一类职责原则(SRP)
- 保持类职责的单一性,设计高内聚的类
用例分析与用例设计的差别
差别主要表现在类的职责和操作的差别
-
分析阶段:只是定义类的初步职责
-
设计阶段:需要定义类的具体操作来实现这些职责,如,在交互图中:
- 发送到设计类的消息,对应该类的操作
- 发送到子系统的消息,对应其接口的操作
分析中主要的业务职责集中在控制类中,故设计的重点就是对控制类职责的实现
- 注意:设计时应避免臃肿的控制器的产生
什么是臃肿的控制器?
臃肿的控制器:低内聚、缺乏重点,且处理过多的职责,即违背了OO的相关设计原则,如:
-
高内聚、低耦合
-
单一类职责原则(SRP)
解决方案——还是分解
-
加入更多的控制器(更多的分层)
-
将部分职责委托给其它对象
用例设计——改进用例实现步骤
确定参与用例流中参与交互的每个对象
- 如:用设计元素取代分析类——即设计类或主动类
在交互图中描绘每一个参与对象
- 遵循相应的设计原则和模式,利用交互图完成职责分配过程——即参与交互的每个对象都有职责存在
递增地并入可适用的构架机制
- 引入所需的设计机制(设计模式),调整和完善交互图——即遗留系统的子系统和接口的引入
在交互图中如何表示子系统?
接口
-
即任何实现该接口的模型元素(具体类或子系统)
-
由于接口没有任何实现,故不能发出任何消息
代理类
-
为每个子系统定义一个代理类,代表特定的子系统
-
代理类可以发送和接收消息
二、子系统设计
子系统设计
子系统设计(Subsystem Design)的目标
-
用所包含的设计元素、外部子系统/接口的协作来定义在子系统接口中指定的操作
-
定义子系统的内部实现结构
-
定义子系统接口和所包含的类之间的关系
-
确定与其他子系统之间的依赖关系
输入(子系统的来源):
- 具有接口定义的设计子系统
输出(设计后的结果)
-
更新后的接口详细定义
-
子系统内部设计模型(静态结构——类图)
子系统建模的约定
子系统设计步骤
①将子系统的行为分发到各个子系统元素中——即分发子系统的职责
②描述子系统内部元素:在交互图的基础上,定义每个设计元素的结构和关系——即完成子系统内部设计模型——类结构设计
③定义子系统间的依赖关系:分析子系统与外部设计元素之间的依赖关系——即明确子系统之间的耦合来实现复用
子系统的职责
接口操作用来定义子系统的职责
- 对接口的操作实现进行建模
接口操作可以由以下实现
-
内部类的操作
-
内部子系统的操作
子系统设计策略
遵循面向接口编程的思想:
- 采用大量的接口来解耦子系统与外部的耦合,才可以保证子系统的独立性和可替换性,从而提高系统的稳定性。
定义子系统间的依赖关系
- 子系统与子系统之间依赖关系
- 子系统与包之间依赖关系(小心使用)
实例:PaymentSystem依赖关系
构件(Component)图的引入
构件图的系统逻辑:
-
描述系统的词汇(类):类图
-
描述系统希望的行为(需求分析):用例图
-
描述如何完成系统的行为:交互图、状态机图、活动图
系统的实现和配置:
- 描述软件组件及软硬件配置:构件图、部署图
什么是构件图(Component Diagram)?
构件图描述软件系统中构件与构件之间的关系,显示代码的物理结构。
构件是逻辑构架中定义的概念和功能(类、对象及它们之间的关系、协作)在物理构架中的实现。如图所示:
构件图组成
①构件
- 构件图中的构件是定义了良好接口的物理实现单元,是系统中可替换的物理部件。构件表示将类、接口等逻辑元素打包而成的物理模块。
②接口(构件之间必须依赖于接口)
- 在构件图中,构件可以通过其他构件的接口来使用其他构件中定义的操作——通过使用命名接口,可以避免在系统中各个构件之间直接发生依赖关系,有利于构件的替换。构件图中的接口使用一个小圆圈表示。
③接口和构件的关系
-
实现关系:接口和构件之间用实线连接表示实现关系;
-
依赖关系:接口和构件之间用虚线箭头表示依赖关系;
注意:实现关系须将存在的接口分配给构件。
绘制构件图与构件间的关系
设计者应根据软件系统的实际业务组成情况,绘制构件图,并描述构件之间的依赖关系。
构件图建模技术
实际建模中,设计者可参照以下步骤进行:
-
对系统中的构件建模(定义构件);
-
定义相应组件提供的接口(定义所需接口);
-
对它们之间的关系建模(定义依赖关系);
-
将逻辑设计映射成物理实现(定义构件代码);
-
对建模的结果进行精化和细化(实现细化);
-
采用构件包建模子系统(打包)。
三、类设计
设计类
来自分析类,是已经完成了规格说明并且达到能够被实现程度的类。类的完整信息包括:
-
属性:名称、类型、缺省值
-
方法:名称、参数、返回值
-
关系:多重性、角色名以及实现的考虑
主要来源于问题域和求解域
-
问题域:通过对分析类的精化得到的具体问题域——添加类的实现细节
-
求解域:来自于不同的实现环境,提供了能够实现系统的技术工具,如Java类库、框架库等
设计类剖析
分析时,主要尽量捕获系统需要的行为,而完全不必考虑如何去实现这些行为;
设计时,必须准确地说明类是如何履行它们的职责,主要包括:
-
完整的属性集合:包括详细说明的名称、类型、可视性和一些默认值 等;
-
将分析类指定的职责转化成设计类的一个或多个方法的完整集合
类设计应该主要完成的工作
①创建初始设计类——定义类的操作
- 确定类的职责
②定义操作和方法——定义类的方法和状态
-
方法:操作的具体代码实现
-
状态:对象的状态如何影响它的行为
③状态建模——建立状态机图,描述类的状态
④定义类的完整属性——描述类的属性特征
⑤定义类之间的关系——描述业务的语义联系
3.1、创建初始设计类
创建初始设计类,需要考虑三个方面:
①分析类的构造型
-
边界:用户界面和系统接口,主要考虑用户界面类
-
控制:负责边界类与实体类的交互控制
-
实体:业务数据
②可适用的设计模式
- 如,MVC
③构架机制
- 持久性、分布、遗留系统、…
边界类的设计策略
用户界面(UI)边界类:
-
使用什么用户界面开发工具?如,4GL
-
哪些界面可以用开发工具直接创建?
外部遗留系统接口边界类
- 通常建模为子系统,并对外提供公共接口
实体类的设计策略
实体对象通常是被动的和持久性的
依据性能需求可能要对实体类进行重构
持久性构架机制影响实体类的创建
控制类的设计策略
如何处理控制类
-
是否真正地需要它们?(由设计类的方法来实现)
-
它们应当被分开吗?(分解)
下列情况下,控制类可能变为真正的设计类
-
封装非常重要的控制流行为(完成的职能工作)
-
封装的行为很可能变化(流程处理)
-
必须跨越多个进程或处理器进行分布(远程调用)
-
封装的行为要求一些事务管理(如数据库事务处理)
调整控制类的基本策略
提供公共控制类
- 多个用例若有同样活动的控制类,将其整合起来,把相同部分作为一个新的控制类——定义公共接口
删除冗余的控制类
- 把一些简单的由边界类委托的职责交给实体类之后,删除没有进行场景控制的实体类——与业务无关的
分解复杂的控制类
- 若用例的控制流程过于复杂,则可考虑根据不同的控制处理业务分解成多个控制类——处理流程分解
3.2、定义操作和方法
定义操作
操作:即类的行为特征——描述了该类对于特定请求(行为)做出的应答(事务处理)
操作语法格式:可见性 操作名([参数方向] 参数名称:参数类型[多重性]=默认值, …):返回类型[多重性]
UML中操作的可见性:
-
公有(Public, +)
-
私有(Private, -)
-
保护(Protected, #)
-
包(Package, ~)(不常用)
发现操作:显示在交互图中的消息(用例设计)
其它独立功能的实施
-
类自身的管理功能(如:数据的“四轮马车”)
-
类复制的需要(测试类是否相等,创建类副本等)
-
其它操作机制的需要(垃圾收集(删除)、测试等)
定义方法
方法是指操作的具体算法实现,用来描述操作如何实现的流程
目标
- 描述了类对外提供的接口,是类的外在行为。可通过交互图完成操作内部实现算法的设计——操作的具体实现。
定义方法时需要考虑的内容
-
特殊算法
-
使用到的其它对象和操作
-
类本身属性和参数如何实现和使用
-
类之间的关系如何实现和使用
方法(行为)的实现往往受对象的状态影响
3.3、状态建模
对象的状态(State)反映于现实世界的一系列特征,这些特征影响该对象的实现(如:门的状态)
状态建模的目标
- 明确一个对象的状态如何影响对象的行为
利用状态机图进行建模
- 状态机图(State Machine diagram)是由状态和转移(转换)组成的有向图,描述了一个对象的发展历史(即对象的状态变迁过程)
状态机图的作用
-
可以帮助系统分析员、设计人员和开发人员理解系统中各个对象的行为变化过程。
-
在UML中,状态机图和类图需要相互配合,以便完整描述类的属性特征。
注意:仅用类图是不够的,因为它只能描述类对象的静态特征,而利用状态机图可以对类对象的动态行为进行建模。
如何理解状态机(State Machine)
是一个类的对象所有可能的生命历程的模型。
状态机包括状态图和活动图两种表示方法。
-
状态图以状态为中心,用于对系统的动态行为进行建模,展示的主要内容是根据对象的状态所做出的行为。
-
活动图以活动流程为中心,用于对计算流程或业务处理过程建模,展示的主要内容是对象的活动状态。
状态机图:用来定义类的状态
通常描述一个特定对象在其生命周期中的所有可能的状态以及由于各种事件的发生(或满足一定条件)而引起状态的转移(状态转换),如图所示:
实例:图书的状态机图
状态的表示
状态(state)描述了对象的生命周期中所处的某种条件或状况。主要包括以下4种:
-
入口动作(entry/动作名)、出口动作(exit/动作名)
-
状态活动(do/动作名)
-
内部转移(????/动作名)、延迟事件(事件名/defer)
-
子状态机(不常用)
转移(转换)的表示
转移(transition):状态间的关联。是从一个源状态到一个目标状态之间的一个有向关系。
包括三个要素:
-
事件:事件发生时转移才有可能发生
-
监护条件:当事件发生时,监护条件为真,则发生转移;否则忽略该事件
-
动作:当转移发生时所执行的动作。
状态建模方法
状态建模主要考虑的三个问题:
-
哪些对象有重要的状态:识别一个要对其生命周期进行描述的参与行为的类
-
状态建模:如何确定一个对象可能的状态,并分析状态之间的转移,完成状态机模型
-
如何将状态机图(状态、事件与动作信息)映射到类模型的其它部分
状态机图中活动和动作的区别
活动(Activities)
-
关联于一个状态
-
在进入状态时开始
-
要花费时间完成
-
可中断的
动作(Actions)
-
关联于一个转移
-
是瞬间完成的
-
是不可中断的
状态机图映射到类模型的其它部分
状态机图中的事件信息可以映射成操作
- 类的操作应当使用状态的特定信息来更新
状态机图中的状态常常使用属性来表示
3.4、定义类的完整属性
类的属性是类的一个组成部分,描述了类在软件系统中所代表的一类事物的特征。
发现属性(attributes)的方法:
-
检查类自身需要维护的所有信息(事物特征)
-
检查方法描述
-
检查状态
属性的表示
指定名字、类型、可见性和可选的缺省值
- attributeName : Type = Default
设计时应遵循编程语言和项目团队的命名约定
类型应当是一个编程语言中基本数据类型
属性和操作的可见性
3.5、定义类之间的关系(重点)
类图中的关系及解释
关联关系及表示方法
-
描述了类的静态结构之间的关系,具有方向、名字、角色和多重性等信息。
-
一般的关联关系语义较弱。也有两种语义较强,分别是聚合与组合
关联类的设计
关联类用来描述关联关系本身的属性及行为
-
注意:面向对象编程语言不支持关联类的实现
-
设计时需要根据业务规则将关联类直接定义为普通的类,即将一个多对多的关系转变为两个一对多的关系。如图所示:
关联类(association class)
多重性(Multiplicity)设计
分析阶段定义多重性时,只设定了具体的重数
设计阶段,要考虑重数对实现的影响。如:
-
多重性
1
- 所链接的对象一定存在
-
多重性
0..1
- 所链接的对象也有不存在的情况
- 注意:需要添加判断链接的对象是否存在的操作
-
多重性
*
或n
- 实现时准备容器类。如,在java中可使用Vector类和List类等
多重性>1的设计方案
将关联关系精化为聚合/组合关系
聚合(Aggregation)关系和组合(Composition)关系都是一种特殊的关联关系
-
都由普通关联关系精化而来
-
都表示整体和部分的含义,即整体拥有部分
组合是聚合的一种特殊形式。整体和部分具有很强的归属关系或一致的生存期,即:
- 整体消失则部分也消失
类图中的关系及解释
聚合关系
特殊关联关系,指明一个聚集(整体)和组成部分之间的关系
示例
一台Computer可能连接到0..n台Printers
任何时候一台Printer连接到0..1台Computer
随着时间推移,许多台Computers可以使用一台给定的Printer
即使没有所连接的Computers,那台Printer也可以生存
即:Printer是独立于Computer的
聚合是关联的特例——描述了has a
的关系。
聚合图示方式:在表示关联关系的整体方直线末端加一个空心的小菱形,空心菱形紧挨着具有整体性质的类
组合关系
语义更强的聚合,部分和整体具有相同的生命周期——整体消失则部分也消失
示例
Button离开Mouse对象则不能独立存在
销毁Mouse则也销毁Button,因为它们是Mouse对象整体的一个部分
每个Button只能仅仅属于一个Mouse(如树和树叶)
组合关系是聚合关系中的复合聚合。如果构成整体类的部分类完全隶属于整体类,则这样的聚合称为复合聚合或组合。
实例1:聚合关系
汽车由轮胎、车身、发动机、底盘等聚合而成,当汽车报废时,轮胎等还可以单独存在。设计类图如何设计?
实例2:组合关系
人由头部、躯干、四肢、内脏等部分组成。它们的生命周期一致,当人出生时,头部、躯干、四肢、内脏同时诞生,当人死亡时,各组成部分同时死去。设计类图如何设计?
实现关系
对应于类和接口之间的关系——多态机制
实现是规格说明和其实现之间的关系,它将一种模型元素与另一种元素连接起来。
图示方式:符号与泛化关系的符号类似,空心三角箭头后为虚线 (注意:接口不同的构造型)。
依赖关系
依赖关系描述的是两个或多个模型元素(类、用例、包、子系统等)之间的语义上的连接关系。它是类之间耦合程度最低的一种关系,通过带有箭头的虚线来表示 ,箭头表明了依赖的方向
关联、实现和泛化都是依赖关系,但都有特殊的语义,所以在设计时被分离出来作为独立的关系
泛化关系
泛化采用继承机制,表示一个抽象的元素和一个具体的元素之间的关系。泛化可用于类、用例等各种模型元素
泛化关系设计方案(图书管理)
实例:确定类间的关系 ——网络购物系统