OO unit3博客

于忠雨-23371320 2026-05-28 11:16:23

Unit3 博客作业

一、JML 与规格驱动开发的理解

经过 Unit3 三次代码作业的实践,我对 JML和规格驱动开发有了从理论到实践的深入理解。

JML 是一种契约式编程的体现。 在 hw9 初次接触 JML 时,我习惯性地先读方法名和参数,然后直接开始写实现。结果在 queryMutualFollowingSum 等方法上频繁出现与预期不符的行为。后来我才意识到,JML 的 requiresensuresassignablesignals 等子句实际上是在方法实现者与调用者之间建立了一份严格契约。例如 queryMutualFollowingSum 的 JML 规格明确使用 \sum 量词定义了互关对的计数方式,任何偏离此定义的实现(如简单地用单向关注数除以 2)都是违约。

规格先行,实现后置。 在 hw10 实现 clean_spam_comments 时,JML 中对 commentIdscommentContents 的严格对应关系、删除后剩余元素的保序性、以及 result[0]result[1] 的精确语义,都让我在编码前先完成了逻辑梳理。这种"先读透规格,再动笔实现"的开发模式,显著降低了我因理解偏差导致的返工。

puresafe 的边界意识。 hw11 的 recommendNthUp 被标注为 pure,这要求方法不能修改任何对象状态。在编写 JUnit 测试时,我通过 strictEquals 对比调用前后 Network 中所有 User 对象的状态,确保没有隐蔽的副作用。而 safe 标注则提醒我:虽然允许修改 JML 中明确提及的容器,但绝不能触碰规格之外的对象或属性。这种边界意识对于维护大型系统的可预测性至关重要。

二、JUnit 测试经验总结

三次作业的 JUnit 测试要求让我对单元测试有了系统性的认识,总结为以下几点:

1. 测试不仅要测正确性,还要测"不变性"

在 hw9 测试 queryMutualFollowingSum 时,我最初只验证了返回值是否正确。后来通过阅读指导书,我意识到 pure 方法的测试必须验证调用前后系统状态完全一致。因此我设计了 recordFollowingMatrixassertStateUnchanged 辅助方法,在多次调用 queryMutualFollowingSum 前后比对所有用户的关注关系矩阵,确保没有隐蔽的副作用。

2. 边界条件与异常路径必须覆盖

hw10 的 clean_spam_comments 测试是我编写得最完整的一次。除了正常的删除场景,我还专门设计了:

  • 无匹配关键词(result[0] == 0result[1] == 0
  • 全部评论都被删除
  • 关键词重叠匹配(如 "aaaa""aa" 出现 3 次)
  • 空字符串关键词
  • 连续两次清理的幂等性验证
  • 不影响其他视频的隔离性验证

这些边界测试在互测环节帮助我快速定位了若干隐蔽 bug。

3. 利用 strictEquals 进行深度状态校验

课程组提供的 strictEquals 方法极大地简化了状态一致性检查。在 hw11 测试 recommendNthUp 时,我通过 getUsers() 获取调用前后的用户数组,并对每个用户调用 strictEquals,确保推荐算法这个 pure 方法没有意外修改任何用户的属性。

4. 针对常见错误实现设计"陷阱"测试

在 hw9 中,我专门设计了 testCatchHalfUnidirectional 测试用例:构造 1-2 互关(1 对),同时存在 1→3 和 2→3 两条单向关注。如果实现者错误地将"单向关注总数 / 2"作为互关数,会得到 2,而正确答案是 1。这种针对常见错误模式的测试设计,是提升测试覆盖率的关键。

三、三次作业迭代分析

3.1 功能迭代脉络

hw9:基础社交网络骨架

  • 核心功能:add_userfollow_userupload_videowatch_videoquery_shortest_path 等。
  • 数据结构:使用 HashMap<Integer, UserInterface> 存储用户,HashMap<Integer, VideoInterface> 存储视频,便于 O(1) 查询。
  • 容器设计:User 中使用 ArrayList 维护 followingfollowersreceivedVideos

hw10:经济体系与互动机制

  • 新增属性:User 中增加 coinswatchedVideoslikedVideosmedalscontributors/contributionsVideo 中增加 playCountlikesforwardCountcoinscommentIds/commentContents
  • 新增业务:like_video(点赞/取消点赞)、coin_video(投币,涉及事务性操作:扣除观看者硬币、增加视频硬币、增加 UP 主硬币、更新贡献者列表)、forward_videosend_commentclean_spam_commentspurchase_medal
  • 迭代挑战:事务一致性。例如 coin_video 需要同时修改观看者、视频对象、UP 主三个实体的状态,JML 中 assignable 子句明确列出了所有可能被修改的字段,这要求我在实现时必须保证原子性逻辑。

hw11:智能推荐系统

  • 新增属性:User 中增加 typeCounts(各分区观看计数)、videos(用户发布的视频列表)、types(固定 7 个分区字符串数组)。
  • 新增业务:recommend_video(基于兴趣度的视频推荐)、recommend_Nth_up(基于 UP 主综合评分的排序推荐)、query_most_influential_upquery_user_profilequeryGlobalBestContributor
  • 算法引入:兴趣度计算 interest = typeCounts[i] * (totalVideos - watchedVideos.size() + 1),UP 主评分 computeUpScore = Σ interest[type] * influence[type]

3.2 容器与方法的迭代变化

容器层面的演进:

  • hw9 到 hw10:User 类从 3 个容器扩展到 8 个容器,新增了 ArrayList<<VideoInterface> 等复杂结构。这要求我在 strictEquals 中增加对新容器的深度比对。
  • hw10 到 hw11:引入了固定长度的 int[] typeCountsString[] types,用于支撑兴趣度计算。这种从动态容器到固定数组的变化,反映了业务需求从"记录关系"到"支持数值计算"的转变。

方法层面的演进:

  • hw9 的方法以查询和修改基础关系为主,JML 相对简单。
  • hw10 的方法引入了事务性操作(如 coin_video)和双向状态维护(如 like_video 的点赞/取消点赞切换逻辑)。
  • hw11 的方法则涉及排序与选择recommendNthUp 需要按 computeUpScore 降序排列,同分按 ID 升序),以及全局聚合计算queryGlobalBestContributor 需要遍历所有用户,对每个有贡献者的用户调用 queryBestContributor,再统计频率)。

