UML 的图分成两大族:结构图(静态:有哪些东西、怎么组成)和行为图(动态:它们怎么动、怎么配合)。其中类图是面向对象设计的主力——它由一个三格方框(类名/属性/方法)搭起来,前面加符号表示可见性(- 私有、+ 公开、# 保护),方框之间用五种箭头表示关系(依赖 → 关联 → 聚合 → 组合 → 泛化,由弱到强)。看懂类图,就等于看懂了别人的设计图。
9.2.3 讲了封装/继承/多态三个概念,本节把它们变成图上的符号:-/+ 就是封装;那个空心三角箭头就是继承(UML 里叫"泛化");聚合/组合这两种关系,正是 9.2.3 说的"包含(has-a)"的两种强度。本节是本章最"符号密集"的一节,别急着记全,先把三格框 + 五种关系看懂,其余用到再查。
一、两大类图:静态与动态
| 结构图(静态) | 行为图(动态) | |
|---|---|---|
| 回答 | 系统里有哪些东西、它们怎么组成 | 这些东西怎么动、按什么顺序配合 |
| 好比 | 一张零件清单 + 装配图 | 一段操作录像 |
| 代表图 | 类图、对象图、包图、组件图、部署图 | 用例图、时序图、活动图、状态图、协作图 |
| 什么时候看 | 想搞清楚"系统由什么构成" | 想搞清楚"某个功能怎么跑的" |
记一个小口诀:结构图看"长什么样",行为图看"怎么跑"。 两类图常常配着用——先画类图说明有哪些角色,再画时序图说明这几个角色在一次"借书"里的来回调用。
二、类图的三格框:一个类的"身份证"
类图里,一个类就是一个方框,框内被横线分成三格:
┌─────────────────────────┐
│ Account │ ① 类名(放在最上面,习惯首字母大写)
├─────────────────────────┤
│ - balance : double │ ② 属性(有什么数据)
│ - owner : string │
├─────────────────────────┤
│ + deposit(amount: double) : void │ ③ 方法(会做什么)
│ + withdraw(amount: double) : void │
│ + getBalance() : double │
└─────────────────────────┘
每一格的写法规律:
| 格 | 写法 | 例子 | 读作 |
|---|---|---|---|
| 类名 | 名字,首字母大写 | Account |
—— |
| 属性 | 可见性 名称 : 类型 |
- balance : double |
私有的、叫 balance、类型 double |
| 方法 | 可见性 名称(参数) : 返回类型 |
+ deposit(amount: double) : void |
公有的、叫 deposit、收一个 double、不返回值 |
小提示:类型写在前还是写在后,看习惯(UML 标准是"名字在前、类型在后",用
:隔开)。你自己画草图时,写成double balance也没人拦你——图的目的是让人看懂,不是考试。
三、可见性符号:三把小锁
这一格符号就是 9.2.3 的"封装"落到图上:
| 符号 | 名称 | 含义 | 类比 |
|---|---|---|---|
+ |
public(公开) | 谁都能访问 | 大门——对外提供的服务 |
- |
private(私有) | 只有这个类自己能访问 | 私房钱——外人碰不到 |
# |
protected(保护) | 自己和子类能访问 | 家传手艺——本家人才会 |
~ |
package(包) | 同一个"包"内可见 | 同部门的内部资料 |
看图经验:一张合格的类图,属性那格通常是一排 -,方法那格通常是一排 +——因为数据要藏、服务要露。如果一张类图的属性全是 +,那它的"封装"基本是白讲的。
四、五种关系:从弱到强的"黏合度阶梯"
类图里最需要看懂的就是方框之间的连线。UML 里常用五种关系,按联系由弱到强排成一阶梯:
| 强度 | 关系 | 记号 | 大白话 | 代码里的样子 |
|---|---|---|---|---|
| 最弱 | 依赖(Dependency) | 虚线 + 开放箭头 - - -> |
"我临时用一下你" | 在某个方法里用到对方(参数、局部变量) |
| ↓ | 关联(Association) | 实线(可带箭头) | "我长期认识你" | 成员变量(一般是指针) |
| ↓ | 聚合(Aggregation) | 空心菱形 + 实线 | "你属于我,但你可以独立存在" | 成员指针,对象在别处创建 |
| ↓ | 组合(Composition) | 实心菱形 + 实线 | "你是我身上长出来的,我死你也死" | 成员指针,对象由我创建、由我销毁 |
| 最强 | 泛化(Generalization,即继承) | 空心三角箭头 + 实线 | "你是一种我"(is-a) | class 子 : public 父 |
菱形在哪一头,很关键:菱形画在"整体"那一边。
┌──────────┐ ┌──────────┐
│ Library │◆────────────>│ Book │ 组合:图书馆没了,书也没了
└──────────┘ └──────────┘
┌──────────┐ ┌──────────┐
│ Team │◇────────────>│ Player │ 聚合:球队散了,球员还在(可转会)
└──────────┘ └──────────┘
聚合 vs 组合,怎么一句话分辨——问"整体没了,部分还能独立活吗?"
| 例子 | 整体消亡时 | |
|---|---|---|
| 聚合(空心◇) | 球队 ↔ 球员 | 球队解散,球员还在(能去别的队) |
| 组合(实心◆) | 人 ↔ 心脏 | 人没了,心脏也没了(不能独立存在) |
三种关系里最常被搞混的一对:"关联"(只是认识)vs "聚合"(算是我的)——比如"学生 ↔ 课程":学生选了课程,但课程不属于学生,所以是关联;而"班级 ↔ 学生",学生属于这个班,就是聚合。
五、多重性:一端可以有几个
在线条两端写数字,表示这条关系两端各有多少个对象:
| 写法 | 含义 | 例子 |
|---|---|---|
1 |
正好一个 | 一个班有一个班主任 |
0..1 |
零或一个 | 一个学生可以有零或一个导师 |
* 或 0..* |
零个或多个 | 一个班有零或多个学生 |
1..* |
至少一个 | 一个订单至少有一条明细 |
3..7 |
范围内 | 可指定区间 |
┌───────┐ 1 * ┌───────┐
│ Class │────────────>│Student│ 读作:一个班级有零或多个学生
└───────┘ └───────┘
六、怎么读一张类图(现场演示)
┌──────────────────────┐ 1 ┌──────────────────────┐
│ Library │◇────────────────│ Book │
├──────────────────────┤ * ├──────────────────────┤
│ - name : string │ │ - title : string │
│ - books : Book[] │ │ - author : string │
├──────────────────────┤ │ - onShelf: bool │
│ + addBook(b: Book) │ ├──────────────────────┤
│ + find(title) : Book │ │ + borrow() : bool │
└──────────────────────┘ │ + giveBack() : void │
△ └──────────────────────┘
│ 泛化 △
│ │ 泛化
┌──────────────────────┐ ┌──────────────────────┐
│ SchoolLibrary │ │ EBook │
├──────────────────────┤ ├──────────────────────┤
│ - campus : string │ │ - fileSize : double │
└──────────────────────┘ └──────────────────────┘
逐句读出来(这就是"会读类图"的意思):
- 系统里有四个类:
Library、Book、SchoolLibrary、EBook。 Book内部有三个属性,其中onShelf(在架状态)是私有的(-);对外提供borrow()和giveBack()两个方法(+)——数据藏、服务露,标准的封装。- 一个
Library里可以有零或多个Book(1对*),而且是组合(实心◆)——图书馆不存在了,它收藏的书也就随之销毁。 SchoolLibrary是一种Library(空心三角箭头向上,泛化/继承)——它自动拥有name、books和那两个方法,只额外加了campus。EBook是一种Book——它继承title、author和borrow()/giveBack(),多出一个fileSize。books : Book[]这个属性写在Library里,它就是"组合"关系的代码体现——图上的菱形,和代码里的成员变量,是同一件事的两种说法。
一句话总结读法:先看有几格(类名/属性/方法),再看每条线的"头"(符号表示关系种类)和"尾"(数字表示数量),最后看线指向谁。
七、动手 Demo:把"图书"画成类图
不写代码,动手就是画。 参照上面的读法,自己把下面这三个类画出来(纸笔或文本方框都行):
需求:图书馆有若干本图书;一个读者可以借多本书(最多 5 本)。
要画的类与关系:
| 类 | 属性 | 方法 |
|---|---|---|
Reader(读者) |
- name : string、- id : string |
+ borrow(b: Book) : bool、+ giveBack(b: Book) : void |
Book(图书) |
- title : string、- onShelf : bool |
+ borrow() : bool、+ giveBack() : void |
Library(图书馆) |
- books : Book[] |
+ addBook(b: Book) : void |
连线要求:
1. Library ◆—— * Book:组合(图书馆没了,书也没了)
2. Reader ——* Book:关联(读者借的书不属于读者,是临时持有)
3. 在 Reader 那端标上多重性 1,在 Book 那端标上 *。
看到什么算成功:你能画出一个组合(实心菱形)和一个关联(普通实线),并说清为什么一个用菱形、一个不用。
30 秒实验(关键辨析):把"读者借书"改成"读者买书"——这时关系会变吗?
→ 会。买下的书属于这位读者了(别人不能借走同一本),整体-部分的意味出来了,可以升级成聚合(空心◇)。"同一个动作(借/买),关系的类型却不同"——这就是类图的价值:它逼你把"到底谁拥有谁"这件事想清楚。
八、常见误区
- "类图上所有属性都得是
-"——方向对,但不是死规矩。有些简单的值(比如Point的 x/y)直接公开也很常见;关键看有没有"必须走方法才能保证的规则"(比如余额不能为负)。 - "泛化 = 组合"——完全不同。泛化是"是一种"(is-a,空心三角);组合是"是我的部分"(has-a,实心菱形)。 把 has-a 画成继承是最典型的设计错误(9.2.3 讲过)。
- "聚合和组合差不多,随便挑一个"——不行。判据是"整体消亡时部分能否独立存在":能独立 → 聚合(空心◇);不能 → 组合(实心◆)。
- "多重性的
*表示"很多",具体多少无所谓"——*是"零或多个",如果需求是"至少一个",必须写1..*。这一笔差别在需求评审里经常是重点。 - "画了类图,代码必须严格照图写"——不必。类图是设计意图的表达,实现时会有细节取舍(比如为了性能把两个类合并)。图可以比代码"粗",但结构意图应当一致。
- "类图里画了方法就一定要写"——不是。类图上列的方法常常是设计阶段的意图,实现时会增删。图是给人讨论用的,不是给编译器看的。
自测三问(点开对答案)
1. 类图的方框分哪三格?可见性符号各表示什么?——**类名/属性/方法**三格。符号:**`+` 公开、`-` 私有、`#` 保护(子类可见)、`~` 包内可见**。 2. 聚合和组合怎么区分?各用什么记号?——**问"整体没了,部分还能独立存在吗"**:能独立 → **聚合**(**空心菱形◇**,如球队与球员);不能 → **组合**(**实心菱形◆**,如人与心脏)。**菱形都画在"整体"那一端。** 3. 泛化关系在图上是什么样?它对应前面学过的哪个概念?——**一条实线,末端是空心三角箭头,指向父类**。它对应 **9.2.3 的"继承"**,判断标准是**"是一种"(is-a)**。- ☐ 9.3.3 面向对象的建模(下一节)
相关笔记
- 统一建模语言概述 —— 上一节:UML 的来历与定位
- 面向对象的建模 —— 下一节:从需求走到这张图
- 面向对象的特点 —— 图上的符号对应的概念:封装/继承
- 结构体的嵌套 —— "包含"在代码里长什么样
- 🔷 C++基础 · 索引 —— 全课路线图