305
社区成员
发帖
与我相关
我的任务
分享经过 Unit3 三次代码作业的实践,我对 JML和规格驱动开发有了从理论到实践的深入理解。
JML 是一种契约式编程的体现。 在 hw9 初次接触 JML 时,我习惯性地先读方法名和参数,然后直接开始写实现。结果在 queryMutualFollowingSum 等方法上频繁出现与预期不符的行为。后来我才意识到,JML 的 requires、ensures、assignable 和 signals 等子句实际上是在方法实现者与调用者之间建立了一份严格契约。例如 queryMutualFollowingSum 的 JML 规格明确使用 \sum 量词定义了互关对的计数方式,任何偏离此定义的实现(如简单地用单向关注数除以 2)都是违约。
规格先行,实现后置。 在 hw10 实现 clean_spam_comments 时,JML 中对 commentIds 和 commentContents 的严格对应关系、删除后剩余元素的保序性、以及 result[0] 和 result[1] 的精确语义,都让我在编码前先完成了逻辑梳理。这种"先读透规格,再动笔实现"的开发模式,显著降低了我因理解偏差导致的返工。
pure 与 safe 的边界意识。 hw11 的 recommendNthUp 被标注为 pure,这要求方法不能修改任何对象状态。在编写 JUnit 测试时,我通过 strictEquals 对比调用前后 Network 中所有 User 对象的状态,确保没有隐蔽的副作用。而 safe 标注则提醒我:虽然允许修改 JML 中明确提及的容器,但绝不能触碰规格之外的对象或属性。这种边界意识对于维护大型系统的可预测性至关重要。
三次作业的 JUnit 测试要求让我对单元测试有了系统性的认识,总结为以下几点:
在 hw9 测试 queryMutualFollowingSum 时,我最初只验证了返回值是否正确。后来通过阅读指导书,我意识到 pure 方法的测试必须验证调用前后系统状态完全一致。因此我设计了 recordFollowingMatrix 和 assertStateUnchanged 辅助方法,在多次调用 queryMutualFollowingSum 前后比对所有用户的关注关系矩阵,确保没有隐蔽的副作用。
hw10 的 clean_spam_comments 测试是我编写得最完整的一次。除了正常的删除场景,我还专门设计了:
result[0] == 0 且 result[1] == 0)"aaaa" 中 "aa" 出现 3 次)这些边界测试在互测环节帮助我快速定位了若干隐蔽 bug。
strictEquals 进行深度状态校验课程组提供的 strictEquals 方法极大地简化了状态一致性检查。在 hw11 测试 recommendNthUp 时,我通过 getUsers() 获取调用前后的用户数组,并对每个用户调用 strictEquals,确保推荐算法这个 pure 方法没有意外修改任何用户的属性。
在 hw9 中,我专门设计了 testCatchHalfUnidirectional 测试用例:构造 1-2 互关(1 对),同时存在 1→3 和 2→3 两条单向关注。如果实现者错误地将"单向关注总数 / 2"作为互关数,会得到 2,而正确答案是 1。这种针对常见错误模式的测试设计,是提升测试覆盖率的关键。
hw9:基础社交网络骨架
add_user、follow_user、upload_video、watch_video、query_shortest_path 等。HashMap<Integer, UserInterface> 存储用户,HashMap<Integer, VideoInterface> 存储视频,便于 O(1) 查询。User 中使用 ArrayList 维护 following、followers、receivedVideos。hw10:经济体系与互动机制
User 中增加 coins、watchedVideos、likedVideos、medals、contributors/contributions;Video 中增加 playCount、likes、forwardCount、coins、commentIds/commentContents。like_video(点赞/取消点赞)、coin_video(投币,涉及事务性操作:扣除观看者硬币、增加视频硬币、增加 UP 主硬币、更新贡献者列表)、forward_video、send_comment、clean_spam_comments、purchase_medal。coin_video 需要同时修改观看者、视频对象、UP 主三个实体的状态,JML 中 assignable 子句明确列出了所有可能被修改的字段,这要求我在实现时必须保证原子性逻辑。hw11:智能推荐系统
User 中增加 typeCounts(各分区观看计数)、videos(用户发布的视频列表)、types(固定 7 个分区字符串数组)。recommend_video(基于兴趣度的视频推荐)、recommend_Nth_up(基于 UP 主综合评分的排序推荐)、query_most_influential_up、query_user_profile、queryGlobalBestContributor。interest = typeCounts[i] * (totalVideos - watchedVideos.size() + 1),UP 主评分 computeUpScore = Σ interest[type] * influence[type]。容器层面的演进:
User 类从 3 个容器扩展到 8 个容器,新增了 ArrayList<<VideoInterface> 等复杂结构。这要求我在 strictEquals 中增加对新容器的深度比对。int[] typeCounts 和 String[] types,用于支撑兴趣度计算。这种从动态容器到固定数组的变化,反映了业务需求从"记录关系"到"支持数值计算"的转变。方法层面的演进:
coin_video)和双向状态维护(如 like_video 的点赞/取消点赞切换逻辑)。recommendNthUp 需要按 computeUpScore 降序排列,同分按 ID 升序),以及全局聚合计算(queryGlobalBestContributor 需要遍历所有用户,对每个有贡献者的用户调用 queryBestContributor,再统计频率)。BFS 最短路径: hw9 的 queryShortestPath 使用 BFS,时间复杂度 O(V+E)。由于互测数据量不大(3000 条指令),直接使用 HashMap 作为距离表即可满足要求。但如果数据规模进一步扩大,可能需要考虑双向 BFS 或预处理。
最长递减序列: hw10 的 queryLongestDecSeq 我采用了按年龄排序后 DP 的做法。最初我担心 O(V²) 的复杂度在 10000 条指令下会超时,但实际测试中发现用户数和关注关系密度有限,该复杂度在 10s 时限内完全可行。
推荐算法的遍历开销: hw11 的 recommendVideo 和 recommendNthUp 都需要遍历全量视频或用户。在 hw11 中,recommendNthUp 的排序逻辑涉及对每个候选 UP 主调用 computeUpScore,而后者又要遍历 7 个分区类型。虽然当前数据规模下 O(N log N) 的排序可接受,但如果用户和视频数量级提升,这种"全量遍历+多重计算"的模式会成为明显瓶颈。这启示我:JML 规格只保证正确性,不保证性能,实际开发中需要在满足规格的前提下考虑缓存(如缓存 UP 主的影响力值)或增量更新策略。
在实现 purchaseMedal 时,我曾写出:
UserInterface uploader = getVideo(video.getUploaderId());
将 getVideo() 返回的 VideoInterface 赋值给 UserInterface 类型变量。这个错误虽然低级,但反映了在快速迭代中容易混淆"通过视频 ID 获取视频对象"和"通过用户 ID 获取用户对象"的逻辑。修复: 改为 getUser(video.getUploaderId())。
最初编写 testRecommendNthUpBasic 时,我使用了:
assertTrue(res == 1 || res == 2);
这种模糊断言无法检测排序逻辑错误。当两个 UP 主评分相同时,JML 明确要求返回 ID 更小的那个。我后来将其修正为精确的 assertEquals(1, res),并增加了 testRecommendNthUpNotFollowingNotSelf 等测试,确保推荐结果既排除自身,也排除已关注用户。
Network.recommendVideo 和 queryUserProfile 中需要通过 getWatchedVideos() 判断用户是否有观看记录,但 User 类最初未提供该方法。这导致 Network 中不得不进行不安全的类型转换或无法编译。修复: 在 User 中补充 getWatchedVideos() 方法。
在 queryGlobalBestContributor 中,我最初使用了 catch (Exception e) 来吞掉 queryBestContributor 可能抛出的异常。虽然逻辑上这些异常不会发生(因为有前置判断),但这种不精确的异常处理在静态检查中会触发 IDE 警告,且不符合 Java 最佳实践。修复: 改为精确捕获 UserIdNotFoundException | NoContributorsException。
hw10 中 queryMostPopularVideo 的 bestHeat 使用 double 类型,因为 getHeat() 返回 double。hw11 指导书明确将 getHeat 和 queryMostPopularVideo 中的浮点类型改为整数类型。我在合并代码时差点遗漏这一变更,幸好通过回归测试及时发现。反思: 迭代开发中,指导书的"修改说明"必须逐条核对,不能假设旧代码完全兼容。
在 Unit3 的开发过程中,我使用了大模型辅助完成部分工作,总结其优势与局限如下:
优势:
clean_spam_comments 的 \forall 嵌套和 \num_of 量词),大模型能够帮助我将其"翻译"为更直观的自然语言描述,降低理解门槛。requires 和 ensures 自动生成覆盖正常路径、异常路径和边界条件的测试框架,我只需在此基础上补充业务细节。getVideo 误用)时,大模型能够快速定位问题并给出修正建议,还能帮助优化代码风格(如将 equals("") 改为 isEmpty(),使用 Comparator.comparingInt 链式调用)。局限:
recommendNthUp 的排序实现,大模型给出的就是 O(N log N) 的全量排序,不会提示"是否可以缓存 UP 主评分"。User 中新增 getWatchedVideos(),但并未主动提醒我需要同步更新 strictEquals 方法以包含新容器的比对。架构层面的一致性仍需开发者自己把控。catch (Exception e) 这样的通用捕获块,需要开发者自行细化到具体的受检异常。使用建议: 大模型适合作为"高级代码审查员"和"文档翻译器",但最终的架构决策、性能优化和边界条件验证仍需开发者基于对 JML 的深入理解来完成。
由于外出差未能参加线下的 JML "击鼓传花" 研讨课,但我结合三次作业中自己写 JML、读 JML、改代码的经历,对多人协作中的规格统一有以下思考:
1. 需求边界必须显式化
JML 的价值在于将模糊的自然语言需求转化为形式化的约束。在多人协作中,如果只有口头沟通,很容易出现"我以为 A,他以为 B"的情况。例如 recommendNthUp 中"同分按 ID 排序"这一规则,如果不在 JML 中显式写出(\result 的 ensures 中使用 \min 约束),不同开发者可能实现为随机返回或按插入顺序返回。
2. 减少信息差的措施
requires、ensures 和异常行为达成一致。testRecommendNthUpNotFollowingNotSelf,就直接表达了"推荐不能包含已关注用户"的业务规则。invariant 子句是团队协作的"底线"。在 hw10 中,contributors.length == contributions.length 这一不变式确保了任何修改这两个容器的开发者都必须同步维护其长度一致性。3. 迭代中的规格演化
从 hw9 到 hw11,我深刻体会到规格不是一成不变的。当 hw11 在 User 中新增 typeCounts 时,所有依赖 User 状态的方法(如 strictEquals)都需要同步更新。这提示在团队开发中,任何规格变更都必须有变更日志和影响面分析,否则会导致模块间的不一致。
Unit3 的三次作业让我从"凭感觉写代码"转变为"按契约写代码"。JML 虽然在初期增加了阅读和理解成本,但在迭代开发和团队协作中展现出了强大的规范性价值。JUnit 测试则让我养成了"写一点,测一点,边界全覆盖"的开发习惯。这些经验不仅适用于课程作业,也为今后参与大型软件项目的规格化开发打下了坚实基础。