305
社区成员
发帖
与我相关
我的任务
分享JML 是一种形式化的规格描述语言,它将“契约式设计”落到 Java 代码上。通过 requires、ensures、assignable、signals 等关键字,我们能精确地定义方法的前置条件(调用者的义务)、后置条件(实现者的承诺)、副作用(允许修改的范围)以及异常行为。
规格驱动开发的核心价值在于:
三次作业下来,我深刻体会到:好的规格非常有用,让调用者和实现者各司其职,一旦违反契约,要么是调用方输入不合法(抛异常),要么是实现方逻辑有误(测试失败)。这种“按契约编程”的思维方式,让代码的可靠性有了坚实的理论支撑。
在实践“基于规格的自动化测试”的过程中,我积累了以下方法:
状态快照 + 副作用验证
在 TestNetwork 中,我编写了 assertJMLBehavior() 方法:每次调用纯查询方法前,保存所有 User 的 id、age、name、关注关系、粉丝关系,以及所有 Video 的评论列表等完整快照;调用后逐项比对,确保 pure 方法和 assignable \nothing 的方法没有“偷偷”修改任何数据。在 HW11 的 RecommendNthUpTest 中,我还更加严格地验证了推荐方法不会改变用户状态。
结果正确性断言
将 JML 中的 ensures 表达式字面意思用最简单粗暴的方式直接翻译成 Java 代码。例如 queryMutualFollowingSum 的期望值通过双重循环手工计算,与返回值对比。对于 cleanSpamComments,我手动统计了期望的删除数量和最大关键字出现次数,并与方法返回值比对。
幂等性检查pure 方法应当不改变对象任何状态,所以对 pure 方法连续调用两次,断言结果应当相同。如果不相同则可以说明方法内部偷偷改变了对象数据。
异常优先级测试
在第三次作业中,针对 recommendNthUp 方法,我专门写了测试用例分别触发 UserIdNotFoundException、InvalidRankException、NoVideoUploadedException、ColdStartUserException,验证异常抛出的顺序与规格一致。
这些测试帮助我在迭代重构时迅速发现问题,保证了代码的正确性。
| 作业 | 新增功能 | 容器 / 数据结构变化 |
|---|---|---|
| HW9 | 基础用户/视频管理、关注、互关计数、未观看视频队列 | HashMap 存用户/视频,HashSet 存关注/粉丝,LinkedList+HashSet 存未观看视频 |
| HW10 | 点赞/投币/转发、贡献者统计、勋章、评论、垃圾清理、最长递减序列 | 增加 HashMap 类型的 coinRecord 增量维护最佳贡献者;最长递减序列增加缓存失效机制 |
| HW11 | 推荐视频、推荐UP主、影响力、兴趣画像、全局最佳贡献者 | 未观看视频队列改为手写双向链表+HashMap;增加 typeCounts 和 typeInfluences 数组;queryLongestDecSeq 增加缓存;queryGlobalBestContributor 临时构建统计 |
每次阅读新规格时,我会先对比新增的方法和已有的数据字段,思考:
watchVideo 需要从未观看队列中删除任意视频(不仅是头部)。原先的 LinkedList + remove(Integer) 是 O(n),而且需要遍历。我意识到可以改造成双向链表 + 映射,于是实现了 VideoNode 内部类,用 head/tail 指针和 receivedVideoMap 实现 O(1) 删除。queryLongestDecSeq,我最初每次调用都 O(n²) 计算,后来发现图变化(增删用户、关注关系)远少于查询次数,于是增加 isCacheValid 标记,在 addUser、followUser、unfollowUser 中置为 false,查询时按需计算一次并缓存。recommendVideo 每次 O(m) 遍历所有视频,但指令条数在 10000 条以内,可以接受。三次作业我均顺利通过强测和互测,未出现功能性 bug。
游戏中,我注意到几个常见现象:
在作业中,我尝试过用大模型辅助编程,在 JML 的约束下大模型能快速生成结构基本正确的代码。但是大模型很容易忽略效率问题和可扩展性,在我询问性能优化建议时也很容易分析错误,给出没有必要甚至负向优化的方案,往往需要我批判地采纳,并重新设计数据结构和优化方式。
对于单元测试,大模型根据 JML 中的 ensures 和 signals 生成的断言代码往往检查不够全面,仍然需要我多次排查快照对比、副作用验证中遗漏的部分。
Unit3 让我真正理解了“契约式设计”的力量。JML 将模糊的自然语言需求转化为精确的、可验证的逻辑公式,而基于规格的 JUnit 测试则确保了实现与规格的一致性。三次作业的迭代过程也锻炼了我根据规格推导数据结构和算法的能力,不是机械地翻译规格,而是设计高效的增量维护策略。规格驱动开发,不仅是编程技巧,更是一种可靠的工程思想。