面向对象 Unit3 总结博客:规格驱动开发与 JML 实践

王一舟-24371515 2026-06-02 19:38:00

 

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

Unit3 的核心是围绕 JML(Java Modeling Language)规格展开程序设计、实现与测试。与前两个单元根据自然语言题面直接“放飞自我”写代码不同,本单元要求我们先读懂形式化规格,再严格根据规格约束来实现方法行为。

我对“规格驱动开发”的理解是:程序的正确性首先不依赖于底层的实现细节,而是依赖于接口所规定的契约(Contract)。

  • 契约的履行:在编写代码时,必须保证代码满足 requires(前置条件)、ensures(后置条件)、assignable(副作用范围)和 signals(异常行为)等约束。同时,在调用方法时,也必须在满足前置条件的情况下去使用。

  • 容易忽视的陷阱:其中最容易被忽视的是 assignablepure(纯方法)。很多方法表面上只是执行查询,但如果在实现中为了优化而更新了缓存、修改了集合顺序,或者清理了无效数据,就可能在不经意间违反了无副作用的限制。

  • 规格的边界:规格驱动开发的优势在于,它把模糊的自然语言需求拆解成了可以用形式化逻辑检查的条件,利用量词(\forall, \exists)、旧值(\old)、返回值(\result)等机制精确描述行为边界,减少了主观理解差异。

核心感悟:JML 规格强调的是“做什么(What)”,而不是“怎么做(How)”。例如,某些查询在 JML 中表现为多重量词的嵌套遍历,但如果我们写代码时也照搬多重循环,面对大规模数据必然会超时(CTLE)。因此,规格驱动开发解决的是正确性边界问题,而性能和架构依然需要程序员基于数据规模、调用频率去主动设计。

二、 JUnit 测试经验总结

在本次单元中,我总结出了一套行之有效的 JUnit 测试策略:

  1. 全面覆盖(正常行为与异常分支)

    对于带有 requiressignals 的方法,绝不能只测试合法输入。例如添加用户、上传视频等方法,必须刻意构造重复 ID、不存在 ID 等非法参数。如果忽略了异常分支,极易出现“异常虽然抛出了,但对象状态已经被破坏”的致命隐患。

  2. 严查副作用(assignablepure

    查询类方法通常是 pure 的。测试时,不能仅仅断言(Assert)返回值是否正确,还要在调用前后保存对象的状态快照。必须比对用户列表、关注关系、视频集合等是否在查询后发生了意外改变。

  3. 警惕“过度测试”(不过度推断)

    测试不能过强。JML 只规定了逻辑结果,往往没有规定内部容器的顺序或底层数据结构。如果测试用例强行检查数组的绝对顺序或对象的内存地址,就会把合法的实现误判为错误。JUnit 测试应该克制,只检查 JML 明确契约的内容,区分“规格保证的行为”与“我自己想象的实现”。

  4. 随机测试与构造测试相结合

    随机生成数据可以扩大测试广度,发现复杂的组合状态问题;而手工构造测试则适合精准打击边界条件(如空集合、单元素、链式关注、关键词出现 0 次等)。最高效的方法是:将 JML 拆解成一条条 ensures,然后为每一条 ensures 设计一组最小反例。

三、 三次作业的迭代过程分析

  • 第一次作业:围绕基础社交网络展开,维护用户和关注关系。数据规模较小,使用直观的容器(如 List)和遍历方式就能轻松保证正确性。

  • 第二次作业:引入视频、评论、观看等功能,数据关系网变得复杂。

  • 第三次作业:增加了智能推荐等复杂的业务场景,方法间的依赖和性能压力陡增。

迭代感悟:面向对象的迭代难点不在于“新增一个方法”,而是新需求往往会打破原有类的职责划分和复杂度假设。

随着查询种类的增多,如果每次查询都去遍历全部网络,性能就会崩塌。因此,在后续迭代中,我大量使用了 HashSetHashMap 进行高频查询优化,并为用户额外维护了关注者、贡献值等缓存映射,通过“空间换时间”的增量维护策略,避免了重复计算。

我发现定位性能瓶颈通常有两招:一是直接从 JML 推导(如果规格里有两层 \forall,直接实现就是 $O(N^2)$,必须优化);二是通过运行现象倒推(如果小数据 AC,大数据 TLE,说明基础逻辑没错,但特定查询复杂度过高)。

四、 程序中出现过的 Bug 及原因分析

回顾本单元,我遇到的问题主要集中在以下几类:

  1. 规格理解不完整:一开始只盯着 \result 返回值,忽略了查询方法中为了方便而修改的内部缓存,导致违反了 pure 限制。

  2. 状态维护不一致:随着迭代,同一个业务实体被多个容器引用。例如在添加关注时,如果只更新了 following 却忘了同步更新目标的 followers,后续涉及到粉丝相关的统计就会彻底算错。

  3. 异常捕获(try-catch)的滥用:我曾为了防止程序崩溃,在较大的代码块外层直接套用宽泛的 try-catch。这导致一些本该暴露的逻辑错误被默默吞掉,增加了 Debug 的难度。异常处理应当精准,绝不能用来掩盖逻辑缺陷。

注:由于在架构设计初期我就考虑到了性能瓶颈,尽量避免了朴素遍历的写法,因此在强测和互测中幸运地规避了大规模的性能 Bug。

五、 JML “击鼓传花”游戏的感悟

在第二次研讨课上,我们玩了 JML “击鼓传花”游戏(自然语言 NL $\rightarrow$ JML $\rightarrow$ 另一人的 NL $\rightarrow$ 另一人的 JML)。这让我对规格的准确性有了极深刻的体会:

  1. 边界条件极易丢失:最初的自然语言往往明确提到了 null、空数组、重复元素等特殊情况,但在几轮 JML 翻译后,如果某位同学只关注主干功能,边界条件就会被忽略。最终的 JML 看似完整,实际上约束已经大打折扣。

  2. 语义传递的畸变:NL 和 JML 并非简单的一一对应。一旦某处量词范围写错,后续的人就会在错误的基础上继续“脑补”解释,导致任务从“查询对象”演变成了“修改对象”。一处微小的歧义,会在团队协作中被无限放大。

对团队协作的启发

团队开发绝不能仅仅依赖口头沟通,必须形成统一的需求文档和接口规格。为了减少组内的信息差,我认为应当做到:

  • 为每个核心接口建立标准模板(包含前、后置条件、异常行为和副作用)。

  • 任何需求变更必须落实在共享文档中,禁止仅在微信群口头通知。

  • 关键方法实现前,先进行规格评审

  • 测试用例必须与规格绑定,每条关键规格至少对应一个测试场景。

  • Code Review 时不能只看“代码能不能跑通”,更要审查“代码是否严格遵守了规格”。

六、 总结

Unit3 是一次思维模式的转变。JML 提供了一种形式化的方式,使程序的行为、异常边界和副作用范围变得清晰明确;而 JUnit 则是我们验证代码是否履行了这份“契约”的最强武器。

在三次作业的迭代中,我深刻认识到:正确性和性能都不是碰运气写出来的,而是提前设计出来的。程序的正确性依赖于对 JML 每一条子句的敬畏与执行,而性能则依赖于合理的数据结构抽象与状态的巧妙维护。

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

307

社区成员

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

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