OO Unit 3:规格驱动开发与 JML 实践

高晨凯-24373278 2026-06-09 02:12:06

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

规格驱动开发的核心在于“先契约,后实现”。JML 就像是在架构设计者和实际开发者之间的一份具有强制性的合同。

  • 离了“做什么”与“怎么做”:JML 极其详尽地规定了前置条件(requires)、后置条件(ensures)以及副作用(assignable / pure)。在编写代码时,不再需要去猜测业务的隐藏逻辑,一切以规格为准。
  • 消除自然语言的二义性:自然语言很容易出现歧义,但数学逻辑不会。通过 \forall\exists\sum 等量词的组合,JML 能够绝对精确地定义状态的变化。
  • 严苛的约束感:规格是底线。任何超出规格的行为(例如在 @pure 方法中修改了对象的内部状态),即便在当前业务流中看似无害,在更庞大的系统中也往往为未来可能的致命 Bug 埋下伏笔。

二、JUnit 测试的经验总结

  • Pure 方法的快照防重叠漏洞(Alias 污染)
    在测试 @pure 契约时,我曾构建了一个全知快照系统(Omniscient Snapshot System)来进行 beforeafter 的深度对比。最初,我直接使用了 this.profileSnapshot = user.getProfile();,导致快照仅仅拷贝了引用。当被测方法违背契约偷偷修改了底层列表时,快照和当前状态一起被“污染”了,导致 assertEquals 永远返回 true
    经验:测试纯净性时,必须执行绝对的深拷贝(Deep Copy),切断一切引用别名。
  • 异常幂等性与状态无变化对照
    JML 中定义了大量的异常分支(如 ColdStartUserException)。良好的测试不仅要断言异常被成功抛出,更要利用连续调用测试,在异常发生后再次调用并比对快照,确保抛出异常的整个过程中系统的任何状态都未发生脏写。
  • 针对多排位阶梯验证的自动化验证
    对于类似 recommendNthUp 这种具备同分排序(分数降序,同分 ID 升序决胜)逻辑的方法,测试用例必须能够自动化生成同分偶对,去强行冲撞决胜逻辑,否则 Off-by-one (差一错误) 极难被暴露。

三、架构迭代与性能瓶颈

1. 发现方法/容器的迭代变化

随着三次作业的推进,系统的核心由简单的增删查改逐渐向复杂的图算法和排行推荐演进。应对变化最有效的方法是对 JML 进行 Diff 比对。例如在新增视频点赞或投币逻辑时,仔细比对规格会发现,它不仅影响了视频本身的热度,还隐式地要求同步更新发布者的全局影响力(Influence)。这种级联更新促使我将原本独立的散列容器封装成具有联动更新能力的管理器。

2. 追踪性能瓶颈

JML 给出的是逻辑层面的定义,如果照搬 JML 将量词翻译为 for 循环,极易写出 O(N^2) 甚至 O(N^3) 的代码(典型的 TLE 场景)。

  • 图算法的优化:在实现 queryShortestPath 时,最初简单的 BFS 如果不配合优秀的去重和双向搜索,在稠密图中会极易超时。
  • 缓存与动态维护:对于 queryGlobalBestContributor 或获取最高影响力 UP 主这类高频查询,决不能在每次查询时全量排序。必须在 coinVideo 等修改状态的方法中,引入动态维护机制(使用 TreeSetPriorityQueue 等数据结构缓存 Top N)。

四、典型 Bug 分析与溯源

1. computeUpScore 隐式调用导致的推荐算分错误

  • 现象:在测试 recommend_Nth_up 时,原本应当推荐得分更高的 UP20,系统却输出了得分为 0 的 UP10。
  • 原因:这是由于对 JML 翻译时的识别失误导致的严重逻辑错误。JML 中明确写明了核心算式为 getInterest(...) * up.getInfluence(...)。然而在代码实现时,我将后半段错写成了 long influence = getInfluence(type);。由于漏掉了 up. 前缀,导致程序默认调用了 this.getInfluence(即当前请求推荐的观众自身的影响力)。因为观众的影响力为 0,所有候选人的得分都变成了 0,最后触发了同分按 ID 升序的决胜规则,导致输出了小 ID。
  • 教训:在规格驱动开发中,方法调用的主体对象(Prefix)极其关键。绝不能凭直觉写代码,必须逐字比对 JML 里的对象引用。

