308
社区成员
发帖
与我相关
我的任务
分享本文是 BUAA 面向对象设计与构造(OO)2026 第四单元的技术总结博客。第四单元以"图书馆管理系统"为载体,第一次让我们真正做"正向建模"——不是给一份 MDJ 让你解析,而是先画类图、再写代码,并用两阶段的严格一致性检查逼迫二者收敛。我提交了十几次评测、push 了十来个 commit,从"差不多能跑"打磨到"细节都站得住"。下面我把本单元的架构、踩过的每一个坑、啃下来的每一个难点,连同四个单元的思维演进,一并整理出来。
前三个单元,无论是表达式、电梯还是社交网络,我都是"先写代码,模型在脑子里"。第四单元彻底反过来:先有模型,后有实现,且模型与代码必须可追踪地保持一致。
HW15 把这一理念直接写进了规则——两阶段提交:
Main.java(输出类名字符串)+ uml_pre.mdj。此时还没有完整实现,要求先交一份预备类图。src/ 全部 Java 代码 + uml_ultimate.mdj + 评测后下发的 config.yml。这一阶要做 R2–R5 的严格"程序–类图一致性检查",属性、方法、可见性、关联关系都要对得上,覆盖率不得低于 60%。这套"先画图骨架,再补全实现并锁死一致性"的机制,就是我理解的两阶类图:第一阶负责"设计先行",第二阶负责"代码归位"。
第一层作用——一阶类图:把架构想清楚再动手。
uml_pre 阶段强迫我在写第一行业务逻辑之前,先把系统拆成有职责边界的类:
| 类 | 职责 |
|---|---|
LibraryManager | 主协调器,持有所有位置对象与用户表,统一调度 open/close/arrange/borrow/return/order/pick/read/restore/grade/renew/query |
Book | 单本书,持 isbn 属性,用 @Trigger 注解覆盖状态机转换 |
BookIsbn | ISBN 级别,维护评分列表与 avgScore,isPremium() 判断是否进精品架 |
User | 用户,credit(初始 100,范围 0–180),持 heldBooks/readingBook/pendingReservation |
Reservation | 预约,含开馆前/闭馆后不同的失效起算逻辑 |
| 位置类 | Bookshelf、TreasuredBookshelf、BorrowAndReturnOffice、AppointmentOffice、ReadingRoom,各自持 List<Book> |
把上表落到一张类图上,整个系统的骨架就一目了然了(下图为便于理解做了简化,省略了次要字段与 getter/setter):
classDiagram
class LibraryManager {
-Map users
-Map isbnTable
+open() close()
+arrange() move()
+borrow() return()
+order() pick()
+read() restore()
+grade() renew() query()
}
class Location {
<<abstract>>
#List~Book~ books
+addBook(Book)
+removeBook(Book)
}
class Bookshelf
class TreasuredBookshelf
class BorrowAndReturnOffice {
+return() restore()
}
class AppointmentOffice {
+order() pick()
}
class ReadingRoom {
+read()
}
class Book {
-String id
-String isbn
+trigger()
}
class BookIsbn {
-String isbn
-int avgScore
+isPremium() bool
}
class User {
-String id
-int credit
+clearReadingBook()
}
class Reservation {
-LocalDate deadline
+isExpired() bool
}
Location <|-- Bookshelf
Location <|-- TreasuredBookshelf
Location <|-- BorrowAndReturnOffice
Location <|-- AppointmentOffice
Location <|-- ReadingRoom
LibraryManager o-- Location : 持有场所
LibraryManager o-- User : 用户表
LibraryManager o-- BookIsbn : ISBN 索引
Location o-- Book : List~Book~
BookIsbn o-- Book : 同 ISBN 多本
User --> Book : heldBooks / readingBook
User --> Reservation : pendingReservation
Reservation --> Book : 预留
说明:图中
Location抽象基类是为了把"各场所都持List<Book>"这一共性提炼出来,让图更清爽;若你的实际代码用的是接口或并列独立类,按实情微调即可。
一阶类图先把"骨架"立起来,确立系统的基本组成要素,这是自底向上构建系统的第一步。
第二层作用——二阶类图:把动态关系与约束补全。
二阶不只是把代码补齐,更要补全动态特征:类与类之间的继承、实现、关联,以及状态机的状态流转。尤其状态图,它在二阶承担了"数学约束"而非"示意图"的角色——这是本单元给我冲击最大的一点。
状态图要求从初始状态到任意状态的所有简单路径上,全部 Guard 条件的逻辑与(conjunction)必须有解(SAT 可满足)。我最初图省事,用一个 actionType 变量区分所有迁移,结果只要一条简单路径上出现两条迁移,合取就立刻不可解。反复推导后,我把设计改成每个源状态各用一个独立的 dest 变量(bsDest、tbsDest、aoDest、broDest、rrDest):同源出口用同一变量的不同取值保证互斥,跨源之间用不同变量避免合取冲突,才同时满足"互斥性"和"可解性"。下图是 Book 状态机的简化示意:
stateDiagram-v2
[*] --> Bookshelf
Bookshelf --> BorrowAndReturnOffice : [bsDest==BRO]
Bookshelf --> ReadingRoom : [bsDest==RR]
Bookshelf --> AppointmentOffice : [bsDest==AO]
TreasuredBookshelf --> BorrowAndReturnOffice : [tbsDest==BRO]
TreasuredBookshelf --> ReadingRoom : [tbsDest==RR]
TreasuredBookshelf --> AppointmentOffice : [tbsDest==AO]
BorrowAndReturnOffice --> Bookshelf : [broDest==BS]
BorrowAndReturnOffice --> TreasuredBookshelf : [broDest==TBS]
BorrowAndReturnOffice --> AppointmentOffice : [broDest==AO]
AppointmentOffice --> Bookshelf : [aoDest==BS]
AppointmentOffice --> BorrowAndReturnOffice : [aoDest==BRO]
ReadingRoom --> BorrowAndReturnOffice : [rrDest==BRO]
ReadingRoom --> Bookshelf : [rrDest==BS]
设计要点:同源出口(如
Bookshelf的三条出边)共用bsDest的不同取值,天然互斥;不同源用不同变量(bsDest/tbsDest/...),避免在同一条简单路径上做逻辑与时发生冲突——这正是状态图 SAT 可解性约束逼出来的精细设计。
两阶类图的真正价值,不在于画图本身,而在于它用一致性检查这道关卡,把"边写边补的图"拦在了门外——而边写边补的图,迟早会和现实背道而驰。
本单元的架构在代码层面高度契合 UML 模型逻辑,追踪关系体现在三个维度:
Book 的生命周期由状态图描述,代码侧用 @Trigger 注解覆盖每一次状态转换,状态图里 Guard 引用的变量名必须与代码成员变量严格对齐。List<Book>、用户表等持有结构。下图把这三层追踪关系并排画出来,左边是 UML 模型元素,右边是代码落地形态,箭头即"可追踪"的对应关系:
flowchart LR
subgraph UML[UML 模型侧]
U1[Class: LibraryManager]
U2[Class: Book + isbn 属性]
U3[StateMachine: Book 生命周期]
U4[Association: 管理类 *-- 场所]
end
subgraph CODE[代码实现侧]
C1[class LibraryManager]
C2[class Book 字段 isbn]
C3["@Trigger 注解的状态转换方法"]
C4["Map / List 持有结构"]
end
U1 -. 一一对应 .-> C1
U2 -. 属性对齐 .-> C2
U3 -. Guard 变量名对齐 .-> C3
U4 -. 关联落地 .-> C4
追踪关系不是天然成立的,它是被"语义评测"硬逼出来的。我第一次二阶提交分数不高,主要扣在核心类完备性和要素定位准确性:评测机要求管理类必须叫 LibraryManager、Book 必须有 isbn 属性、各场所类必须有对应业务方法(BorrowReturnOffice.return/restore、AppointmentOffice.order/pick、ReadingRoom.read),管理类里要有 arrange/open/close/move。
而我最初的设计把所有业务逻辑都集中在 Library 里,场所类只是数据容器。这种设计代码上能跑通,却和语义检查期望的"职责分散到场所类"不符。于是我做了一次重构:Library → LibraryManager,给各场所类补上业务方法,给 Book 补上 isbn。
但重构立刻引爆了另一个矛盾:两阶相似度从达标跌到 48.6%,低于 60% 阈值——因为我改动了代码结构,却没同步改一阶已提交的类图。最后我专门写了一份重构说明文档,解释这些改动是为符合语义评测、而非结构性颠覆,才把一阶分保住。
这件事让我对"追踪关系"有了血的体会:代码设计和 UML 模型不是两份独立的交付物,而是同一份设计的两个投影。改动任何一边,另一边都要跟着追踪到位,否则就会在一致性检查上付出代价。
这一单元我栽的跟头几乎覆盖了"结构、规范、逻辑、时序"四个层面。下面按"现象 → 根因 → 修复"逐条复盘,希望能帮后来人少踩。
坑 1:MDJ 类名必须与评测器期望对齐。
现象:第一版我把所有位置内嵌进 Library 类,评测只对齐了 3 个类,关键词覆盖率仅 14.81%。
根因:评测机按预期类名做要素定位,内嵌设计让它找不到对应类。
修复:位置(书架、还书处、预约处、阅览室)必须各自建类;主协调类叫 LibraryManager 而非 Library。
坑 2:根目录和 src/ 不能同时存在同名 public class。
现象:Main_classnames.java 里写了 public class Main,加上根目录 Main.java 和 src/Main.java,三个 Main 类直接导致编译失败。
修复:Phase 1 只留根目录 Main.java,其余全删。
坑 3:MDJ 里 StateMachine/Collaboration 必须在 UMLModel 内部。
现象:第一版把 StatechartDiagram 和 SequenceDiagram 放到 Project 根层,导致 classTest1 解析失败;另外评测机要求 UMLStateMachine 直接挂在 Project 根节点下——我按 StarUML 常规操作右键 Model → Add Diagram 创建,状态机被放进了 UMLModel 内部,结果报"StateMachine1 不存在"。
根因:StarUML 的可视化操作和评测机期望的 mdj 层级有隐性出入。
修复:花了一个多小时反复调 mdj 结构,最终对齐到下面这棵树:
flowchart TD
P[Project 根节点]
P --> UM[UMLModel]
P --> SM[UMLStateMachine 直接挂根]
UM --> CL[Classes ...]
UM --> SCD[StatechartDiagram]
UM --> COL[Collaboration]
UM --> SEQ[SequenceDiagram]
教训:mdj 是数据文件,评测机按固定层级解析,画图工具的默认放置位置不等于评测机要的位置。
坑 4:EmptyLineSeparator——方法/字段之间必须有空行(23 处违规)。
把 getter 写成连排一行式是违规的,每个方法和字段定义前都要加空行。
坑 5:NeedBraces——if/for/while 永远加大括号(53 处违规)。if (x) return null; 这种一行式全部违规,无论多短都要补 {}:
// 违规
if (x) return null;
// 合规
if (x) {
return null;
}
坑 6:AvoidStarImport——禁止通配符导入(2 处违规)。import com.oocourse.library3.* 和 import java.util.* 都不允许,必须逐一列出用到的类。
坑 7:扣分后必须同时清除 readingBook(重复扣分 Bug)。
现象:关闭时对未归还阅览书扣 -10,若不清除 user.readingBook,下次关闭会再扣一次。
修复:applyReadingPenalties 扣分后立即调用 user.clearReadingBook()。这类"状态没复位导致副作用反复触发"的 bug 极隐蔽,一定要在扣分的同时把触发条件清掉。
Bug A:取书前必须再校验一次持有限制。
我第一版下意识觉得"预约成功了,取书肯定能成功",于是取书时没复查。结果用户在预约和取书之间又借了一本同类型书,强测立刻挂。修复:取书(pick)时重新校验 B 类"一人最多一本"、C 类"按 ISBN 限制"。预约成功 ≠ 取书一定合法,中间的时间窗会改变状态。
Bug B:预约失效时间窗算错。
指导书写"保留 5 天",但开馆前送达和闭馆后送达的起算点不同。我专门画了张时间轴才理清:
flowchart LR
A[预约书送达 AO] --> B{送达时机?}
B -->|开馆前送达| C["expiry = today + 4<br/>当天起算"]
B -->|闭馆后送达| D["expiry = today + 5<br/>次日起算"]
C --> E[第 5 天闭馆后失效再算一次]
D --> E
关键:同样是"5 天",起算点差一天,逾期判定就差一天,边界数据立刻暴露。
Bug C(最折磨):整理流程的 in-place 复用,少了一行 move 就全错。
这是本单元让我熬了一整晚的 bug。整理流程要同时满足三条规则:
我最初的思路是"先满足预约,再清扫剩余位置"。但有个隐蔽 bug:一本逾期的预约书还停在预约处,我图省事直接"原地改个预留人"复用给新预约——物理上书没动,所以我没输出 move 行。结果评测机靠 move ... for 学号 来追踪预约状态,看不到这条 move,就以为这本书没为新用户预留,后续取书直接判错。
这个 bug 必须**"过期书 + 同 ISBN 新预约 + 真的来取"三连**才会触发,简单数据全过、强测才挂。下面这张时序图说明了为什么"少一行 move"会让评测机崩溃:
sequenceDiagram
participant U as 新用户
participant LM as LibraryManager
participant AO as 预约处
participant J as 评测机
Note over AO: 一本逾期预约书原地停留
U->>LM: 预约同 ISBN 的书
LM->>AO: 原地改预留人(物理未移动)
Note over LM,J: ❌ 未输出 move ... for 新用户
J-->>J: 状态恢复: 该书未为新用户预留
U->>LM: 取书 pick
LM->>J: 输出取书成功
J-->>U: ❌ 判错: 你没有为该用户预留过
修复:把整理顺序改成**"先标记过期 → 显式把过期书搬回书架 → 满足预约时统一从书架往预约处搬"**,这样每次预约满足都有明确的 for 学号。修复前后的流程对比:
flowchart TB
subgraph WRONG[修复前: in-place 复用]
W1[逾期预约书停在 AO] --> W2[原地改预留人]
W2 --> W3["不输出 move (物理没动)"]
W3 --> W4[评测机丢失预留状态 ❌]
end
subgraph RIGHT[修复后: 显式搬运]
R1[标记过期] --> R2[过期书显式搬回书架]
R2 --> R3[从书架统一搬往 AO]
R3 --> R4["每次预留都有 move ... for 学号 ✅"]
end
这个 bug 的本质教训写在第七节"课程收获"里:输出协议是契约,不是可选项。
借阅、预约、阅览三类操作彼此关联,信用分(credit)是贯穿其中的隐藏主线,规则如下:
| 行为 | 信用分变化 |
|---|---|
| 按时还书 | +10 |
| 主动归还阅览书 | +10 |
| 逾期还书 | −15 |
| 阅览未还(闭馆时) | −10 |
| 预约未取 | −15 |
准入门槛:借书 / 预约 / 阅览要求 credit > 80;A 类书阅览要求 credit > 40。credit 初始 100,范围 0–180。书籍按 A/B/C 分类,B 类一人最多持一本,C 类按 ISBN 限制——这些限制在借书、预约、取书三处都要校验(见 Bug A)。
除了上面的 bug,本单元还有几个"想清楚就豁然开朗、想不清楚就反复卡"的概念难点。
难点 1:整理流程的三规则博弈。
三条规则交织时,稍不小心就写出"循环移动"或"漏整理"。关键是想清楚**"每本书每次整理最多移动一次,但终点是预约处不计入"**这条豁免——它允许"书架→预约处"这一步不占用移动次数,从而既能满足预约、又不违反移动上限。把这条想透,整理流程的顺序设计(先清扫、后预留)就自然成立了。
难点 2:状态图不是示意图,是数学约束。
我最初以为状态图"画着好看",后来才知道它的 Guard 要做 SAT 求解、路径可达性要做图遍历检查。这逼我把"一个 actionType 表达所有动作"的偷懒思路,改成"按源状态拆分独立 dest 变量"的精细设计(见第一节状态图)。模型一旦带上数学约束,就再也不能糊弄。
难点 3:UML 与代码的双向一致性。
R2–R5 要求属性、方法、可见性、关联关系都对得上,覆盖率 ≥ 60%。我中间因为改了 Book 的成员变量,状态图里 Guard 引用的变量名对不上,被打回好几次。双向一致性意味着任何一边的改动都要同步到另一边,这也是"先建模后实现"被反复强调的原因——边写边补的图迟早和代码脱节。
本单元我把大模型(LLM)深度融入"Vibe Coding"工作流,处理 MDJ 文件结构这类有一定复杂度的解析与建模任务。实践下来,**引导大模型完成架构设计的核心是"精准投喂"与"分步拆解"**:
1. 明确数据与约束上下文,不让模型猜。
绝不让大模型猜数据结构。我会把 MDJ 文件的 JSON 片段结构、核心字段含义、以及评测机的隐性约定(比如 UMLStateMachine 必须直接挂 Project 根节点)明确提供给模型。JSON 在这里只是纯粹的数据存储格式,把这层关系讲清楚,是生成有效代码的前提。
2. 分治式架构生成,而非一次成型。
我按"自底向上"分三步引导:先让模型设计底层数据存储结构(如 ID→元素的 HashMap 映射池),再让它生成针对特定元素(类 / 状态图)的局部解析逻辑,最后才让它封装统一查询接口。每一步可验证、可回退。
3. 用图论算法和一致性检查做反馈纠偏。
基础代码生成后,重点针对二阶关联检查向模型发问——比如检测循环继承、重复实现时,要求它给出具体的图遍历算法(DFS/BFS 变体),再结合低耦合原则重构。更关键的是把评测反馈(相似度、语义检查、SAT 报错)作为新一轮 prompt 喂回去,让模型在真实约束下迭代,而不是在真空里想象。
一句话总结:复杂场景下引导大模型做架构,靠的不是一句"帮我写个图书馆系统",而是把"上下文—拆解—反馈"三件事做扎实,让模型在每一步都站在确定的地基上。
flowchart LR
U1["U1 表达式解析<br/>面向过程→面向对象<br/>递归下降 / 层次结构"]
U2["U2 多线程电梯<br/>并发 + 设计模式<br/>生产者-消费者 / 线程安全"]
U3["U3 JML 社交网络<br/>契约式设计<br/>图论算法优化"]
U4["U4 UML 正向建模<br/>系统级建模<br/>模型驱动 / 双向一致"]
U1 --> U2 --> U3 --> U4
四个单元走下来,我对"架构"的理解从"代码怎么组织"升级为"如何用模型驾驭复杂性,再让代码忠实地映射模型"。
flowchart LR
T1["U1 黑盒 + 边界值<br/>大数 / 嵌套 / 符号冗余"]
T2["U2 压力 + 时序<br/>高并发随机投放<br/>死锁 / 轮询"]
T3["U3 JUnit + 基于属性<br/>前置/后置条件对照"]
T4["U4 数据驱动 + 图完整性<br/>异常图模型 / 交互式模拟器"]
T1 --> T2 --> T3 --> T4
HW15 给了我一次刻骨铭心的教训:我自己的用例都集中在"借→还""读→归"这种主路径上,最后挂强测的却是 3.2 节那个"过期 + 复用"的 in-place bug——必须有时间跨度、状态组合才能触发。后来我专门写了一个交互式模拟器,模拟评测机随机生成 returned/picked/restored 命令,才把这类隐藏 bug 测出来。测试边界条件,比测试主路径重要得多。
四个单元走完,收获远不止"熟练掌握 Java 与面向对象范式",更是工程思维的全面升级。几条最深的体会,都来自本单元的踩坑:
1. 输出协议是契约,不是可选项。
"我内部状态对了"和"评测机能从我的输出里复原我的状态"是两回事。3.2 节的 in-place 复用 bug 就是典型:内部状态完全正确,输出里少一行 move,评测机就崩了。输出是面向外部世界的契约,必须让对方能从中无歧义地恢复你的全部状态。
2. 正向建模 ≠ 反向贴图。
第一阶段画一阶类图时我确实"先设计后写代码",但细节实现还是改了不少,导致二阶相似度栽跟头。边写边补的图,迟早和现实背道而驰——这正是指导书反复强调"先建模后实现"的意义。
3. 状态图是数学约束,不是示意图。
它的 Guard 要做 SAT 求解、路径可达性要做图遍历检查,逼我把偷懒的 actionType 改成精细的 dest 变量设计。模型一旦带上数学约束,就再也不能糊弄。
4. 测试边界条件,比测试主路径重要。
真正会挂的,永远是那些需要时间跨度、状态组合才能触发的角落,而不是顺顺当当的主流程。
5. 从手动梳理逻辑,到用提示词工程引导 AI 辅助架构。
我学会了把大模型作为架构设计的协作者,用"上下文—拆解—反馈"的方法论驾驭它,而不是被它生成的"看起来对"的代码牵着走。
回头看自己这一单元提交了十几次评测、push 了十来个 commit,确实从最初"差不多能跑"打磨到了"细节都站得住"。设计模式、并发控制、契约编程、正向建模这些核心素养与系统级设计思维,不仅是写出优雅代码的基础,更为我后续做底层系统级编程、复杂自动化项目开发,打下了坚实的理论与工程地基。
OO 这门课最折腾,也最值。
—— 写于 2026 OO 课程第四单元结束之际