🔷 第九章 面向对象编程基础
📄 一句话概括

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  │
        └──────────────────────┘                 └──────────────────────┘

逐句读出来(这就是"会读类图"的意思):

  1. 系统里有四个类:Library、Book、SchoolLibrary、EBook。
  2. Book 内部有三个属性,其中 onShelf(在架状态)是私有的(-);对外提供 borrow() 和 giveBack() 两个方法(+)——数据藏、服务露,标准的封装。
  3. 一个 Library 里可以有零或多个 Book(1 对 *),而且是组合(实心◆)——图书馆不存在了,它收藏的书也就随之销毁。
  4. SchoolLibrary 是一种 Library(空心三角箭头向上,泛化/继承)——它自动拥有 name、books 和那两个方法,只额外加了 campus。
  5. EBook 是一种 Book——它继承 title、author 和 borrow()/giveBack(),多出一个 fileSize。
  6. 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 秒实验(关键辨析):把"读者借书"改成"读者买书"——这时关系会变吗?

→ 会。买下的书属于这位读者了(别人不能借走同一本),整体-部分的意味出来了,可以升级成聚合(空心◇)。"同一个动作(借/买),关系的类型却不同"——这就是类图的价值:它逼你把"到底谁拥有谁"这件事想清楚。

八、常见误区

  1. "类图上所有属性都得是 -"——方向对,但不是死规矩。有些简单的值(比如 Point 的 x/y)直接公开也很常见;关键看有没有"必须走方法才能保证的规则"(比如余额不能为负)。
  2. "泛化 = 组合"——完全不同。泛化是"是一种"(is-a,空心三角);组合是"是我的部分"(has-a,实心菱形)。 把 has-a 画成继承是最典型的设计错误(9.2.3 讲过)。
  3. "聚合和组合差不多,随便挑一个"——不行。判据是"整体消亡时部分能否独立存在":能独立 → 聚合(空心◇);不能 → 组合(实心◆)。
  4. "多重性的 * 表示"很多",具体多少无所谓"——* 是"零或多个",如果需求是"至少一个",必须写 1..*。这一笔差别在需求评审里经常是重点。
  5. "画了类图,代码必须严格照图写"——不必。类图是设计意图的表达,实现时会有细节取舍(比如为了性能把两个类合并)。图可以比代码"粗",但结构意图应当一致。
  6. "类图里画了方法就一定要写"——不是。类图上列的方法常常是设计阶段的意图,实现时会增删。图是给人讨论用的,不是给编译器看的。
自测三问(点开对答案) 1. 类图的方框分哪三格?可见性符号各表示什么?——**类名/属性/方法**三格。符号:**`+` 公开、`-` 私有、`#` 保护(子类可见)、`~` 包内可见**。 2. 聚合和组合怎么区分?各用什么记号?——**问"整体没了,部分还能独立存在吗"**:能独立 → **聚合**(**空心菱形◇**,如球队与球员);不能 → **组合**(**实心菱形◆**,如人与心脏)。**菱形都画在"整体"那一端。** 3. 泛化关系在图上是什么样?它对应前面学过的哪个概念?——**一条实线,末端是空心三角箭头,指向父类**。它对应 **9.2.3 的"继承"**,判断标准是**"是一种"(is-a)**。

相关笔记