3.3 性能瓶颈的发现与优化

BFS 最短路径: hw9 的 queryShortestPath 使用 BFS,时间复杂度 O(V+E)。由于互测数据量不大(3000 条指令),直接使用 HashMap 作为距离表即可满足要求。但如果数据规模进一步扩大,可能需要考虑双向 BFS 或预处理。

最长递减序列: hw10 的 queryLongestDecSeq 我采用了按年龄排序后 DP 的做法。最初我担心 O(V²) 的复杂度在 10000 条指令下会超时,但实际测试中发现用户数和关注关系密度有限,该复杂度在 10s 时限内完全可行。

推荐算法的遍历开销: hw11 的 recommendVideorecommendNthUp 都需要遍历全量视频或用户。在 hw11 中,recommendNthUp 的排序逻辑涉及对每个候选 UP 主调用 computeUpScore,而后者又要遍历 7 个分区类型。虽然当前数据规模下 O(N log N) 的排序可接受,但如果用户和视频数量级提升,这种"全量遍历+多重计算"的模式会成为明显瓶颈。这启示我:JML 规格只保证正确性,不保证性能,实际开发中需要在满足规格的前提下考虑缓存(如缓存 UP 主的影响力值)或增量更新策略。

四、Bug 分析与反思

Bug 1:类型混淆导致的编译错误(hw11)

在实现 purchaseMedal 时,我曾写出:

UserInterface uploader = getVideo(video.getUploaderId());

getVideo() 返回的 VideoInterface 赋值给 UserInterface 类型变量。这个错误虽然低级,但反映了在快速迭代中容易混淆"通过视频 ID 获取视频对象"和"通过用户 ID 获取用户对象"的逻辑。修复: 改为 getUser(video.getUploaderId())

Bug 2:JUnit 测试断言过于宽松(hw11)

最初编写 testRecommendNthUpBasic 时,我使用了:

assertTrue(res == 1 || res == 2);

这种模糊断言无法检测排序逻辑错误。当两个 UP 主评分相同时,JML 明确要求返回 ID 更小的那个。我后来将其修正为精确的 assertEquals(1, res),并增加了 testRecommendNthUpNotFollowingNotSelf 等测试,确保推荐结果既排除自身,也排除已关注用户。

Bug 3:缺少辅助方法导致的调用失败(hw11)

Network.recommendVideoqueryUserProfile 中需要通过 getWatchedVideos() 判断用户是否有观看记录,但 User 类最初未提供该方法。这导致 Network 中不得不进行不安全的类型转换或无法编译。修复:User 中补充 getWatchedVideos() 方法。

Bug 4:异常处理不精确(hw11)

