OO Unit3 总结

曾立宏-73066204 2026-06-09 14:50:24

OO Unit3 总结

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

JML 把“需求”拆成了更接近程序行为的形式。自然语言指导书会描述业务背景,例如用户关注、视频互动、推荐视频;而 JML 更关注一个方法在调用前后必须满足什么条件:哪些对象应该存在,哪些异常应该抛出,哪些容器应该改变,哪些状态必须保持不变。

在 JML 约束下,除了能跑,还必须证明这个函数没有改变任何不该改变的状态。例如 queryMutualFollowingSumrecommendNthUp 这类方法,从业务上看只是查询,但如果在查询时顺手重排用户列表、补全缓存或修改中间容器,就可能违反 pure 语义。

另外,JML 也重视异常顺序。当很多错误输入同时满足多个异常条件时,最终抛出的异常必须和规格一致。实现时必须要按照规格规定的优先级逐项检查。

二、三次作业的架构迭代

1. 第一次作业:基础社交网络

第一次规格化作业的核心对象是 UserVideoNetwork。我的整体设计比较直接:

  • Network 使用 HashMap<Integer, User> 维护用户,用 HashMap<Integer, Video> 维护视频,保证按 id 查询是常数级。
  • User 内部维护关注列表、粉丝列表、粉丝年龄段计数,以及收到但未观看的视频列表。
  • Video 只保存视频 id 和上传者 id。

这次作业中最需要注意的是关注关系带来的派生状态。queryMutualFollowingSum 如果每次查询都遍历所有用户对,复杂度会比较高,而且 pure 方法中临时修正状态也不合适。因此我在 Network 中维护了 mutualFollowingSum,在 followUserunfollowUser 时增量更新。这样查询时可以直接返回缓存值。

另一个性能点是用户收到的视频。指导书中 query_received_unwatched_videos 只需要返回前若干个未观看视频,因此我没有只用普通列表线性删除,而是用链表节点加索引的方式维护未观看视频。这样在 watchVideo 时可以通过索引找到节点并删除,同时保留接收顺序。

queryShortestPath 则是标准图搜索问题。我使用 BFS,从起点沿关注关系扩展,找到终点后返回距离;如果不可达则抛出 UncessException

2. 第二次作业:互动系统和事务性操作

第二次迭代在原有社交网络上加入了大量业务状态:

  • 用户新增硬币余额、观看历史、点赞记录、勋章集合、贡献者及贡献数。
  • 视频新增分区类型、播放数、点赞数、转发数、投币数和评论区。
  • Network 新增充值、点赞、投币、转发、评论、清理垃圾评论、购买勋章等方法。

这一轮最大的变化是操作更像“事务”。例如投币不仅要修改观看者硬币,还要修改 UP 主硬币、视频硬币数和 UP 主收到的贡献记录。只要前置条件不满足,就不能改动任何状态。因此实现时必须先完成所有合法性检查,再统一执行状态修改。

评论区我使用 LinkedHashMap<Integer, String> 保存评论 id 和内容。这样既能快速判断评论 id 是否重复,又能保留评论插入顺序,便于 getCommentIds()getCommentContents() 返回一一对应的结果。cleanSpamComments 则遍历评论,删除包含关键字的评论,并统计被删除评论中关键字出现次数的最大值。

这次迭代也暴露出旧容器设计需要调整的问题。第一次作业中,一个视频对一个用户而言只需要判断“是否收到”;第二次作业出现转发后,同一个视频可能通过不同路径进入未观看列表。于是未观看视频索引从单个节点扩展为 videoId -> 节点队列,避免同一视频多次接收时删除逻辑错误。

3. 第三次作业:推荐系统

第三次迭代继续保留前两次功能,并加入推荐视频、推荐 UP 主、查询最有影响力 UP 主、用户兴趣画像、全局最佳贡献者等功能。

这次主要增加了两个和推荐有关的状态:

  • User 中维护 typeCounts,记录用户在每个分区观看过的视频数量。
  • User 中维护自己上传的视频集合,用于计算某个 UP 主在指定分区的影响力。

推荐视频的核心是把视频热度和用户兴趣相乘。用户兴趣由观看历史决定,因此每次 watchVideo 时必须同步更新 typeCounts。推荐 UP 主则要排除自己和已经关注的用户,然后根据当前用户兴趣与候选 UP 主影响力计算分数,再按分数和 id 进行排序。

第三次作业还有一个细节:指导书说明 hw10 中原有的 queryMostPopularVideogetHeat 的浮点类型改为整数类型。这个变化看似很小,但会影响比较逻辑和接口签名。如果只是在旧代码上局部添加推荐方法,而不重新核对接口,就很容易遗漏这种既有方法的变化。

三、如何发现迭代中的变化

三次作业共用同一套业务背景,因此最危险的地方不是新增方法,而是旧方法和旧容器在新规格下含义发生了变化。我的做法是把变化分成三类:

