面向对象分析与设计方法

面向对象设计

软件工程 第三部分 第8章,主讲人:李欣

Created: 2024-03-05 Tue 16:42

0.1. 互动课堂

Click to host the seminar.

0.2. 签到

https://dash.memopixel.com/tool/attendance/

0.3. 本次课的目标

  • 第三部分:面向对象分析与设计方法
    • 第8章:面向对象设计
      • 面向对象设计过程与准则
      • 体系结构模式及依赖性
      • 系统分解
      • 问题域部分的设计
      • 人机交互部分的设计
      • 任务管理部分的设计
      • 数据管理部分的设计
      • 对象设计
      • 优化对象设计模型
面向对象设计的主要任务
  • 面向对象分析 的基础上完成
    1. 体系结构设计
    2. 接口设计(或人机交互设计)
    3. 数据设计
    4. 类设计
    5. 构件设计

由于在类中封装了 属性方法 ,因此在类设计中已经包含了传统方法中的 过程设计

1. 面向对象设计过程与准则

1.1. 面向对象设计过程

  • 建立 系统 环境模型
  • 设计 系统 体系结构
  • 对各个 子系统 进行 设计
  • 对象 设计优化

1.1.1. 建立系统环境模型

与目标系统交互的系统
  • 上级系统 :把 目标系统 作为某些高层处理方案的 一部分
  • 下级系统 :被 目标系统 使用,并为了完成目标系统的功能提供必要的 数据处理
  • 同级系统 :在 对等 的基础上 相互作用
  • 参与者 :通过产生和使用信息,实现 与目标系统交互 的实体。

system-environment-diagram.png

Figure 1: 系统环境图

每个 外部实体 都通过某一 接口 (带阴影的小矩形)与 目标系统 进行通信。

1.1.2. 设计系统体系结构

  • 自底向上 :将 关系紧密 的对象组织成 子系统
  • 自顶向下 : 尤其是使用 设计模式遗产系统 时,会从子系统的划分入手。

根据客户的需求选择 体系结构 风格,之后对可选的体系结构 风格模式 进行分析。 以导出最适合 客户需求质量属性 的结构。

体系结构设计 往往要经过多次 反复迭代求精 才能得到满意的结果。

1.1.3. 对各个子系统进行设计

对于 面向对象的系统 ,典型的子系统有:

  • 问题域 子系统
  • 人机交互 子系统
  • 任务管理 子系统
  • 数据管理 子系统

1.1.4. 对象设计及优化

对象设计 以问题领域的对象设计为核心,其结果是一个详细的 对象模型

对象设计过程包括的几个方面
  • 使用模式设计对象
  • 接口规格说明
  • 对象模型重构
  • 对象模型优化活动

1.2. 面向对象设计准则

模块化
  • 面向 过程 方法中的模块
    • 函数过程子程序
  • 面向 对象 方法中的模块
    • 对象接口构件
抽象
  • 过程 抽象
  • 数据 抽象( 实际上就是一种抽象数据类型)
信息隐藏
  • 通过对象的封装性实现
  • 属性 的表示方法,以及 操作 的实现算法
弱耦合
  • 对某一部分的理解、测试或修改,无须涉及系统的其他部分
强内聚
  • 服务内聚: 一个 服务 应该完成一个且仅完成一个 功能
  • 类内聚: 一个 应该只有一个 用途 ,它的 属性服务 应该是高内聚的
  • 一般-特殊内聚:对相应的 领域知识 的正确抽取
可重用

2. 体系结构模式及依赖性

体系结构设计 描述了建立计算机系统所需的 数据结构程序构件

一个好的体系结构设计要求
  • 软件模块的分层 : 不允许非相邻层间的构件进行直接交互,因此降低了结构的复杂性, 使模块间的 依赖关系 更容易理解。
  • 编程标准的执行 : 在编译过程中,模块之间的 依赖性 清晰可见, 并且禁止使用运行时程序结构混乱不清的编程方案。

