CS2 软件设计与数据结构

Chapter 2 Object Oriented Programming

| 关于   «  1. 面向对象程序设计导论   ::   目录   ::   3. 软件开发过程  »

2. 统一建模语言

2.1. 统一建模语言

统一建模语言(Unified Modeling Language,简称 UML)是一种用于描述和分析软件 设计的行业标准图形表示法。UML 中使用的符号和图形,源自 20 世纪 80 年代和 90 年代初为计算机辅助软件工程 (CASE) 制定标准的一系列努力。UML 代表了这些努力的 统一 。1994 至 1995 年间,建模语言发展中的几位领军人物——Grady Booch、 Ivar Jacobson 和 James Rumbaugh——试图统一他们各自的工作。他们认为,方法学的 碎片化阻碍了建模工具的商业化采用;为了消除这种碎片化,他们开发了 UML,为所有 工具厂商提供了一个公平竞争的环境。

UML 已被对象管理组织(Object Management Group,OMG) [1] 接受为标准。OMG 是 一个拥有约 700 名成员的非营利组织,为分布式面向对象计算制定标准。

UML 最初主要由 Booch、Jacobson 和 Rumbaugh(人称 三巨头 (the three amigos)) 的雇主 Rational Software 出资,该公司于 2002 年出售给 IBM。

软件模型 (software model) 是对软件系统某个方面的任何文本或图形表示,可以包括 需求、行为、状态,或者系统的安装方式。模型 不是 实际的系统,而是对待开发 系统不同方面的描述。UML 定义了一组可用于对系统建模的图及相应的规则。UML 中的 图一般分为两大类,或者说两种 视图 (view): 静态 视图与 动态 视图。

本课程提供的内容远谈不上是对 UML 的全面综述,其意图只是介绍理解本课程所讲 设计所需的基础知识。你在职业生涯中很有可能会遇到 UML 或与之非常相似的东西, 而且本课程使用的这些图不仅用于 UML,也用于其他建模系统 [2] [3] [4] 。

2.1.1. 静态图与动态图

静态图强调系统的静态结构,即系统中对象的属性、方法和关系。静态视图包括:

  • 类图

  • 部署图

在本课程中,我们主要关注类图。

动态图强调系统的动态行为,即系统的状态或模式,以及对象之间的协作。动态视图 包括:

  • 顺序图

  • 状态图

  • 用例图

2.1.2. 类图

类图 (class diagram) 是最常见的一类图。它描述系统中对象的类型,以及这些 对象之间存在哪几种静态关系。

在 UML 中,类表示为一个含有一层或多层水平分栏的矩形。按照惯例,类名以大写 字母开头。另一个惯例是:如果类是 AbstractClass 这样的抽象类,就把类名写成 斜体。顶部分栏存放类名,类名是类图中唯一必需的字段。矩形中间的分栏存放类的 属性列表,底部分栏存放方法列表。

属性和方法的可见性用位于类成员前面的单个字符来表示。静态成员则通过在成员名 下方加下划线来表示。UML 中静态成员的术语是 分类符成员 (classifier members)。

UML 中属性的语法是: visibility name : type = defaultValue

符号

可见性

+

公有

-

私有

#

受保护

/

派生

~

包

类图使用不同的记法标准来表示类的继承、类的组合以及其他关联关系。

继承关系

在 UML 中,继承关系被称为 泛化 (generalization)。

继承画成一条空心箭头,从子类指向父类 (superclass)。父类被视为子类的一种 泛化 (generalization),所以箭头指向父类是合理的。箭头想表达的意思是:子类 IS A 父类的一种类型。

在示例图中,有两个类继承自更一般的父类。规范并未明确要求必须把指向父类的线 合并为一组。一些 UML 绘图工具会把每条继承线画成一条独立的直线连到父类,这对 关系的含义没有任何影响。把关系画成一条合并的线,并不意味着这两个子类除了共享 一个共同祖先之外,还存在任何相互依赖。

实现关系

实现 (realization) 是两个模型元素之间的关系:其中一个模型元素(客户)实现 (执行)另一个模型元素(供应者)所指定的行为。

UML 中实现的图形表示是:在虚线(或多条线组成的线树)靠接口的一端画一个空心 三角形,这条虚线把接口连接到一个或多个实现者。而在把接口连接到其使用者的虚线 的接口一端,则使用普通箭头。

实现是类、接口、组件和包之间的关系,它把客户元素与供应者元素连接起来。类与 接口之间、组件与接口之间的实现关系表明:该类实现了该接口所提供的操作。

在本课程中,我们主要关注类之间的关系。注意 Person 类顶部新增的内容: <<interface>> 。尖括号定义的是一个 构造型 (stereotype)。构造型允许 UML 建模者扩展模型元素的词汇,或者更具体地说明某个模型元素的角色或用途。 在这里,构造型 <<interface>> 告诉我们:这并非一个普通的类,而是一个定义 了 接口 (interface) 的类。