2. JUnit 快照测试失败:原生数组的 .equals() 陷阱与状态漏判

  • 现象:在验证纯净性方法(@pure)时,自己编写的深拷贝测试用例 strictEquals 总是无故返回 false,导致 Case 无法通过。
  • 原因:
    a. 在比较两个 User 对象的 typeCounts(一个 int[] 原生数组)时,错误地使用了 this.typeCounts.equals(user.typeCounts)。在 Java 中,原生数组并没有重写 Objectequals 方法,这导致它实际上比较的是堆内存地址(==),对于深拷贝出来的对象必然返回 false
    b. strictEquals 漏写了对内部关键状态数组 influences 的一致性比对。
  • 教训:比对原生数组必须使用 Arrays.equals();对于对象的严格相等比较,必须对着类属性声明列表,挨个字段进行不遗漏的校验。

3. uploadVideo 导致的状态机不同步(个人主页漏维护)

  • 现象:上传视频后,全局能够查询到该视频,但在推荐算法计算或查询该 UP 主的个人数据时,系统抛出冷启动异常或权重为 0。
  • 原因:在实现 NetworkuploadVideo 时,只记得把新建的视频放入了全局的 videos HashMap 中,却忘了调用发布者(Uploader)自身的 addUploadedVideo 方法。全局网络和 UP 主个人对象的视频列表出现了状态割裂。
  • 教训:JML 规格中如果有关于 \old 容器大小加一的描述,通常意味着需要同步维护多个数据结构。局部状态与全局状态的一致性是 OO 系统稳定性的基础。

五、大模型与 Code Agent 使用经验

  • 大模型的优势:在解读复杂的 JML 嵌套量词(如多重 \exists\forall 组合)时,大模型能非常迅速地将其翻译为人类可读的业务逻辑约束。同时,在搭建 JUnit 基础脚手架、生成样板数据时效率极高。
  • 大模型的短板——忽视效率与容器特质:大模型在根据 JML 编写代码时,是不带有任何架构师思维的。例如,它会字面意义上地把 JML 里的 \sum 翻译成嵌套循环,完全无视这会引发超时。
  • 辅助单元测试:最有效的用法是用大模型去穷举边界。你可以把 JML 发给它,命令它:“列出这段规格中所有可能抛出异常的前提组合,并给出极端的 off-by-one 差一测试数据”。这在进行排位截断和同分决胜测试时有很大帮助。

六、 JML“击鼓传花”游戏的感悟与团队协作思考

  • 边界的悄然偏移:在传递过程中,我深刻体会到了信息是如何失真的。由于不同人对自然语言的理解存在微小差异,传递两轮之后,原本严格的 < 变成了 <=,原本应该维持不变的 \old 状态被直接遗忘,甚至异常的抛出优先级也发生了颠倒。

团队协作的统一方案

  • 确立单一事实来源 (SSOT):在组队编程时,绝不能靠口头或者聊天记录同步需求。必须在动手写一行逻辑代码之前,全员对齐 JML 或 API 接口文档,一切以文档契约为最终要求。
  • 交叉 Code Review 与测试前置:采用 TDD(测试驱动开发)的思想,负责编写规格的人应当同步给出该规格的自动化测试套件。如果接口实现者无法通过测试,说明要么理解有偏差,要么规格有漏洞。
  • 降维沟通机制:统一团队内部的变量命名规范和领域词汇表。消除“你说的这个 ID 是 Video ID 还是 User ID”这种低级信息差,是保证敏捷迭代不出错的基础。
...全文
31 回复 打赏 收藏 转发到动态 举报
写回复
用AI写文章
回复
切换为时间正序
请发表友善的回复…
发表回复

309

社区成员

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

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