BUAA OO 2026 第四单元总结与课程全程回顾——正向建模、两阶类图、踩坑全记录与四单元思维演进

杨二郎ZC061030 2026-06-20 00:46:33

BUAA OO 2026 第四单元总结与课程全程回顾——正向建模、两阶类图、踩坑全记录与四单元思维演进

本文是 BUAA 面向对象设计与构造(OO)2026 第四单元的技术总结博客。第四单元以"图书馆管理系统"为载体,第一次让我们真正做"正向建模"——不是给一份 MDJ 让你解析,而是先画类图、再写代码,并用两阶段的严格一致性检查逼迫二者收敛。我提交了十几次评测、push 了十来个 commit,从"差不多能跑"打磨到"细节都站得住"。下面我把本单元的架构、踩过的每一个坑、啃下来的每一个难点,连同四个单元的思维演进,一并整理出来。


一、本单元的正向建模与开发:两阶类图扮演的角色

1.1 什么是"正向建模"

前三个单元,无论是表达式、电梯还是社交网络,我都是"先写代码,模型在脑子里"。第四单元彻底反过来:先有模型,后有实现,且模型与代码必须可追踪地保持一致

HW15 把这一理念直接写进了规则——两阶段提交

  • Phase 1(一阶):根目录只放 Main.java(输出类名字符串)+ uml_pre.mdj。此时还没有完整实现,要求先交一份预备类图
  • Phase 2(二阶):提交 src/ 全部 Java 代码 + uml_ultimate.mdj + 评测后下发的 config.yml。这一阶要做 R2–R5 的严格"程序–类图一致性检查",属性、方法、可见性、关联关系都要对得上,覆盖率不得低于 60%。

这套"先画图骨架,再补全实现并锁死一致性"的机制,就是我理解的两阶类图:第一阶负责"设计先行",第二阶负责"代码归位"。

1.2 两阶类图在正向建模中的两层作用

第一层作用——一阶类图:把架构想清楚再动手。

uml_pre 阶段强迫我在写第一行业务逻辑之前,先把系统拆成有职责边界的类:

职责
LibraryManager主协调器,持有所有位置对象与用户表,统一调度 open/close/arrange/borrow/return/order/pick/read/restore/grade/renew/query
Book单本书,持 isbn 属性,用 @Trigger 注解覆盖状态机转换
BookIsbnISBN 级别,维护评分列表与 avgScoreisPremium() 判断是否进精品架
User用户,credit(初始 100,范围 0–180),持 heldBooks/readingBook/pendingReservation
Reservation预约,含开馆前/闭馆后不同的失效起算逻辑
位置类BookshelfTreasuredBookshelfBorrowAndReturnOfficeAppointmentOfficeReadingRoom,各自持 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 变量bsDesttbsDestaoDestbroDestrrDest):同源出口用同一变量的不同取值保证互斥,跨源之间用不同变量避免合取冲突,才同时满足"互斥性"和"可解性"。下图是 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 模型设计的追踪关系

2.1 代码—模型的正向映射

本单元的架构在代码层面高度契合 UML 模型逻辑,追踪关系体现在三个维度:

  • 类映射:每个 UML Class 对应一个 Java 类,类内部维护自身属性与基于 ID 的关联索引。
  • 状态机映射Book 的生命周期由状态图描述,代码侧用 @Trigger 注解覆盖每一次状态转换,状态图里 Guard 引用的变量名必须与代码成员变量严格对齐。
  • 关联映射:UML 中的关联在代码里落地为 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

2.2 一次真实的"追踪危机":语义评测倒逼的重构

追踪关系不是天然成立的,它是被"语义评测"硬逼出来的。我第一次二阶提交分数不高,主要扣在核心类完备性要素定位准确性:评测机要求管理类必须叫 LibraryManagerBook 必须有 isbn 属性、各场所类必须有对应业务方法(BorrowReturnOffice.return/restoreAppointmentOffice.order/pickReadingRoom.read),管理类里要有 arrange/open/close/move

而我最初的设计把所有业务逻辑都集中在 Library 里,场所类只是数据容器。这种设计代码上能跑通,却和语义检查期望的"职责分散到场所类"不符。于是我做了一次重构:LibraryLibraryManager,给各场所类补上业务方法,给 Book 补上 isbn

但重构立刻引爆了另一个矛盾:两阶相似度从达标跌到 48.6%,低于 60% 阈值——因为我改动了代码结构,却没同步改一阶已提交的类图。最后我专门写了一份重构说明文档,解释这些改动是为符合语义评测、而非结构性颠覆,才把一阶分保住。

这件事让我对"追踪关系"有了血的体会:代码设计和 UML 模型不是两份独立的交付物,而是同一份设计的两个投影。改动任何一边,另一边都要跟着追踪到位,否则就会在一致性检查上付出代价。