2.1. 类及其依赖性

2.1.1.

面向对象 的程序设计中, 接口 是程序的基本组成单元。

一个典型程序需要
  • 界面类 负责 表示用户界面信息
  • 数据库类 负责 与数据库进行交互
  • 业务逻辑类 负责 算法计算
  • ……

在计算机程序中,要设计和实现所有类都具有唯一的名字, 在 不同的阶段 或从 不同的角度 可以将它们称为
设计类实现类系统类应用类 等。

2.1.2. 继承依赖性

多态继承
  • 编译 时继承依赖性
  • 运行 时继承依赖性
无多态继承
  • 子类不覆盖从父类继承来的方法,则 不存在 多态性继承问题。
扩展继承
  • 子类继承父类的 属性
  • 并且提供额外 属性 来增强类定义。
约束继承
  • 一个类覆盖了继承来的 方法
  • 并对一些继承来的 功能 进行了限制。

2.1.3. 交互依赖性

  • 交互依赖性 也称为 方法依赖性 ,是通过消息连接产生的。
  • 类之间的依赖性相当于类成员之间的依赖性。
%%{init: { 'theme': 'forest', 'fontFamily': 'DejaVu Sans' }}%%
classDiagram
    class CActioner {
      emp:EEmployee
      do1()
    }
    class EEmployee {
      do3()
    }
    class EOutMessage {
      emp:EEmployee
      do2()
    }
    CActioner "0..n" --> "0..1" EEmployee
    CActioner ..> EEmployee
    EOutMessage "0..n" --> "0..1" EEmployee
    EOutMessage ..> EEmployee

cross-dependency.svg

Figure 2: 交互依赖性

public class CActioner {
    EEmployee emp;
    public void do1() {
        emp.do3();
    }}
public class EOutMessage {
    EEmployee emp;
    public void do2() {
        emp.do3();
    }}

2.2. 接口及其依赖性

2.2.1. 接口

接口
  • 是不可直接实例化的特性集合的声明,
  • 即其对象不能直接实例化,需要通过类来实现,
  • 实现接口的类需要实现接口中声明的方法。

接口抽象类 有相似之处。

抽象类
  • 是至少包含一个 没有实现的方法
  • 如果在一个抽象类中 所有的方法都没有实现 ,则称其为 纯抽象类

2.2.2. 实现依赖性

Class1 实现了 Interface1Interface2 ,而 Class2 只实现了 Interface2

%%{init: { 'theme': 'forest', 'fontFamily': 'DejaVu Sans' }}%%
classDiagram
    class Interface1 {
      <<interface>>
      a1
      op1()
    }
    class Interface2 {
      <<interface>>
      a2
      op2()
    }
    class Class1
    class Class2
    Interface1 <|.. Class1
    Interface2 <|.. Class1
    Interface2 <|.. Class2

realization-dependency.svg

Figure 3: 实现依赖性

2.2.3. 使用依赖性

%%{init: { 'theme': 'forest', 'fontFamily': 'DejaVu Sans' }}%%
classDiagram
    class Interface1 {
      <<interface>>
      a1
      op1()
    }
    class Interface2 {
      <<interface>>
      a2
      op2()
    }
    class Class1 {
      do1()
    }
    Interface1 --> Interface2 : 《uses》
    Interface1 <-- Class1 : 《uses》

use-dependency.svg

Figure 4: 使用依赖性

public class Class1 {
    Interface1 myInterface;
    public void do1() {
        myInterface.op1();
    }
}

Class1 使用 Interface1Interface1 使用 Interface2

在Java语言中,

  • 不允许 接口之间的使用
  • 只允许 接口间的扩展继承

2.3. 包及其依赖性

2.3.1.

当系统中涉及的类的数量比较多时,往往会将关系紧密的类组织到 包(package) 中。 按照UML的定义, 包是一组命名的建模元素集合。一个包可能包含其他包。

