OO UNIT4总结

刘子豪-24373149 2026-06-19 20:30:32

OO 第四单元总结

一、正向建模与两阶类图

1.1一阶类图的真正价值

回头看 HW13 一阶提交后,我得到的最有价值的反馈不是"R1 通过"那行绿色字,而是评测器对照出的几条信息:

  • "对齐类数: 7 | 缺失: LibraryManager" —— 我一阶把核心类命名为 Library,二阶因为已开始照官方包写代码改成了 LibraryManager,相似度评测当场把我抓住。
  • "R3 状态图路径 InitState → … → user 不可满足" —— 我一阶画了一个 phase >= 2 的 guard 想让状态机更"严密",结果几条迁移合在一起 guard 解不开。

1.2 二阶类图的真正价值

HW14 我在二阶提交前临时加了一个 GradingOffice 类来管理评分,差点没在类图里同步——这正是评测要拦截的:

R2 / R3 检查:
- 类图中每个类的属性 → 与程序中类的属性一致
- 类图中每个类的方法 → 与程序中类的方法一致
- 检测覆盖率:60% 的属性 / 方法对应

二阶相当于一份"完工验收"。任何二阶时代码长出来的新字段、新方法,都必须在类图里有对应;反之亦然。


二、本单元架构与追踪关系

2.1 三次作业架构的演化

作业主线新增设计单元新增方法(关键)
HW13基础借阅 + 预约 + 评分12 个:MainClass、LibraryManager、Place、Bookshelf、TreasuredBookshelf、BorrowReturnOffice、AppointmentOffice、ReadingRoom、GradingOffice、Book、User、Orderborrow/order/pick/return/restore/read/grade/query/arrange/open/close
HW14加入图书状态机同 HW13(结构未变,把状态用 LibraryBookState 集中管理)状态机为方法补 @Trigger 注解
HW15信用分 + 借阅期限 + 续订 + 顺序图新增 CreditOffice(13 个),Book/Order/User 扩展字段renew / queryCreditScore / applyEndOfDayCredits / catchUp*

HW13 提交时类图只有 12 个单元,关键词覆盖率 23/27 ≈ 85%。HW15 加入 CreditOffice 让 "credit" 与 "renew" 这两个关键词同时进入覆盖,最终 25/27 ≈ 93%。

2.2 关键设计决策记录

1. 把"地点"抽象成 Place 接口

5 个地点类(普通书架、精品书架、借还处、预约处、阅览室)的行为高度同构:都需要 addBook / removeBook / getBooks / findByIsbn。直接抽出 Place 接口后,LibraryManager.placeOf(state) 只用 5 行就能把"状态→地点"对应起来,整理流程的 doMove 完全不必关心目标是 bs / tbs / bro / ao / rr,只调用 place.addBook(copy) 即可。

private Place placeOf(LibraryBookState state) {
    if (state == LibraryBookState.BOOKSHELF) return bookshelf;
    if (state == LibraryBookState.TREASURED_BOOKSHELF) return treasuredBookshelf;
    if (state == LibraryBookState.BORROW_RETURN_OFFICE) return borrowReturnOffice;
    if (state == LibraryBookState.READING_ROOM) return readingRoom;
    return appointmentOffice;
}

2. 把整理流程拆成 collectPool / assignOrders / redistribute 三段

HW13 一开始我把 arrange() 写成一个大方法,结果 checkstyle 第一时间提示 MethodLength: 60+ lines。重构成三个独立私有方法后,每段职责单一,且和 @Trigger 注解结构对齐:

  • collectPool:把所有"可能要移动"的副本收集到一个池子
  • assignOrders:从池中给待送达预约挑书,移动到 ao
  • redistribute:池里剩余的副本按当前 rating 回归 bs 或 tbs

3. HW15 引入 CreditOffice

最初考虑把信用分直接放进 User,但很快发现:

  • 信用分的业务规则有 5 条加减 + 3 条权限判定,全塞进 User 会让它臃肿
  • 关键词"credit"需要在类名 / 属性 / 方法里出现,单独建类便于关键词检测
  • User 与"积分逻辑"是两个变化频率不同的维度