请注意 泛化 (Generalization) 关系与 实现 (Realization) 关系之间的 相似之处。 泛化 建模的总是类之间的 继承 关系; 实现 建模的总是类 之间的 接口实现 关系。

关联

关联表示两个类之间的关系。两个类之间的关联用一条连接这两个类的线来表示。 关联意味着一个类使用了另一个类的属性或方法。如果线上没有箭头,就认为该关联 是双向的,也就是说,两个类都保存着关于对方类的信息。单向关联则用一个从持有者 对象指向被持有对象的箭头来表示。

关联是各类关系中最不特化的一种。当两个类各自拥有自己的生命周期、彼此独立时, 就使用关联。例如,两个类之所以相关,可能只是因为其中一个类(或两个都)把对方 作为某个方法的参数传入。

public class Author {
 
  public void write(Book b) { 
    // do something with the Book
  }
}

多重性

关联具有多重性 (multiplicity),有时也称基数 (cardinality),它表示在一个给定 的关系中,每个类的对象可以合法参与的个数。多重性用 n..m 记法表示,写在 关联线一端的附近,紧靠我们想展示其在关联中多重性的那个类。

这里 n 表示可能参与该关联的类实例的最小个数, m 表示这类实例的最大 个数。若 n = m ,则只显示 n 值。可选关系通过把最小个数写成 0 来 表示。通配符 * 用来表示 零个或多个 这一概念。

多重性取值示例

基数与强制性

多重性取值

一对一且强制 1

一对一且可选 0..1

一对多且强制 1..*

一对多且可选 *

下界为 l 且上界为 u l..u

下界为 l 且无上界 l..*

聚合

如果一个关联表达的信息是:一个对象是另一个对象的一部分,但二者的生命周期相互 独立(它们可以独立存在),那么这种关系就称为聚合 (aggregation)。

例如,一所大学拥有多个院系(如化学系),每个院系又有若干名教授。如果大学关闭 了,院系将不复存在,但这些院系里的教授们会继续存在。因此,可以把大学看作院系 的组合 (composition),而院系则是教授的聚合 (aggregation)。此外,一名教授可以 任职于多个院系,但一个院系不可能属于不止一所大学。例如:

public class Department {
 
  private Professor prof;
 
  public void setProf(Professor prof) { 
    this.prof = prof; 
  }
}

小技巧

请审慎使用聚合

UML 中没有几样东西比聚合 (aggregation) 与组合 (composition) 更让人 头疼了,尤其是它们与普通关联的区别。

事情的全貌被历史搅得混沌不清。在 UML 之前的方法学中,有一种常见的 记法,用来定义某种 “部分—整体” 关系。麻烦在于,每种方法学为 这些关系定义的语义各不相同(不过平心而论,其中有些关系本来就没多少 语义可言)。

所以到了要标准化的时刻,很多人都想要 “部分—整体” 关系,但对 它的含义却无法达成一致。于是 UML 引入了两种关系。

聚合 (白色菱形 ) 除了普通关联之外没有任何其他语义。正如

Jim Rumbaugh 所说,它是一剂建模安慰剂。人们可以、而且确实会使用它—但它并没有标准含义。我建议,若不附带某种形式的解释,就不要 自己使用它。

组合 (黑色菱形 ) 则确实带有语义。其中最特别的一条是:一个对象

只能处于一个组合关系之中。因此,即便窗口和面板都能容纳菜单栏,任何 菜单栏实例也只能被唯一的一个整体持有。这一约束用普通的多重性标记是 很难表达的。

-- Martin Fowler, AggregationAndComposition blog post ,2003 年 5 月 17 日。

组合

组合比聚合更加特化。与聚合一样,也是一个类 拥有 另一个类的一个实例,但 子类实例的生命周期依赖于父类实例的生命周期。换言之,父类消亡,子类也随之 消亡。

一个例子是 Car 和 Engine 两个类。创建一辆 Car 时,它会自带一台 Engine。 Engine 只有在 Car 存在的期间才能存在。此外,Engine 只为包含它的那辆 Car 服务—其他任何汽车都不能使用这台发动机。Car 被销毁时,Engine 也随之 销毁。例如:

public class Car {
 
  private Engine mopar = new Engine();
 
}


// an alternative method using constructor initialization 
public class Car {
 
  private Engine mopar;
 
  public Car() {
    mopar = new Engine();
  } 
}


// another alternative using lazy initialization
public class Car {
 
  private Engine mopar;
 
  public void buildEngine() {
    if (null == mopar) {
      mopar = new Engine();
    }
    mopar.doSomething();
  }
}

依赖关系

当对一个类的引用作为方法参数传入另一个类时,就表示为依赖 (dependency)。 例如,Book 类的一个实例被传给 Customer 类的一个方法:

public class Customer {
    
  public void purchase(Book b) {}
}

Customer 类需要 Book 类才能工作,但并不拥有它。purchase 方法的调用者 必须提供一个 Book 。

更多示例图和解释可以在 uml-diagrams.org 上查看。

   «  1. 面向对象程序设计导论   ::   目录   ::   3. 软件开发过程  »

关闭窗口