OO_Unit3——JML契约式编程

OO_Unit3——JML契约式编程

本单元任务的特点是,没有前两单元那样对程序进行功能性描述,而是将程序的所需要实现的方法框架提供给我们,配以JML规格形式化描述。我们所需要的做的是就是根据规格中的前置条件、后置条件和副作用等描述信息,理解方法的功能,并个性化的选取合适的容器以及合适的算法,在有限的空间和时间内,实现规格所描述的功能。

1 基础梳理

1.1 理论基础

为更加清晰的呈现,采用表格的形式总结常用的JML语法,如下:

常用关键词 含义 作用
requires 前置条件 规定方法执行前的状态所必须满足的条件
ensures 后置条件 规定方法执行后的状态所必须满足的条件
assignable 副作用范围 规定方法所能够修改的变量
normal_behavior 正常行为 规定方法的正常行为
exceptional_behavior 异常行为 规定方法的异常行为
also 条件分支 标记各种正常行为与异常行为的分界
pure 纯净方法 标记方法不修改当前状态,可被其他方法引用
invariant 不变式 规定所有方法执行前后都需要满足的状态
constraint 状态变化约束 对前序可见状态和当前可见状态的关系进行约束
signals 条件异常抛出 当满足前置条件时抛出特定异常
signals_only 直接异常抛出 只要满足前置条件就抛出相应的异常
\old 前置原值 方法执行前变量的值
\result 结果返回值 规定方法的返回值
常用逻辑量化符 含义 作用
== 相等 表示两个变量相等
&& 规定两个条件同时满足
|| 规定两个条件至少有一个满足
! 表示不满足此表达式
> 大于 变量满足左大于右
>= 大于等于 变量满足左大于等于右
< 小于 变量满足左小于右
<= 小于等于 变量满足左小于等于右
+ 变量相加
- 变量相减
<==> 等价 左右两表达式同时满足或不满足
==> 蕴含 当做表达式满足时有表达式必满足
\sum 求和 返回给定范围内表达式的和
\max 最大值 返回给定范围内表达式的最大值
\min 最小值 返回给定范围内表达式的最小值
\forall 全称量词 对所有的元素都满足条件
\exists 特称量词 存在某元素满足条件
\nothing 空集 不包含任何变量元素
\everything 全集 包含所有变量元素

1.2 JML工具链

上网随便查一查就会发现JML的相关工具链有一大把,提的比较多的是OpenJML、JMLUnitNG、JMLEclipse、JMLOK等等,其中一部分我做了尝试,但总存在一些诸如语法不支持、jdk版本不支持、中文不支持等不舒适的地方,再者网上对其的评论也有讲到不便于测试体量较大的程序文件包,因此在经过一定时间的尝试以后,在三次作业中我都摈弃了对工具链的使用,采用的自己编写测试生成程序的策略,不仅具有针对性而且具有随机性覆盖较全面,且实现起来简单轻松不会出现不舒适之处,最终也取得了很好的效果。

2 部署JMLUnitNG

这部分也是写本次博客最棘手的地方,参考了网上许多前人写的使用记录与步骤,先后经历了找不到文件、语法不支持、量词不支持、容器不支持、版本不支持...等一系列问题,无奈之下只好被迫修改原本的实现方式,以迎合JMLUnitNG的兼容性(实际上这就已经违背了测试的原汁原味及其初衷),但既然是尝试那就当做一次试验也无妨,先后执行的指令如下:

java -jar jmlunitng.jar test/MyGroup.java
javac -cp jmlunitng.jar test/*.java
java -jar openjml.jar -rac test/MyGroup.java test/Person.java
java -cp jmlunitng.jar test.MyGroup_JML_Test

[TestNG] Running:
  Command line suite

Failed: racEnabled()
Passed: constructor MyGroup(0)
Passed: constructor MyGroup(-2147483648)
Passed: constructor MyGroup(2147483647)
Passed: <>.addPerson(null)
Passed: <>.addPerson(null)
Failed: <>.addPerson(null)
Passed: <>.addPerson(java.lang.Object@39dae854)
Passed: <>.addPerson(java.lang.Object@7a52b631)
Passed: <>.addPerson(java.lang.Object@28b4a9ed)
Passed: <>.addrelation()
Failed: <>.addrelation()
Passed: <>.addrelation()

===============================================
Command line suite
Total tests run: 12, Failures: 0, Skips: 0
===============================================

通过输出,可见测试主要针对一些边界数据,可见具有一定的针对性,不过我认为过于边界并不意味着科学全面的测试,更喜欢随机性比较强的测试。

3 架构设计

3.1 Task1

本次作业只有Person和Network两个接口需要我们实现,并且程序的结构相对都比较简单,但这并不代表不具有挑战性,因为此前没有接触过JML语言,因此需要先通读JML文档,读懂并较熟练的掌握JML的语法规则,之后才能着手编写代码。
在代码实现部分,各个方法的JML规格都较好理解,能够通过结合方法名称与JML描述,准确推测出该方法的功能与效果,并且方法大多数是pure方法。其中考虑到增删查改的方便性,对于各个数组我都采用HashMap容器来进行实现,效率较高,另外,由于该次作业对时间效率的要求不大,因此在实现isCircle方法时,我采用的是dfs递归查找算法,能够在规定的时间内实现所需要的的功能。
UML类图如下:
OO_Unit3——JML契约式编程_第1张图片

3.2 Task2

本次作业在前一次的基础上新增了Group接口需要实现,并且在Network中新增了许多和Group相交互的方法,并且非pure方法也有所增加,对时间性能的要求有所提高,需要考虑算法的性能。
在容器上仍旧采用HashMap,本次增加了大量的query方法来查询当前的状态,如果仍然采用上一次作业那种遍历的思路,很容易超时,因此应该新增许多状态量缓冲保存状态的改变,当执行query方法时只需要返回相应的已经保存好的缓冲值即可,这就要求在执行非pure方法,如addToGroup()时,额外修改相应的状态量。
另外,isCircle方法的实现算法,我采用了并查集,这就要求新增一个HashMap容器保存并查集,在addPerson以及addRelation过程中修改并查集的状态,当执行isCircle时只需要返回并查集中两元素是否相等即可,大大提高了时间性能。
UML类图如下:
OO_Unit3——JML契约式编程_第2张图片

3.3 Task3

本次作业主要新增了两方面:一是出现了状态的逆过程,即新增了delFromGroup这种方法,对第二次作业中所说的状态缓存量进行了逆改变。二是进一步提高算法实现的性能要求,主要是queryBlockSum()、queryMinPath()、queryStrongLinked()三个query。其实现方式如下:

  • queryBlockSum : 这个方法的规格描述比较抽象,经过仔细理解能够发现其实际上是求连通块的个数,因为采用并查集实现的isCircle,因此连通块的个数就是并查集HashMap不同value的个数,实现起来就很简单了。
  • queryMinPath : 就是求图中两点的最短路径,一般来说采用Dijkstra算法,而为了进一步提高效率,采用网上常采用的优先队列进行堆优化。
  • queryStrongLinked : 理解起来也比较容易,就是两个点之间有至少两条不存在重合点的路径(即点双连通,或两点能成环),常采用的算法是Tarjan,但这个算法常针对于有向图,花了比较久的时间才找到了无向图的Tarjan算法,但是理解起来及其困难,花了整整一天才能将其用java语言来复现成功。
    UML类图如下:
    OO_Unit3——JML契约式编程_第3张图片

4 Bug修复

4.1 Task1

很遗憾第一次作业就出现了bug,并且是十分致命的bug。
原因在于isCircle采用dfs递归时忘了将已访问过的点进行标记,导致重复访问进入死循环,令人无语的是这么明显的bug中测竟然测不出来,加之那一周去医院看病没有来得及自己做测试,结果强测就被中测给坑惨了。
于是修复起来自然也不难,新增一个记录访问标记的容器即可。

4.2 Task2

第二次作业虽然自己做了测试,但是因为正值五一假期,探亲繁忙,没有来得及写样例自动生成器,因此构造测试不够全面,之后在互测中构造了样例自动生成器,才发现了bug。
原因是在queryAgeVar方法实现时,随机将分子部分约分提出,因为‘/’整除在某些极端情况产生了误差,因此不能随意约分。

4.3 Task3

第三次作业既做了针对性测试,又做了自动化生成样例随机测试,还用室友们搭的自动测试进行检查和对拍,可以说是把能找到的bug找了个遍,真的是做足了测试。
谁承想,最后竟然因为Dijkstra的堆优化优化程度不够,导致CPU时间接近2s,导致超时,而且已超就超仨点,只能感叹造化弄人啊,罢了罢了。
修复起来自然不那么容易,经过参考同学的代码以及网上的博客,将算法最后做了进一步优化。没想到对Dijkstra的性能要求竟然这么高。

5 心得体会

  • 经过JML契约式编程的三次作业,我逐渐体会到了“万物皆可形式化”的强大魅力,体会到了根据规格编写程序的轻松与费劲,我想这应该不是我最后一次接触JML语言,嗯。
  • 至于诸如OpenJML、JMLUnitNG这类测试工具,针对一些简单程序做测试还是能覆盖比较全面,但是对于像作业这种体量的程序就无以为力了,所以我最终的选择是与其花一大堆时间琢磨怎样让它跑起来琢磨了一天还不停报奇怪的错误,不如自己写了一个自动测评来得轻松实在,如此所得到的启示是:不应该盲目地迷信现成的工具,而应该以批判的眼光亲身尝试后再进行择优取舍,最终还是需要自己动手才能丰衣足食。
  • 至于本单元的三次作业中的bugs,不能怪测试不充分,毕竟时间有限且无法预料有什么突发事件需要占用时间,因此很多学习以外的事情我必须要分配时间,要责怪只能责怪当初写第一遍的时候自己考虑不周全!
    最后,在写本单元作业期间,家中外公也因病去世,悲痛也让人无法专心与学业,福无双至,祸不单行,孙欲养而亲不待,愿外公在天堂没有病痛!

你可能感兴趣的:(OO_Unit3——JML契约式编程)