queryGlobalBestContributor 中,我最初使用了 catch (Exception e) 来吞掉 queryBestContributor 可能抛出的异常。虽然逻辑上这些异常不会发生(因为有前置判断),但这种不精确的异常处理在静态检查中会触发 IDE 警告,且不符合 Java 最佳实践。修复: 改为精确捕获 UserIdNotFoundException | NoContributorsException

Bug 5:浮点改整数的回归问题(hw10 → hw11)

hw10 中 queryMostPopularVideobestHeat 使用 double 类型,因为 getHeat() 返回 double。hw11 指导书明确将 getHeatqueryMostPopularVideo 中的浮点类型改为整数类型。我在合并代码时差点遗漏这一变更,幸好通过回归测试及时发现。反思: 迭代开发中,指导书的"修改说明"必须逐条核对,不能假设旧代码完全兼容。

五、大模型辅助开发经验

在 Unit3 的开发过程中,我使用了大模型辅助完成部分工作,总结其优势与局限如下:

优势:

  1. JML 快速解读:面对复杂的 JML 规格(如 clean_spam_comments\forall 嵌套和 \num_of 量词),大模型能够帮助我将其"翻译"为更直观的自然语言描述,降低理解门槛。
  2. 测试用例生成:在编写 JUnit 时,大模型能够基于 JML 的 requiresensures 自动生成覆盖正常路径、异常路径和边界条件的测试框架,我只需在此基础上补充业务细节。
  3. 代码审查与重构:当我遇到 IDE 类型警告(如 getVideo 误用)时,大模型能够快速定位问题并给出修正建议,还能帮助优化代码风格(如将 equals("") 改为 isEmpty(),使用 Comparator.comparingInt 链式调用)。

局限:

  1. 忽视效率问题:大模型在根据 JML 生成代码时,倾向于直接翻译规格中的数学表达式,而不会主动考虑性能。例如 recommendNthUp 的排序实现,大模型给出的就是 O(N log N) 的全量排序,不会提示"是否可以缓存 UP 主评分"。
  2. 忽视架构一致性:在 hw11 迭代时,大模型建议我在 User 中新增 getWatchedVideos(),但并未主动提醒我需要同步更新 strictEquals 方法以包含新容器的比对。架构层面的一致性仍需开发者自己把控。
  3. 异常处理过于粗糙:大模型有时会生成 catch (Exception e) 这样的通用捕获块,需要开发者自行细化到具体的受检异常。

使用建议: 大模型适合作为"高级代码审查员"和"文档翻译器",但最终的架构决策、性能优化和边界条件验证仍需开发者基于对 JML 的深入理解来完成。

六、关于规格化协作的思考(击鼓传花游戏感悟)

由于外出差未能参加线下的 JML "击鼓传花" 研讨课,但我结合三次作业中自己写 JML、读 JML、改代码的经历,对多人协作中的规格统一有以下思考:

1. 需求边界必须显式化
JML 的价值在于将模糊的自然语言需求转化为形式化的约束。在多人协作中,如果只有口头沟通,很容易出现"我以为 A,他以为 B"的情况。例如 recommendNthUp 中"同分按 ID 排序"这一规则,如果不在 JML 中显式写出(\result 的 ensures 中使用 \min 约束),不同开发者可能实现为随机返回或按插入顺序返回。

2. 减少信息差的措施

  • 接口先行:在编码前,团队应共同评审 JML 规格,确保所有人对 requiresensures 和异常行为达成一致。
  • 提供可运行的测试契约:除了 JML,还应提供一组"黄金测试用例"作为活文档。例如我在 hw11 中编写的 testRecommendNthUpNotFollowingNotSelf,就直接表达了"推荐不能包含已关注用户"的业务规则。
  • 严格的状态不变式invariant 子句是团队协作的"底线"。在 hw10 中,contributors.length == contributions.length 这一不变式确保了任何修改这两个容器的开发者都必须同步维护其长度一致性。

3. 迭代中的规格演化
从 hw9 到 hw11,我深刻体会到规格不是一成不变的。当 hw11 在 User 中新增 typeCounts 时,所有依赖 User 状态的方法(如 strictEquals)都需要同步更新。这提示在团队开发中,任何规格变更都必须有变更日志和影响面分析,否则会导致模块间的不一致。

结语

Unit3 的三次作业让我从"凭感觉写代码"转变为"按契约写代码"。JML 虽然在初期增加了阅读和理解成本,但在迭代开发和团队协作中展现出了强大的规范性价值。JUnit 测试则让我养成了"写一点,测一点,边界全覆盖"的开发习惯。这些经验不仅适用于课程作业,也为今后参与大型软件项目的规格化开发打下了坚实基础。

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

305

社区成员

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

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