最终独立出 CreditOfficeUser 只持有自己的状态(持有图书、预约、阅读状态),CreditOffice 集中所有积分增减与判定。这种 SRP 拆分在后续添加新规则(如"特殊用户白名单")时只动 CreditOffice 一个文件即可。


三、大模型辅助正向建模:从工具到协作伙伴

本单元我大量使用了Claude来辅助mdj文件的生成、状态图的设计、以及代码 / 图一致性的检查。比起前几单元更纯粹的"代码助手",本单元的大模型使用更像"建模顾问"。

3.1 我用得最有效率的几种prompt模式

1. 让大模型生成 mdj 骨架,让自己检查

"我有 13 个类要画 UML 类图,给我生成一个 Python 脚本,输入是类名列表 +
每类的属性/方法,输出 StarUML 4.x 兼容的 mdj 文件。要满足 R1:
所有元素 name 不为空、direction='return' 的 UMLParameter 例外、
无循环继承、接口属性方法必须 public..."

这种"给规则+给输入格式+给输出格式"的 prompt 一次就能产生可用的 gen_mdj.py。后续每次需要给类加方法 / 改属性时,只改输入数据,不改生成逻辑。整个 HW15 的 mdj 生成器是一个 ~400 行的 Python 脚本,跑一次产生 19 万字节的 mdj。

3.2 引导大模型完成复杂建模任务的方法

总结下来,我的几条实践经验:

  1. 用规则约束输出格式 — 不要让它"自由发挥"。明确告诉它"我要 JSON / mdj / Java",告诉它字段命名规则、版本兼容性。
  2. 分阶段确认,不要一口气出整个答案 — 先让它列出"我打算这么做"的设计意图,确认后再让它生成实现;不要看到一大段生成内容就直接复制。
  3. 把评测规则贴成 prompt 的一部分 — R1-R5、关键词列表、@Trigger 注解语法,全部贴进 system prompt,相当于给它一份"评测器视角"。
  4. 建立"反例驱动"的迭代循环 — 每次它生成一版,我跑一次评测器(或自己写 SAT / 一致性检查),把失败点反馈回去。这个循环走 3-5 轮基本能把疑难解决。
  5. 永远保留 human in the loop — 大模型给的方案在"语法层面"几乎都对,但在"意图层面"可能完全跑偏(比如 HW15 信用分扣分的时序)。最终决策必须我自己拍板。

四、四单元架构设计思维的演进

Unit 1 — 从过程式到组合模式

第一单元的最大突破是 从多项式表示到 AST。第一次作业写完后,我手里是一个 Map<Integer, BigInteger> 表示多项式系数的"过程式实现";到第二次作业增加 exp、选择因子、自定义函数时这个表示就崩了,必须重构成 AST。

这次重构是我第一次体会到 抽象基类 + 子类继承 + 递归遍历 的威力——所有节点都实现统一接口,求导 / 化简 / 代入只是不同的 visitor。后续要加 sin / cos 因子时,只用新增节点类,主流程一行不改。

主要收获面向接口而非面向实现

Unit 2 — 从单线程模型到并发架构

第二单元的核心难度从"算法"转移到"线程同步"。三次作业的架构演进是:

  1. HW5:单线程生产者-消费者
  2. HW6:加状态机,引入 volatile 与精细锁
  3. HW7:双轿厢防碰撞,独立 Shaft 锁层级

最大的思维跃迁是把锁视为资源:不同的资源用不同的锁保护,避免嵌套;用 synchronized(queue) 保护任务队列、用 synchronized(shaft) 保护井道位置,二者完全解耦。

另一个收获是 Producer-Consumer 模式的高内聚低耦合:调度器只与队列通信、电梯只与队列通信,调度算法的修改不会影响电梯运行逻辑。

主要收获用模式名思考问题

Unit 3 — 从主动设计到规格驱动

