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

建模 = 在写代码之前,先动手把"系统里有什么、各自会什么、谁跟谁有关系"想清楚并画出来。 它的流程只有四步:找对象 → 定属性 → 定行为 → 定关系。找对象的窍门是从需求里的"名词"里挑候选、从"动词"里找方法。画完之后要自问三句:职责清不清?关系对不对?有没有多余的类? 建模不是画完就完——代码变了,图要跟着改。

📝 本节和前面几节的关系

9.3.2 教了"类图长什么样、怎么读",本节回答"这张图是怎么从需求变出来的"——它是 9.2(对象思路)+ 9.3.2(图的符号)的落地流程:9.2.2 说"第一步是把名词圈出来",本节把这个动作变成一套可重复的四步法。学完这一节,你就有了从"一句话需求"走到"一张类图"的完整能力——这正是动手写 class 之前该走完的路。

一、四步建模法

一张类图从来不是"想出来的",而是一步步推出来的。四个步骤,顺序不能乱:

步骤 做什么 关键问题
① 找对象 从需求里挑出候选的类 系统里有哪些角色?
② 定属性 每个类自己要知道什么 它的状态由哪几项组成?
③ 定行为 每个类自己要会做什么 哪件事主要改自己的数据?
④ 定关系 类之间怎么连、连几条、多强 是"是一种"还是"有一个"?整体没了部分还在吗?

为什么顺序不能乱:先想"谁"、再想"它有什么"、再想"它会什么"、最后想"它们怎么连"。反过来(先定关系)会陷入"这两个类该不该连"的死循环,因为你还不知道它们各自是什么。

二、第一步:找对象——从"名词"里挑候选

这是最关键也最容易做过头的一步。方法很朴素:

把需求描述里的名词(以及名词短语)圈出来,它们就是"类的候选名单"。

例(一句话需求):

"读者拿着借书证到图书馆借一本图书,管理员登记一条借阅记录。"

圈出的名词:读者、借书证、图书馆、图书、管理员、借阅记录。

候选名单有了,但还不能全要——要过一遍筛子:

筛子 问题 本例判断
是系统里的角色/事物吗? 还是只是一句话的修饰? 管理员——如果系统只是自助借还,就不是类(是人不是软件对象)
它有数据或行为要管吗? 纯粹的值不值得建类? 借书证——只有编号的话,可能只是"读者"的一个属性
它是不是多余的? 能不能并入别的类? 借阅记录——要留!它有"借出日期、应还日期",是独立的数据

筛完的结果:读者、图书馆、图书、借阅记录(4 个类)。

永远问一句:"这个类真的需要吗?" 多一个类,就多一份要维护的东西;少一个类,就会出现"某个类什么都管"的怪物。这一步的判断力,就是设计经验的核心。

三、第二、三步:定属性、定行为

定属性——问"它自己要知道什么":

类 属性(问:它的"状态"由什么决定?)
读者 姓名、编号、已借数量
图书 书名、作者、是否在架上
借阅记录 借出日期、应还日期
图书馆 全部图书(集合)

定行为——判据只有一条(9.2.2 讲过):

"这件事主要是在改谁的数据?就写在谁身上。"

动作 改谁的数据 结论
借出图书 改图书的在架状态 → Book::borrow()
归还图书 改图书的在架状态 → Book::giveBack()
登记记录 往图书馆的记录集合里加一条 → Library::record()
查还能借几本 看读者自己的已借数量 → Reader::remaining()

注意"读者.借书"这件事:读者发起借书,但真正改状态的是图书。所以 Reader 那边是个"请求"(Reader::borrow(Book*)),它内部去调 book->borrow()。"发起"和"执行"经常不在同一个类上——这是建模时最要细心的一处。

四、第四步:定关系——三条快问

对着每两个类,问这三句:

快问 是 → 否 →
① "A 是一种 B"吗?(is-a) 泛化/继承(空心三角) 继续问 ②
② "A 有一个 B"吗?(has-a) 关联/聚合/组合(实线,看菱形) 可能没关系,或只是依赖
③ 若 has-a:A 没了,B 能独立活吗? 能 → 聚合(空心◇);不能 → 组合(实心◆) ——

本例走一遍:

图书馆 ◆—— * 图书        (图书馆没了,它的藏书也就没了 → 组合)
图书馆 ◆—— * 借阅记录    (同上 → 组合)
读者 —— * 借阅记录        (记录不属于读者,是"关联";读者没了记录要留档)
读者 —— * 图书            (借来的书不属于读者 → 关联,并标多重性)

画出来:

  ┌───────────┐1        *┌───────────┐
  │  Reader   │──────────>│   Book    │
  ├───────────┤           ├───────────┤
  │ - name    │           │ - title   │
  │ - id      │           │ - onShelf │
  │ - count   │           ├───────────┤
  ├───────────┤           │ + borrow()│
  │ + borrow()│           │+ giveBack()│
  └───────────┘           └───────────┘
        │ 1                      △ *   (组合,菱形指向整体)
        │                        │
        │ *              ┌───────────────┐
  ┌───────────┐          │   Library     │
  │ LoanRecord│<────────◆├───────────────┤
  ├───────────┤  *     1 │ - books:Book[]│
  │ - outDate │          │ - records[]   │
  │ - dueDate │          ├───────────────┤
  └───────────┘          │ + addBook()   │
                         │ + record()    │
                         └───────────────┘