第一类是构造方法和接口签名变化。例如 Video 在第一次作业中构造方法是 Video(int id, int uploaderId),第二次开始变成 Video(int id, int uploaderId, String type)。这种变化必须优先处理,否则 Runner 无法正确反射构造对象。

第二类是已有状态的语义变化。例如“未观看视频”在第一次作业中更像一个去重集合,但转发功能加入后,同一视频可能重复出现在接收列表中,因此原来的 HashMap<Integer, VideoNode> 不够,需要扩展成 HashMap<Integer, ArrayDeque<VideoNode>>

第三类是查询方法的性能变化。例如互相关注数、年龄段比例、用户兴趣度都可以每次查询时重新遍历计算,但当查询频繁时就会成为性能瓶颈。因此我把一些高频派生结果维护成增量状态:关注关系改变时更新 mutualFollowingSum,粉丝改变时更新年龄桶,观看视频时更新分区观看次数。

这种方式的核心是:不要只看这次新增了什么方法,还要检查旧容器是否仍然能表达新规格。

四、性能瓶颈与优化取舍

本单元的数据规模虽然不是特别大,但如果完全按 JML 的量词形式直译成多重循环,仍然可能超时。

queryMutualFollowingSum 是最典型的例子。规格可以理解为统计所有互相关注对,如果查询时双重遍历用户,复杂度是 O(n^2)。我将其维护为缓存值,在关注和取关时增量更新,使查询变成 O(1)

queryUpFollowersAgeRatio 也类似。如果每次都遍历 UP 主所有粉丝并分类,在大量查询下会重复计算。因此我在 User 中维护四个年龄段的粉丝计数,关注和取关时更新。

未观看视频列表的瓶颈在删除。单纯用 ArrayList 保存接收顺序,观看视频时删除某个视频需要线性查找;使用链表节点和索引后,可以在已知 videoId 的情况下快速定位节点,同时保留顺序。

推荐系统的复杂度主要来自扫描候选视频和候选 UP 主。recommendVideo 需要遍历所有视频计算分数,recommendNthUp 需要筛选候选用户并排序。由于数据规模不大,这种实现还在可以接受的范围内。

五、JUnit 测试经验

本单元的 JUnit 测试要求测试能够区分目标方法是否符合规格。因此测试不能只覆盖一个样例,而要专门针对 JML 中容易写错的部分设计数据。

第九次作业测试 queryMutualFollowingSum 时,我主要覆盖了:

  • 空网络返回 0。
  • 单向关注不计入互相关注。
  • 多个互相关注对能够正确计数。
  • 取关后缓存值必须同步减少。
  • 重复查询不应改变用户状态,验证 pure 语义。

第十次作业测试 cleanSpamComments 时,我重点关注评论 id 与内容的对应关系:

  • 视频不存在时抛出 VideoIdNotFoundException
  • 没有评论、没有命中关键字时返回 {0, 0} 且评论区不变。
  • 删除部分评论后,剩余评论顺序和 id/content 对应关系保持正确。
  • 关键字多次出现时,第二个返回值应为被删除评论中出现次数的最大值。
  • 连续清理时,第二次应基于第一次清理后的状态。

第十一次作业测试 recommendNthUp 时,我主要覆盖:

  • 按分数排序,分数相同时按 id 处理。
  • 候选 UP 主应排除自己和已经关注的用户。
  • rank 非法、用户不存在、没有视频、候选不足等异常场景。
  • 推荐查询本身应保持 pure,不改变用户、视频、关注关系、兴趣画像等状态。

JUnit 不只是为了提高覆盖率,更重要的是把规格中隐含的边界条件显式化。尤其是 pure 和异常顺序,如果没有专门测试,很容易在实现时忽略。

六、Bug 与问题分析

本单元中没有出现不能通过评测或互测的bug。

七、关于使用 Code Agent 的体会

随着大模型能力增强,在有完整规格的情况下 Code Agent 已经能独立完成本单元从阅读规格、实现到测试的所有工作。

它的优势在于能快速根据接口和已有代码列出可能受影响的容器、方法和测试点。例如在迭代时,可以让它帮助检查哪些旧方法签名发生变化,哪些查询方法可能违反 pure,哪些容器在新增需求后表达能力不足。它也适合根据 JML 描述生成边界测试,例如异常顺序、重复元素、状态不变性等。

需要注意的点是,如果直接让模型根据 JML 写完整实现,它可能更关注“功能上能跑通”,而忽略复杂度这种没有显式说明的约束,需要在提示词显式说明。

八、总结

第三单元体现了规格化设计的价值。JML 把需求变成了可检查的契约,使实现者要全面关注完整的前置条件、后置条件、异常行为和副作用范围。JUnit 测试则是连接规格和实现的工具。它不仅验证结果,也验证状态是否被错误修改、异常是否按顺序抛出、容器关系是否仍然一致。在大模型时代,严格的契约和测试是保证程序的具体实现不跑偏的重要手段。

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

309

社区成员

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

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