308
社区成员
发帖
与我相关
我的任务
分享回头看 HW13 一阶提交后,我得到的最有价值的反馈不是"R1 通过"那行绿色字,而是评测器对照出的几条信息:
Library,二阶因为已开始照官方包写代码改成了 LibraryManager,相似度评测当场把我抓住。phase >= 2 的 guard 想让状态机更"严密",结果几条迁移合在一起 guard 解不开。HW14 我在二阶提交前临时加了一个 GradingOffice 类来管理评分,差点没在类图里同步——这正是评测要拦截的:
R2 / R3 检查:
- 类图中每个类的属性 → 与程序中类的属性一致
- 类图中每个类的方法 → 与程序中类的方法一致
- 检测覆盖率:60% 的属性 / 方法对应
二阶相当于一份"完工验收"。任何二阶时代码长出来的新字段、新方法,都必须在类图里有对应;反之亦然。
| 作业 | 主线 | 新增设计单元 | 新增方法(关键) |
|---|---|---|---|
| HW13 | 基础借阅 + 预约 + 评分 | 12 个:MainClass、LibraryManager、Place、Bookshelf、TreasuredBookshelf、BorrowReturnOffice、AppointmentOffice、ReadingRoom、GradingOffice、Book、User、Order | borrow/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%。
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:从池中给待送达预约挑书,移动到 aoredistribute:池里剩余的副本按当前 rating 回归 bs 或 tbs3. HW15 引入 CreditOffice
最初考虑把信用分直接放进 User,但很快发现:
User 会让它臃肿User 与"积分逻辑"是两个变化频率不同的维度最终独立出 CreditOffice,User 只持有自己的状态(持有图书、预约、阅读状态),CreditOffice 集中所有积分增减与判定。这种 SRP 拆分在后续添加新规则(如"特殊用户白名单")时只动 CreditOffice 一个文件即可。
本单元我大量使用了Claude来辅助mdj文件的生成、状态图的设计、以及代码 / 图一致性的检查。比起前几单元更纯粹的"代码助手",本单元的大模型使用更像"建模顾问"。
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。
总结下来,我的几条实践经验:
@Trigger 注解语法,全部贴进 system prompt,相当于给它一份"评测器视角"。第一单元的最大突破是 从多项式表示到 AST。第一次作业写完后,我手里是一个 Map<Integer, BigInteger> 表示多项式系数的"过程式实现";到第二次作业增加 exp、选择因子、自定义函数时这个表示就崩了,必须重构成 AST。
这次重构是我第一次体会到 抽象基类 + 子类继承 + 递归遍历 的威力——所有节点都实现统一接口,求导 / 化简 / 代入只是不同的 visitor。后续要加 sin / cos 因子时,只用新增节点类,主流程一行不改。
主要收获:面向接口而非面向实现。
第二单元的核心难度从"算法"转移到"线程同步"。三次作业的架构演进是:
volatile 与精细锁Shaft 锁层级最大的思维跃迁是把锁视为资源:不同的资源用不同的锁保护,避免嵌套;用 synchronized(queue) 保护任务队列、用 synchronized(shaft) 保护井道位置,二者完全解耦。
另一个收获是 Producer-Consumer 模式的高内聚低耦合:调度器只与队列通信、电梯只与队列通信,调度算法的修改不会影响电梯运行逻辑。
主要收获:用模式名思考问题
第三单元的范式是 JML 规格优先。这一单元我几乎没"设计架构"——架构基本由官方包定死,我要做的是按 JML 字面意思实现每个方法。这种约束反而让我学到了:
ArrayList 改成 HashMap、把 queryMutualFollowingSum 从 O(n²) 改成增量维护,对外行为不变。getInterest 返回 typeCounts[i]*..." 这样的不变量明确写出来。UID → InvalidRank → NoVideoUploaded → ColdStartUser,这样的优先级关系不是实现细节,而是规格的一部分。主要收获:契约式设计比写代码更早
第四单元在 Unit 3 的基础上再前进一步——把 UML 模型作为单一真理源。代码、状态图、顺序图、@Trigger 注解、@SendMessage 注解,都要围绕同一份 UML 模型展开。
这种风格最大的好处是:架构与实现的差异被自动揭露。如果代码里方法签名漂移,R2 会抓住;如果状态图里 guard 不可满足,R3 会抓住;如果顺序图里消息不存在对应方法,R3 会抓住。
主要收获:正向建模需要"先慢后快"的克制力
最早的测试基本就是 手工拍脑袋写边界:超大的 BigInteger、20 层括号嵌套、递推函数到 f{20} 这样的极端深度。bug 主要出现在复杂度高的方法(圈复杂度 >5 的 Parser.parseFactorInternal、Expr.derive、Parser.expandRecursive),低复杂度方法基本不出问题。
并发测试与之前完全不同:bug 可能在 100 次跑中只出现 1 次,而且复现取决于线程调度。我学到的方法:
for i in {1..500}; do java MainClass < input.txt; done,统计错误率HW7 的一个 wait() 死锁就是这样定位的:100 次中只有 3-4 次复现,但 stderr 日志显示备用电梯一直在 sleepQuietly 自旋。
核心思维:线程相关的 bug 不能靠跑得过来判断没问题。
Unit 3 第一次让我接触 JUnit 单元测试。最大的方法论是反向验证:
这种"用规格的别处部分校验当下结果"的写法比写死期望值更稳健。另一个发现是 assignable \nothing 检查——用孪生 Network 验证 pure 方法不改状态。
核心思维:规格本身就是测试用例的来源。
Unit 4 的测试同时关注三类断言:
1.
四个单元的所有"重构事故",回头看都是因为前期没想清楚就开始写。Unit 1 的多项式 → AST 重构、Unit 2 的双轿厢 Shaft 锁、Unit 3 的容器选型替换、Unit 4 的 phase 变量删除——每一次重构的代价都是几百行代码的同步修改。事前多画半小时图,能省后期几小时的删改。
2.
每个单元都有强测 / 评测器 / 互测,它们的角色其实都一样:找到我没想到的反例。一旦把测试视为"对自己实现的反方辩护",写代码时就会自然地多想几层"如果输入是 X 怎么办"。Unit 3 那个 \exists 谓词 的 bug,就是因为我没把"如果同一视频被 forward 多次"这种情况想到——单删一份的代码看起来对,跑起来没问题,但一旦敌方挑出特定输入立刻就挂。
感谢课程组、感谢助教、感谢一起讨论的同学,也感谢 4 个月里评测失败再爬起来的自己。