307
社区成员
发帖
与我相关
我的任务
分享核心认识
我对JML的理解是,它本质上是把程序的行为契约形式化。一个方法对外暴露的不再是签名和自然语言注释,而是一套数学化的承诺——你满足这些前提,我就保证达到这个结果。这种契约思维的价值在于,调用方和实现方各司其职,责任边界清晰。我作为实现者不需要猜测调用者会传什么数据进来,规格已经用requires写明了哪些情况归我管、哪些情况直接抛异常。这种明确性在多人协作和代码迭代中尤为重要,因为后来者不需要通过读源码来推断方法的行为,直接看规格就够了。
JML让我认识到,程序正确性不能靠感觉判断,必须落实到数学表达式层面。传统开发中,需求文档的一句话可以被理解出好几种实现方式,测试也只能覆盖有限的几个场景。但JML用requires和ensures把所有状态变化都写成逻辑公式,通过与否是确定的,不存在大概差不多的情况。这带来一个根本性的改变:正确性不再是跑通几个测试用例后的主观安慰,而是一个可以被逐条验证的客观结论。每一个前置条件是否满足、每一个后置条件是否达成、每一个异常分支是否正确触发,都有明确的判定标准。这种从大概对到严格对的转变,让我重新理解了什么叫程序可靠。
副作用控制
assignable这个机制尤其改变了我的思维方式。它让我明白一个方法的质量不仅取决于它返回了什么,还取决于它没有碰什么。以前我写方法从来不会刻意去检查是不是意外修改了无关数据,但现在我会把副作用控制当作和功能实现同等重要的事。这种对状态的精确管理,是我在规格驱动开发中学到的最核心的东西。
语法体系的意义
JML的语法设计围绕着一个核心目标:用数学语言精确约束程序行为,让正确性可以被严格验证。
| 语法 | 意义 |
|---|---|
requires / ensures | 明确前置责任与后置承诺,将输入输出关系从模糊描述变成可验证的逻辑命题 |
assignable / pure | 显式约束副作用范围,把不改错不该动的东西提升到和改对该改的东西同等重要的位置 |
\forall / \exists | 用全称量词和存在量词一句话约束整个集合,表达能力超越任何抽样测试 |
\old() | 指代方法执行前的变量值,让规格描述相对变化而非绝对数值,适应性更强 |
signals | 将异常行为纳入规格,让异常处理不再是遗忘的角落 |
invariant | 划定对象合法状态空间,无论多少次方法调用都不会被突破的底线 |
这些语法组合在一起,使程序成为一个所有状态变化都有数学公式严格约束的系统。这种保障程度是自然语言需求文档和传统单元测试都无法达到的。
本单元三次作业分别要求为queryMutualFollowingSum、clean_spam_comments和recommend_Nth_up编写JUnit测试,我形成了以下测试策略:
首先是对pure方法的验证。一开始我直接用getUsers获取调用前后的用户数组做对比,但很快发现这样根本检测不出用户对象内部的属性变化,因为数组虽然是拷贝的,里面的引用还是指向同一个对象。后来我改用strictEquals逐一比对每个用户对象,这个方法会递归比较所有基本类型属性和对象类型属性,才能真正验证方法没有产生任何副作用。这让我意识到测试的状态快照必须足够深,浅拷贝做不了无副作用的验证。
异常测试的关键在于覆盖所有分支。对照JML中的exceptional_behavior,我把每个signals条件都单独构造一个测试用例。第三次作业的recommend_Nth_up涉及五种异常情况,我逐个搭建了用户不存在、系统无用户、无观看记录、关注人数不足、rank为负数的场景,确保每个异常分支都被独立触发并返回了正确类型的异常。
后置条件的断言验证是整个测试的核心部分。以clean_spam_comments为例,JML要求同时验证删除数量、关键词最大出现次数、剩余评论内容是否正确、无关属性是否保持不变。我把ensures里的每一条逻辑都翻译成了对应的assert语句,用getCommentIds和getCommentContents获取删除后的数组状态逐一比对。
整个测试做下来最大的体会是,对着JML写测试用例和对着需求文档写测试用例完全不同。JML规格本身就是一份精确的测试清单,requires告诉你需要测试哪些输入条件,ensures告诉你断言应该怎么写,signals告诉你异常用例怎么构造。只要把规格逐条翻译成测试代码,覆盖率自然就上去了。
第一次作业:基础图结构
核心数据结构是用户关注关系的有向图。方法query_shortest_path需要用BFS计算两点最短路径,query_mutual_following_sum需要统计互相关注对数。
性能改进:query_mutual_following_sum如果每次调用都双重循环遍历所有用户,时间复杂度为O(n²)。我在follow_user和unfollow_user方法中维护一个整型计数器。当用户A关注用户B时,如果B也已经关注了A,计数器加1;取消关注时同理。这样查询操作降为O(1)。
第二次作业:经济系统与互动
新增了视频分区、硬币体系、点赞投币转发评论功能。query_most_popular_video需要返回指定分区热度最高的视频,query_best_contributor需要返回对某up主贡献值最大的用户。
性能改进:视频热度值涉及播放量、点赞数、投币数等多个维度的加权计算。我没有在每次查询时临时计算,而是在每次watch_video、like_video、coin_video操作时直接更新视频对象中缓存的热度值。同样,up主的贡献者排行榜也在每次投币操作时维护一个有序结构,避免查询时全量排序。
第三次作业:推荐系统
新增了用户画像和智能推荐。recommend_video需要基于用户在各分区的观看记录计算兴趣度并推荐视频,recommend_Nth_up需要综合共同关注、同分区视频等维度计算up主推荐分数。
性能改进:recommend_Nth_up需要先对所有up主计算分数再排序。computeUpScore方法涉及遍历关注列表和视频列表。我预先为每个用户维护了一个分区观看计数的哈希表,使得兴趣度计算从O(n×m)降为O(1)。在推荐时,过滤掉用户本人和无视频上传记录的up主,再对剩余对象计算分数并排序,减少无效计算。
第二次作业clean_spam_comments清理不彻底
强测中,当一条视频存在多条包含同一keyword的评论时,我的方法只删除了其中一部分。原因是使用了Java的ArrayList.remove(Object)方法,该方法只删除第一次出现的匹配元素,不会删除所有匹配项。
修复方法:改用removeif方法
优势:形式化规格的精准翻译
大模型在规格驱动开发中最大的优势是能直接读懂JML并精准生成代码。把接口中的requires、ensures和signals输入进去,它能准确识别正常路径与异常分支,自动生成包含所有if判断和throw语句的方法框架,翻译速度快且逻辑准确。在效率方面,大模型并不会一味照搬规格中的遍历逻辑,当我明确指出性能要求时,它能够主动引入缓存变量、哈希表等优化手段,把查询复杂度从全量计算降为增量维护。在架构层面,它也能理解跨类数据协同的必要性,给出Network层统一索引的设计方案。整体来看,大模型在代码生成阶段的表现超出了我的预期,无论是逻辑正确性还是性能意识都可圈可点。
局限:测试生成过于保守
大模型的真正短板在于单元测试的生成。它对照ensures翻译出的assert语句只能覆盖显式的后置条件,对于assignable中那些无需修改的无关属性,几乎不会主动生成验证代码。更麻烦的是,它构造测试数据时思维极其僵化,只会生成几个标准场景的微调版本,很难跳出既定模式去设计真正有挑战性的边界用例。比如让它测试recommend_Nth_up的冷启动异常,它只会机械地构造一个空观看列表的场景,不会想到用户看过视频但那些视频已被删除这种更隐蔽的情况。这种测试覆盖的局限性意味着,我可以信任大模型生成的框架代码,但不能依赖它来保证测试的完整性和深度,边界挖掘和副作用验证仍然需要我自己来设计。
总体来说,大模型在规格驱动开发中的角色更接近一个高效的翻译器和脚手架生成器。它能把规格快速变成可运行的代码和测试框架,节省大量重复劳动。但性能设计、架构选型和测试覆盖的完整性判断,这些需要全局视角和经验积累的决策,目前还是必须由开发者自己来做。
我们组传递了两道JML题目:
1.共同粉丝与关注:计算两个用户的交集粉丝列表和交集关注列表
2.分区视频计数:统计用户观看过的视频中,属于指定up主且属于指定分区的数量,要求用户和up主必须存在
整个传递流程走下来,出现了几类典型问题。最普遍的是自然语言的歧义,JML里用全称量词和存在量词写出的逻辑关系语义是确定的,但一旦被翻译成自然语言,每个人的表述方式就不一样了。同一段遍历逻辑,有人说从A出发,有人说从B出发,虽然数学结果碰巧相同,但到下一轮还原时方向可能就被颠倒。其次是边界条件和前置约束的丢失,第二道题里requires明确要求用户和up主必须存在,但在口口相传中很容易被忽略,后面的人觉得反正要遍历观看列表,用户不存在列表就是空的,加不加检查都一样,根本没意识到这在异常类型和空指针处理上行为完全不同。还有一个问题是信息在传递中被无意识地压缩,JML里用num_of加两个筛选条件的与关系来精确限定计数范围,但口述时很容易被概括成符合条件的视频,下一轮的人按自己的理解展开,与关系就变成了或关系,计数逻辑完全走样。
这些问题的根源都在于自然语言本身就不适合传递精确的逻辑约束。多人协作时如果只靠口述和文字描述来对齐需求,信息差几乎是必然存在的。要想统一所有人对任务的理解,只能把公共接口的约束写成形式化规格,让每个人看到的都是同一份不会变形的描述,并且在规格层面就明确写出哪些分支是正常逻辑、哪些分支是异常行为,不给个人理解留余地。
多人协作建议:今后组队编程时,我会坚持以下做法:
requires和ensures,确认所有人的理解一致