307
社区成员
发帖
与我相关
我的任务
分享JML是一种形式化语言,用于规范类及其属性、方法。对于属性而言,存在invariant(不变式)和constraint(状态变化约束)。不变式即属性构造后直到销毁前都要满足的条件,状态变化约束即属性发生变化时旧值和新值需要满足的关系。方法规格则分为正常行为规格和异常行为规格,由前置条件(requires)、后置条件(ensures)、副作用范围限定(assignable、modifiable、pure以及课程组扩展的safe)组成。
JML好处多多。首先,JML很好的避免了用自然语言描述接口所带来的问题:二义性。使用一阶谓词能很好避免一些能够引起歧义的描述,保证了所有人看到的规格都是一致的。其次,JML仅约束行为,对具体实现不做约束,有足够的自由度。最后,JML也方便进行自动化测试,仅需对照requires、ensures以及副作用限定逐条检查即可。
当然JML也存在一些问题。第一,一阶谓词逻辑尽管不存在歧义,但难以读懂,一些自然语言比较容易描述的(尤其是涉及到最值的)行为,使用谓词逻辑十分冗长,不过这一点可以通过辅以自然语言描述缓解。第二,规格编写本身也是十分困难的,要将一个自然语言的需求精准的用规格描述,不漏掉隐含条件,不过度约束行为,难度甚至高于编写代码。而且JML规格也很难被验证是否正确,没有方法能有效约束JML不出错(从课程组经常修复官方包bug可见一斑)。
抛开编写规格不谈,规格驱动开发可以说是极大的提升了开发效率。开发过程中不再需要揣测用自然语言描述时会出现的模糊的需求,不再需要考虑有哪些异常。测试的编写也十分便捷,仅需检查是否满足规格即可,样例需要满足的条件、期望输出等等规格均完成了规定。这一部分完全可以大胆交给LLM完成,在规格清晰并且算法难度不高的情况下,大模型生成的实现代码可靠性非常好(尽管性能可能有些问题),大部分时候都能满足规格要求;测试虽然经常不够全面,但经过多轮提问也能够基本覆盖。因此,AI时代下,我认为规格驱动开发会是未来,程序员应该会的转向学习如何编写规格、如何实现高效的算法、如何构造细粒度的测试,这种简单的实现以及简单的自动化测试的编写可以全权交给AI。
有了规格之后,对于正确性的测试完全可以交给AI,根据规格AI会生成常规测试以及边界情况测试,考虑的会比拍脑袋写的测试更全面。当然更加合理且全面的方式是根据规格划分等价类,测试每一个等价类以及对应的边界情况,同时测试每一个异常行为是否正确实现。
测试的一大难点是对副作用的测试,包括不变量的测试以及状态转移约束的测试(尽管后者本单元未涉及)。这方面大模型的表现比较糟糕,可能会以错误的方式完成快照和比较,例如比较引用类型时仅仅比较引用是否相同,并不比较引用的对象的状态是否不变。自己编写的时候需要仔细检查每一个属性是否发生了变化,以及每个成员变量的属性是否变化(通过公开的接口)。
| 迭代 | 新增功能 | 涉及接口 |
|---|---|---|
| hw9 | 用户、视频、关注与取关、观看,以及对用户数据的基础统计功能 | UserInterface VideoInterface NetworkInterface |
| hw10 | 硬币、点赞、转发、收藏、评论、热度、勋章、贡献者 | UserInterface VideoInterface NetworkInterface |
| hw11 | 智能推荐、分区排行 | UserInterface NetworkInterface |
方法迭代时,通过diff能很好的看到新增、修改的规格,对照即可发现变化;容器在迭代中的变化则是要在实现时注意,为了更好的性能需要选择合适的容器。
有两种方法可以发现性能瓶颈。
一个是静态代码审查。重点查看有循环、递归的地方,看看能不能减少循环嵌套层数、递归深度,替换为更高效的算法,或者通过预先维护一些字段避免实时计算。
另一个是通过大量随机测试查看热点图,处于热点的方法需要重点优化。不过可能会不够全面,一些方法在随机测试当中可能时间可以接受,但在边界情况下进行压力测试会出现明显性能问题。
在hw9当中,我的代码有性能上的问题。在查询互相关注时,我使用了一个$O(n^2)$的算法查询互相关注的人数。出现的原因是我按照JML规格的思路进行了代码编写,参照规格的两个\forall写出了二重循环。但规格仅仅限定行为,不限制实现,更好的做法是在每次关注、取关的时候都维护互相关注字段,这样只要$O(1)$时间就能完成查询。
本单元的代码手写部分仅1行,其余全部由Agent生成。大模型阅读规格比人类更快,能在短时间内生成大量满足规格的代码。不过写出来的代码通常只是满足规格,性能不一定更好。需要继续提示在满足JML的前提下,进行一些优化,并且需要提示一些优化的方向:预维护字段、更高效算法、更好的容器、建立缓存。之后Code Agent生成的代码性能也就能谋杀要求了。编写测试时候通常需要多轮引导,尤其是副作用测试,需要反复强调要测试什么副作用,强调要用快照而不是浅拷贝。
整个游戏出错最多的地方就是自然语言翻译到JML。诚如上文所言,容易漏隐含条件,一些涉及到一定数据结构的难以使用JML描述。边界也有可能发生变化,涉及到0相关的内容容易出错,究竟是大于还是大于等于在翻译成规格的时候会有问题。需求也有概率发生变化,一些要求返回某一对象的值,可能变成返回这一对象本身。
统一实现方法是没有必要的,保证正确的前提下如何实现重要性不高,能保证代码可读性即可。但统一对需求的理解十分必要,应当在开发前统一讨论需求,并且完成一份可靠的规格说明文档,最好使用形式化语言严谨规定。