五、需求 → 用例 → 类图:一条落地链

真实项目里,建模通常不是从零开始的,而是沿着这条链往下走:

① 需求描述(自然语言)    "读者能借书、还书,管理员能查在架图书"
        ↓  用【用例图】梳理"谁能用系统做什么"
② 用例(谁 + 做什么)     读者:借书 / 还书;管理员:查询
        ↓  对每个用例,走【四步建模法】
③ 类图(有哪些类、怎么连)  Reader / Book / Library / LoanRecord
        ↓  对复杂场景,用【时序图】理清调用顺序
④ 时序图(对象怎么来回配合)读者 → 图书.borrow() → 图书馆.record()
图 站在什么位置
用例图 需求阶段——把"要做什么"列清楚,不涉及内部结构
类图 设计阶段——把"用什么类来实现"定下来
时序图 细节阶段——把一次具体操作的调用顺序画清楚

一句话:用例图管"做什么",类图管"有什么",时序图管"怎么跑"。

六、画完之后:自问三句

图不是画完就算好,收笔前必须过一遍这三问:

自问 发现问题怎么办
① 职责清不清? 如果一个类的方法一半在管自己的数据、一半在管别人的,说明职责越界——把不属于它的方法挪走
② 关系对不对? 检查有没有把 has-a 画成了继承;检查菱形方向(在"整体"那端);检查多重性(是 * 还是 1..*)
③ 有没有多余的类? 只有一两个属性、没有任何方法的类,往往可以并入别的类(比如"借书证"并进"读者")

再加一问(很值钱):"如果我明天要加一个新需求(比如'预约'),这张图会怎么变?" ——改动小的图,是好图(这就是 9.2.2 说的"改动范围收敛",在建模阶段就先验一遍)。

七、建模不是一次性的

最后一件必须记住的事:

代码变了,图要跟着改;否则图就从"资产"变成"负债"。

这跟测试的道理是一样的:不维护的东西,就不能指望它有用。

八、动手 Demo:从一句话需求走到类图

需求(一句话):

"老师布置作业,学生提交作业,系统记录成绩。"

第①步:圈名词 → 老师、作业、学生、成绩(系统本身不算类)

第②③步:列属性与行为

类 属性 方法
Teacher 姓名 + assign(hw: Homework) : void(布置)
Student 姓名、学号 + submit(hw: Homework) : void(提交)
Homework 题目、截止日期 + isLate(now) : bool(判断是否逾期)
Score 分数、评语 ——

第④步:定关系

一对 快问 结论
Homework ↔ Teacher "作业是一种老师"?不是;"作业是老师的部分"?是 组合 ◆(老师不再教这门课,作业也没意义了)
Student ↔ Homework "提交"=临时持有 关联,标 1 对 *
Homework ↔ Score 一份作业对应一个分数,且分数因作业而生 组合 ◆,标 1 对 1

看到什么算成功:你能画出四格方框 + 两条菱形线 + 一条普通实线,并说清"为什么 Score 对 Homework 是组合而不是关联"(分数不能脱离那次作业独立存在)。

30 秒实验:加一个需求——"学生可以补交作业,但会被标记"。你需要在图里改什么?

→ ① Homework 加一个方法 + markLate() : void;② Score 可能要加 - late : bool。注意:Teacher 和 Library 式的"大管家"类一个字都不用改——因为改动被关在了"作业自己的事"里。这就是 9.2.2 那句"谁变了就改谁"在建模阶段的提前演练。

九、常见误区

  1. "建模就是画好看的图"——不。建模的核心产出是想清楚的过程:哪些类、各自职责、什么关系。图只是这个过程的记录。
  2. "每个名词都要建类"——过头了。很多名词只是属性(借书证 → 读者的一个字段)。类越多,维护成本越高。
  3. "建模一次做完就不用动了"——图是活的。代码演进,图要跟着走;否则它就从资产变成了"看起来很有用的误导"。
  4. "必须先画完图才能写代码"——不必这么重。小项目在纸上画十分钟就够了;关键是先想结构,再动手写,而不是"先写 500 行再补图"。建模的度,看项目大小。
  5. "先定关系更省事"——顺序错了。先想清"有哪些类、各自是什么",关系才会自然浮现;上来就纠结"这根线是聚合还是组合",通常是因为还没想清这两个类是什么。
  6. "图越细越好"——细节留给代码。类图里写清主要属性和行为即可;把每个局部变量、每个循环都画进图里,只会让它失去"一眼看懂"的价值。
自测三问(点开对答案) 1. 面向对象建模的四步是什么?顺序为什么不能乱?——**① 找对象 → ② 定属性 → ③ 定行为 → ④ 定关系**。因为**"关系"要建立在"知道双方是什么"的基础上**;先定关系会陷入"该不该连"的死循环。 2. 找对象的窍门是什么?怎么筛选候选?——从**需求描述里的名词**里圈候选。再用三把筛子:**是系统里的角色/事物吗?它有数据或行为要管吗?它能不能并入别的类(多余吗)?** 3. 画完类图后必须自问哪三句?——**① 职责清不清(有没有越界)?② 关系对不对(is-a/has-a 有没有搞混、菱形方向、多重性)?③ 有没有多余的类(只有一两个属性、没方法的类往往可以并掉)?** 另加一句很有用的:**"明天加个新需求,这张图要改多少?"**

相关笔记