在UML中,包是一个 逻辑设计 概念。最终,包必须 实现映射 为编程语言。 Java和C#语言提供了包 概念实现 的直接映射。 通过类的名字空间和引入其他包的形式来支持实现包。

如果包A的一些成员在某种程度上引用了包B的某些成员(包A导入了包B的一些成员), 这隐含着双重含义:

  • 包B的变化可能会影响包A,通常需要对包A重新进行编译和测试;
  • 包A只能和包B一起使用。

2.3.2. 包依赖性

本质上,两个 包之间的依赖性 来自于两个包中 类之间的依赖性

  • 类之间的 循环依赖性 是个特别棘手的问题。

circular-dependencies-between-packages.png

Figure 5: 包之间的循环依赖性

eliminate-circular-dependencies-between-packages.png

Figure 6: 消除包之间的循环依赖性

2.4. 构件及其依赖性

2.4.1. 构件的介绍

传统的 构件 也称为 模块 ,是软件体系结构的一部分。

  • 控制构件 :对问题域中所有其他构件的调用;
  • 问题域构件 :完成部分或全部用户的需求;
  • 基础设施构件 :负责完成问题域中所需相关处理的功能。

在面向对象的软件工程环境中,面向对象技术已达到了类级复用,这样的复用粒度还太小。 构件 级复用则是比 级复用更高一级的复用。

构件级复用
  • 对一组 的组合进行封装,
  • 并代表完成一个或多个功能的特定 服务 ,也为用户提供了多个 接口
  • 通过构件隐藏了具体的 实现 ,只用接口对外提供 服务

2.4.2. 构件的特性

为了能够支持复用,软件构件应具有以下特性:

  • 独立部署单元 :构件是独立部署的,它必须能与它所在的环境及其他构件完全分离。
  • 作为第三方的组装单元 :构件必须封装它的实现,并且只通过良好定义的接口与外部环境进行交互。
  • 构件不能有任何(外部的)可见状态 :这要求构件不能与自己的拷贝有所区别。

在目前的很多系统中,构件被实现为更大粒度的单元,系统中的构件只能有一个实例。

例如,一个数据库服务器可以作为构件, 而它所管理的数据库(可以是一个,也可以是多个)并不是构件, 而是数据库“对象”实例。

2.4.3. 构件的定义

1996面向对象程序设计欧洲会议上面向构件程序设计组给的定义

软件构件 是一种 组装单元 ,它具有规范的 接口规格说明 和显式的 语境依赖 。 软件构件可以被 独立部署 ,并由第三方 任意组装

OMG UML规范中构件的定义

系统中某一 定型化的可配置的可替换的 部件 , 该 部件 封装了 实现 并暴露一系列 接口

上面的两个定义中都提到接口的概念,构件之间是通过接口相互连接的。
接口是可被客户访问的具体操作的集合,每个操作有规定的语义。

2.4.4. 构件图

构件图 表示构件之间的 依赖关系 , 每个构件 实现(支持) 一些接口,并 使用 另一些接口。

dependencies-between-components.png

Figure 7: 构件之间的依赖关系

3. 系统分解

3.1. 子系统和类

在大型和复杂的软件系统中,首先根据需求的 功能模型(用例模型)

  • 系统 分解成若干个 部分
  • 每一 部分 又可分解为若干个 子系统
  • 每个 子系统 还可以由 更小的子系统 组成。

system-structure-class-diagram.png

Figure 8: 系统结构的类图

3.2. 服务和子系统接口

服务
  • 是一组有 公共目的 的相关 操作
  • 子系统通过给其他子系统提供 服务 来发挥自己的能力。
子系统接口
  • 是供其他子系统调用的某个子系统的 操作 集合。
  • 子系统接口包括 操作名操作参数类型返回值

