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
符号 |
可见性 |
|---|---|
|
公有 |
|
私有 |
|
受保护 |
|
派生 |
|
包 |
类图使用不同的记法标准来表示类的继承、类的组合以及其他关联关系。
继承关系
泛化的实际应用:
Students 和 Teachers 都是 People
在 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且上界为ul..u下界为
l且无上界l..*
聚合
如果一个关联表达的信息是:一个对象是另一个对象的一部分,但二者的生命周期相互 独立(它们可以独立存在),那么这种关系就称为聚合 (aggregation)。
聚合是 HAS A 关系的一种形式
例如,一所大学拥有多个院系(如化学系),每个院系又有若干名教授。如果大学关闭 了,院系将不复存在,但这些院系里的教授们会继续存在。因此,可以把大学看作院系 的组合 (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 上查看。