第三单元的范式是 JML 规格优先。这一单元我几乎没"设计架构"——架构基本由官方包定死,我要做的是按 JML 字面意思实现每个方法。这种约束反而让我学到了:

  1. 接口与实现分离 — JML 只说"做什么",怎么做完全是我的事。底层从 ArrayList 改成 HashMap、把 queryMutualFollowingSum 从 O(n²) 改成增量维护,对外行为不变。
  2. 不变量与约束的显式表达 — JML 教我把"用户的勋章数 ≥ 0"、"getInterest 返回 typeCounts[i]*..." 这样的不变量明确写出来。
  3. 异常优先级是契约的一部分UID → InvalidRank → NoVideoUploaded → ColdStartUser,这样的优先级关系不是实现细节,而是规格的一部分。

主要收获契约式设计比写代码更早

Unit 4 — 从规格驱动到模型驱动

第四单元在 Unit 3 的基础上再前进一步——把 UML 模型作为单一真理源。代码、状态图、顺序图、@Trigger 注解、@SendMessage 注解,都要围绕同一份 UML 模型展开。

这种风格最大的好处是:架构与实现的差异被自动揭露。如果代码里方法签名漂移,R2 会抓住;如果状态图里 guard 不可满足,R3 会抓住;如果顺序图里消息不存在对应方法,R3 会抓住。

主要收获正向建模需要"先慢后快"的克制力

五、四单元测试思维的演进

Unit 1 — 边界用例与递归深度

最早的测试基本就是 手工拍脑袋写边界:超大的 BigInteger、20 层括号嵌套、递推函数到 f{20} 这样的极端深度。bug 主要出现在复杂度高的方法(圈复杂度 >5 的 Parser.parseFactorInternalExpr.deriveParser.expandRecursive),低复杂度方法基本不出问题。

Unit 2 — 并发场景的"复现性陷阱"

并发测试与之前完全不同:bug 可能在 100 次跑中只出现 1 次,而且复现取决于线程调度。我学到的方法:

  1. 重复跑同一个用例几百次for i in {1..500}; do java MainClass < input.txt; done,统计错误率
  2. 构造"最坏调度" — 把多个 wait 都设短超时,强行让线程在临界区切换
  3. 状态打印 — 在所有锁释放点加 stderr 日志,事后排查时间线
  4. 不靠"运行没崩"判断正确性 — 必须有期望输出对比

HW7 的一个 wait() 死锁就是这样定位的:100 次中只有 3-4 次复现,但 stderr 日志显示备用电梯一直在 sleepQuietly 自旋。

核心思维线程相关的 bug 不能靠跑得过来判断没问题

Unit 3 — JML 公式的反向验证

Unit 3 第一次让我接触 JUnit 单元测试。最大的方法论是反向验证

这种"用规格的别处部分校验当下结果"的写法比写死期望值更稳健。另一个发现是 assignable \nothing 检查——用孪生 Network 验证 pure 方法不改状态。

核心思维规格本身就是测试用例的来源

Unit 4 — 多维度断言:输出、模型、约束

Unit 4 的测试同时关注三类断言:

  1. 输出正确 — 把官方两个样例跑过、与期望逐字节 diff
  2. 模型一致 — 跑 R1-R5 检查、R3 SAT 检查、关键词覆盖检查
  3. 约束符合 — 信用分上下限、借阅期限、预约 5 天保留期、续订加 7 天

六、整门课的收获与反思

6.1 我学到的最重要的三件事

1.

四个单元的所有"重构事故",回头看都是因为前期没想清楚就开始写。Unit 1 的多项式 → AST 重构、Unit 2 的双轿厢 Shaft 锁、Unit 3 的容器选型替换、Unit 4 的 phase 变量删除——每一次重构的代价都是几百行代码的同步修改。事前多画半小时图,能省后期几小时的删改。

2.

每个单元都有强测 / 评测器 / 互测,它们的角色其实都一样:找到我没想到的反例。一旦把测试视为"对自己实现的反方辩护",写代码时就会自然地多想几层"如果输入是 X 怎么办"。Unit 3 那个 \exists 谓词 的 bug,就是因为我没把"如果同一视频被 forward 多次"这种情况想到——单删一份的代码看起来对,跑起来没问题,但一旦敌方挑出特定输入立刻就挂。

七、结语

感谢课程组、感谢助教、感谢一起讨论的同学,也感谢 4 个月里评测失败再爬起来的自己。

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

308

社区成员

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

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