面向对象的系统设计 主要关注 每个子系统提供服务的定义 , 即枚举所有的 操作操作参数行为

因此,当编写子系统接口的文档时,应 不涉及 子系统 实现 的细节, 其目的是 减少 子系统之间的 依赖性

3.3. 子系统分层和划分

子系统分层 的目的是建立系统的 层次结构

在一个系统的层次结构中,如果
  • 每一层只能访问与其相邻的下一层,称为 封闭体系结构
  • 每一层还可访问比其相邻下一层更低的层次,称为 开放体系结构

closed-architecture-example.png

Figure 9: 封闭体系结构示例

open-architecture-example.png

Figure 10: 开放体系结构示例

3.4. Coad & Yourdon 的
面向对象设计模型

  • Coad & Yourdon 基于 MVC(Model-View-Controller)模型 , 在 逻辑上 将系统划分为4个部分,每一部分又可分为若干个 子系统
  • Coad & Yourdon 在设计阶段中继续采用了 分析阶段 中提到的 5个层次 , 用于建立系统的4个组成部分。

typical-object-oriented-design-model.png

Figure 11: 典型的面向对象设计模型

3.5. 子系统之间的两种交互方式

在软件系统中,提供服务的一端称为 服务器端 ,而将使用服务的一端称为 客户端

子系统之间的两种交互方式
  • 客户-供应商关系客户子系统 调用 供应商子系统 ,后者完成某些服务工作并返回结果。
  • 平等伙伴(peer-to-peer)关系每个子系统 都可能调用 其他子系统 , 因此每个子系统都必须了解其他子系统的接口。

单向交互双向交互 更容易理解,也更容易设计和修改, 因此,应该尽量使用 客户-供应商关系

3.6. 组织系统的两种方案

子系统组织成完整系统时的两种方案
  • 分层组织 :这种组织方案把软件系统组织成一个 层次系统 ,每层是一个子系统。
  • 块状组织 :这种组织方案把软件系统垂直地分解成若干个 相对独立的弱耦合 的子系统, 一个子系统相当于一块,每块提供一种类型的服务。

混合使用 层次结构块状结构 ,可以成功地由多个子系统组成一个完整的软件系统。

当混合使用层次结构和块状结构时, 同一层次可以由若干块组成 ,而 同一块也可以分为若干层

4. 问题域部分的设计

组成典型面向对象系统的三层
  • 数据库层
  • 业务逻辑层
  • 用户界面层
那么,首先从哪一层开始设计呢?

参与人数 0
数据库层 0
业务逻辑层 0
用户界面层 0
, 数据库层 , 业务逻辑层 , 用户界面层 , 0 , 0 , 0

面向对象的分析模型 包括有 用例图类图顺序图包图 , 主要是对问题领域进行描述,基本上不考虑 技术实现 ,也不考虑 数据库层用户界面层

  • 面向对象分析 所得到的 问题域模型 可以直接应用于系统的 问题域部分的设计
  • 面向对象设计 过程中,可能对 面向对象分析 所得出的 问题域模型 做以下方面的补充或调整。
    1. 调整需求
    2. 复用已有的类
    3. 把问题域类组合在一起
    4. 增添泛化类以建立类间的协议
    5. 调整继承的支持级别
    6. 改进性能
    7. 存储对象

5. 人机交互部分的设计

用户界面(即人机交互界面) 是人机交互的主要方式, 用户界面的质量直接影响到用户对软件的使用, 对用户的 情绪工作效率 也会产生重要影响, 也直接影响用户对软件产品的评价,从而影响软件产品的 竞争力寿命

在设计阶段必须根据需求把 交互细节 加入到用户界面设计中, 包括人机交互所必需的 实际显示输入

5.1. 用户界面设计步骤

  • 从系统的 输入输出 及与用户的 交互 中获得信息, 定义 界面对象行为(操作)
  • 定义 那些导致用户界面状态发生变化的 事件 ,对 事件建模
  • 描述 最终向用户展示的每个 界面的状态
  • 简要 说明 用户 如何从 界面提供的 界面信息解释 系统状态