三、踩坑全记录:那些折磨过我的 Bug 与它们的修复

这一单元我栽的跟头几乎覆盖了"结构、规范、逻辑、时序"四个层面。下面按"现象 → 根因 → 修复"逐条复盘,希望能帮后来人少踩。

3.1 结构与 Checkstyle 类(7 条)

坑 1:MDJ 类名必须与评测器期望对齐。
现象:第一版我把所有位置内嵌进 Library 类,评测只对齐了 3 个类,关键词覆盖率仅 14.81%
根因:评测机按预期类名做要素定位,内嵌设计让它找不到对应类。
修复:位置(书架、还书处、预约处、阅览室)必须
各自建类
;主协调类叫 LibraryManager 而非 Library

坑 2:根目录和 src/ 不能同时存在同名 public class。
现象:Main_classnames.java 里写了 public class Main,加上根目录 Main.javasrc/Main.java,三个 Main 类直接导致编译失败。
修复:Phase 1 只留根目录 Main.java,其余全删。

坑 3:MDJ 里 StateMachine/Collaboration 必须在 UMLModel 内部。
现象:第一版把 StatechartDiagramSequenceDiagram 放到 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 极隐蔽,一定要在扣分的同时把触发条件清掉。

3.2 业务逻辑深水区:三个真正"烧脑"的 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。整理流程要同时满足三条规则:

  1. 开馆后借还处、阅览室不能有书;预约处不能有逾期书;
  2. 每本书每次整理最多移动一次(终点是预约处不计入);
  3. 还要尽可能满足用户预约。

我最初的思路是"先满足预约,再清扫剩余位置"。但有个隐蔽 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 的本质教训写在第七节"课程收获"里:输出协议是契约,不是可选项。

3.3 信用分规则速查

借阅、预约、阅览三类操作彼此关联,信用分(credit)是贯穿其中的隐藏主线,规则如下:

行为信用分变化
按时还书+10
主动归还阅览书+10
逾期还书−15
阅览未还(闭馆时)−10
预约未取−15

准入门槛:借书 / 预约 / 阅览要求 credit > 80;A 类书阅览要求 credit > 40credit 初始 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
  • 第一单元(表达式解析):从面向过程到初级面向对象。学会把复杂表达式拆成层次结构(多项式 / 项 / 因子),初步掌握递归下降的解析思维,建立"对象状态 + 行为"的基本概念。
  • 第二单元(多线程电梯调度):并发与设计模式登场。学会用生产者—消费者模式组织线程协作,理解线程安全、锁机制,以及单例、策略、观察者等模式。架构思维从静态顺序数据流,跃迁到动态并发控制。
  • 第三单元(JML 社交网络):向契约式设计与图论算法转变。在 JML 规格的严格约束下,深刻体会"基于契约编程"对正确性的意义,学会在满足规格的前提下优化底层数据结构与图算法(最短路径、连通块)。
  • 第四单元(UML / 正向建模):回归系统级建模。重心转向把现实世界的联系高度抽象成模型、并在内存中优雅复现,把前三单元的训练串了起来——既要写出能跑的代码,又要画出与代码一致的类图和状态图。

四个单元走下来,我对"架构"的理解从"代码怎么组织"升级为"如何用模型驾驭复杂性,再让代码忠实地映射模型"。


七、四个单元测试思维的演进

flowchart LR
    T1["U1 黑盒 + 边界值<br/>大数 / 嵌套 / 符号冗余"]
    T2["U2 压力 + 时序<br/>高并发随机投放<br/>死锁 / 轮询"]
    T3["U3 JUnit + 基于属性<br/>前置/后置条件对照"]
    T4["U4 数据驱动 + 图完整性<br/>异常图模型 / 交互式模拟器"]
    T1 --> T2 --> T3 --> T4
  • 第一单元:黑盒测试 + 边界值分析,关注大数、深层嵌套括号、符号冗余等极端情况。
  • 第二单元:多线程不可重复,转向压力测试与时序分析,构造高并发随机数据捕捉死锁、轮询、线程安全问题。
  • 第三单元:全面引入 JUnit,针对 JML 规格做基于属性的测试,严格对照前置 / 后置条件。
  • 第四单元:数据驱动测试 + 图结构完整性,构造异常继承、复杂状态转移、非法结构的 MDJ 做鲁棒性测试。

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 课程第四单元结束之际

...全文
6 回复 打赏 收藏 转发到动态 举报
写回复
用AI写文章
回复
切换为时间正序
请发表友善的回复…
发表回复

308

社区成员

发帖
与我相关
我的任务
社区描述
2026年北航面向对象设计与构造
java 高校
社区管理员
  • 孙琦航
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告
暂无公告

试试用AI创作助手写篇文章吧