在某些情况下,界面设计师可以从每个界面 状态草图 开始, 如在 各种环境下 用户界面看起来是个什么样子; 接下来定义 对象动作 和其他重要的设计信息。

5.2. Web应用系统的界面设计

5.2.1. 界面设计目标

无论应用的 领域规模复杂度 如何, 界面设计目标都应能在相应的Web应用系统中适用。

Web设计的定期专栏中 Jean Kaiser 提出的设计目标
  1. 简单性 :尽量做到 适度简单 ,不要在页面上提供太多的东西。
  2. 一致性内容构造页面风格 应该一致,界面设计应该定义一致的 交互导航内容显示模式
  3. 确定性 :Web应用系统的 美学界面导航设计 必须与将要构造的应用系统所处的领域保持一致。
  4. 健壮性 :用户期待与他们的要求相关的 健壮的内容功能
  5. 导航性 :应该以 直观的可预测的 方式来设计。
  6. 视觉吸引 :Web应用系统需考虑 视觉效果设计特性审美
  7. 兼容性 :Web应用系统需考虑应用于 不同的环境 ,并且必须 互相兼容

5.2.2. 界面设计工作流程

Web应用系统界面设计的基本工作流程
  1. 回顾那些在分析模型中的信息,并根据需要进行优化。
  2. 开发Web应用系统界面的草图。
  3. 将用户目标映射到特定的界面行为。
  4. 定义与每个行为相关的一组用户任务。
  5. 为每个界面行为设计情节串联图版图像。
  6. 利用从美学设计中的输入来优化界面布局和情节串联图板。
  7. 明确实现界面功能的界面对象。
  8. 开发用户与界面交互的过程表示。
  9. 开发界面的行为表示法。
  10. 描述每种状态的界面布局。
  11. 优化和评审界面设计模型。

6. 任务管理部分的设计

对任务及任务管理的理解
  • 任务进程 的别称,是执行一系列活动的一段程序。
  • 当系统中有许多并发行为时,需要依照各个行为的协调和通信关系, 划分各种任务,以简化并发行为的设计和编码。
  • 任务管理 主要包括任务的 选择调整
常见的任务
  • 事件驱动型任务
  • 时钟驱动型任务
  • 优先任务
  • 关键任务
  • 协调任务
  • ……
设计任务管理子系统时的具体任务
  1. 确定 各类任务,并将任务分配给适当的硬件或软件去执行。
  2. 识别 事件驱动任务
  3. 识别 时钟驱动任务
  4. 识别 优先任务
    1. 高优先级 :为了在严格限定的时间内完成这种服务,可能需要把这类服务分离成独立的任务。
    2. 低优先级 :设计时可能用额外的任务把这样的处理分离出来。
  5. 识别 关键任务
  6. 识别 协调任务
  7. 审查 每个任务
  8. 定义 每个任务
    1. 首先要 为任务命名 ,并 对任务做简要描述 。为面向对象设计部分的每个服务增加一个新的约束,即 任务名
    2. 定义每个任务 如何协调工作 。指出它是 事件驱动 的,还是 时钟驱动 的。
    3. 定义每个任务 如何通信 ,任务 从哪里取数据往哪里送数据

7. 数据管理部分的设计

与结构化设计的对比
  • 在传统的 结构化设计 方法中,很容易将 实体-关系图 映射到 关系数据库 中。
  • 而在 面向对象设计 中,我们可以将UML 类图 看作 数据库概念模型 , 但在UML类图中除了类之间的 关联 关系外,还有 继承 关系。

一个普通的类可以映射为 一个 多个 , 当分解为多个表时,可以采用 横切竖切 的方法。

  • 横切 常用于 记录与时间相关的对象 ,如成绩记录、运行记录等。
  • 竖切 常用于 实例较少而属性很多的对象 ,一般是现实中的事物,将不同分类的属性映射成不同的表。
关联关系的映射
  • 一对一关联的映射 :可以在两个表中都引入外键,这样两个表之间可以进行双向导航。
  • 一对多关联的映射 :可以将关联的“一”端毫无变化地映射到一张表,将“多”端上的类映射到带有外键的另一张表。
  • 多对多关联的映射 :引入一个关联表,将两个类之间的多对多关联转换成表上的两个一对多关联。
继承关系的映射
  • 单表继承 :为基类及其子类建立一张表。适用于 子类属性比较少 的情况。
  • 子类表继承 :将每个子类映射到一张表,没有基类表。在每个子类的表中包括基类的所有属性。 适用于 子类的个数不多、基类属性比较少 的情况。
  • 所有类表继承 :将基类映射到一张表,每个子类都映射为一张表。 在基类对应的表中定义主键,而在子类对应的表中定义外键。

8. 对象设计

对象设计问题域的对象设计 为核心,其结果是一个 详细的对象模型

经过多次反复的 分析概要设计 之后,设计者通常会发现有些内容没有考虑到。 这些没有考虑到的内容,会在 对象设计 的过程中被发现。

对象设计过程包括
  • 使用模式设计对象 :设计者可以选择 合适的设计模式复用 已有的解决方案, 以提高系统的 灵活性 ,并确保在系统开发过程中,特定类不会因要求的变化而被修改。
  • 接口规格说明 :在系统设计中所标识的子系统功能,都需要在类接口中详细说明, 包括 操作参数类型规格说明异常情况 等。
  • 对象模型重构 :重构的目的是改进对象设计模型,提高该模型的 可读性扩展性
  • 对象模型优化 :优化活动是为了改进对象设计模型,以实现系统模型中的 性能 要求。

8.1. 使用模式设计对象

在面向对象设计过程中, 设计模式 是开发者通过很长时间的实践而得到的重复出现问题的 模板化解决方案

设计模式包括的4个要素
  • 名字 :用来将一个设计模式 与其他设计模式区分开
  • 问题描述 :用来描述该设计模式 适用于何种情况 。 通常设计模式所解决的问题是对 可更改性可扩展性设计目标 以及 非功能性需求 的实现。
  • 解决方案 :描述解决该问题 所需要的结合在一起的类接口 的集合。
  • 结果 :描述将要解决设计目标的 协议可供选择的办法

8.2. 接口规格说明设计

接口规格说明包括的活动
  • 确定遗漏的属性和操作
    • 在这个活动中,将检查 每个子系统提供的服务每个分析对象
    • 标识出 被遗漏的操作属性
  • 描述可见性和签名
    • 在这个过程中,将决定哪个操作 对其他对象和子系统是可用的
    • 哪个操作 只对本子系统是可用的
    • 并说明操作的签名(包括 操作名参数名 )。
  • 描述契约
    • 描述每个对象操作应该遵守的 约束条件

8.3. 重构对象设计模型

典型的重构活动的例子包括
  • N元关联 转换成 一组二元关联
  • 两个不同子系统中相似的类 合并为 一个通用的类
  • 没有明显活动特征的类 转换为 属性
  • 复杂类 分解为 几个相互关联的简单类
  • 重新组合 类和操作 ,增加 封装性继承性

9. 优化对象设计模型

几种常用的优化方法
  • 增加冗余关联以提高访问效率
  • 调整查询次序
  • 保留派生属性

10. 课后作业

  1. 习题8.1 软件模块之间的依赖性可以从哪些角度和抽象层次进行分析?
  2. 习题8.2 消除包之间循环依赖性的方法是什么?
  3. 习题8.3 典型的面向对象设计模型在逻辑上由哪几部分组成? 对每一部分进行设计时所包含的主要内容是什么?