面向对象课程第三单元作业总结

面向对象课程第三单元作业总结

一、JML语言

理论基础

JML 预言是一种能够用于约束 Java 模块行为的行为接口规范语言。它结合了契约式设计、基于模型的描述方法和精化验算的一些元素。

契约式设计(Design by Contract)要求软件设计者为软件组件定义正式的,精确的并且可验证的接口。为传统的抽象数据类型又增加了先验条件、后验条件和不变式。Eiffel语言首先提供了按契约设计的概念,它关注的是用程序规定软件模块的权利和责任,以确保程序正确性。

应用工具链

关于JML的应用工具链在其下载页均有描述,下面罗列几个重要工具:

  • OpenJML:集合 JML 规格语法检查、一致性检查等功能的工具。
  • JML Editing:JML 的 Eclipse 扩展,能够与其它 JML 工具一起使用。
  • jml4c:基于 Eclipse Java编译器的 JML 编译器。
  • JMLUnitNG:为使用了 JML 注解的 Java 代码生成自动化测试文件的工具。
  • JMLOK:使用随机测试检查代码规格一致性并指出出可能的错误原因。

二、SMT Solver

1. 环境配置

OS: Ubuntu 18.04.1
openjdk version: "1.8.0_212"

2. 基本用法

编写 Shell 脚本 openjml.sh 并使用命令 chmod +x openjml.sh 赋予执行权限,方便启动:

#!/bin/bash
path/to/java -jar path/to/openjml.jar "$@"

启动方法:

./openjml.sh <options> <source files>

3. 常用选项

-check(-command=check): 语法检查
./openjml.sh -check <other options> <source files>
-esc(-command=esc): 静态检查
./openjml.sh -esc <other options> <source files>
-trace: 静态检查验证失败时输出详细信息
./openjml.sh -esc -trace <other options> <source files>
-sourcepath: 依赖模块路径

比如本次作业中 JML 位于课程组提供的源码中,对 MyPath.java 进行 JML 相关的检查时需要通知设置其依赖的路径,否则 openjml 工具将无法找到其引用的 Path.java 的位置,于是报错。

./openjml.sh -sourcepath path/to/module <other options> <source files>

注意,这里的path是指工程源码目录,也即源码最顶层的包所在的目录。多个 path 之间使用冒号分隔。

-verboseness: 输出运行时的信息

分为 0 ~ 4 五个等级,经过实验,设置 verboseness=2 可获得最好的视觉效果。

-exec: 指定使用的 prover
./openjml.sh -exec path/to/prover <other options> <source files>

4. 实际运行

笔者使用 openjml 工具对 MyPath.java 文件进行了静态检查,使用命令如下:

./openjml -esc -trace -verboseness=2 
		  -sourcepath {$specs-homework-opensource}/src/main/java 
		  {$DirOfMyPath}/MyPath.java

输出结果:

... (此处省略一万条)
Note: Summary:
    Valid:        2
    Invalid:      7
    Infeasible:   0
    Timeout:      0
    Error:        0
    Skipped:      0
   TOTAL METHODS: 9
   Classes:       0 proved of 1
   Model Classes: 0
   Model methods: 0 proved of 0
   DURATION:          4.5 secs
21 warnings

最后发现只有构造方法和迭代器方法符合规格(内心崩溃)。尝试查看trace分析报错原因,然而只能看懂 MyPath.compareTo 方法的“错误”原因。

在此方法中存在一个循环用于依次比较两个Path中每个node的大小。

...
for (i = 0; i < thisLength && i < otherLength; i++) {
    if (this.getNode(i) != (o.getNode(i))) {
        return Integer.compare(this.getNode(i), o.getNode(i));
    }
}
...

根据trace中的提示信息:

src/homework/MyPath.java:65: warning: The prover cannot establish an assertion (Precondition: src/homework/MyPath.java:26: ) in method compareTo
            if (this.getNode(i) != (o.getNode(i))) {
                            ^
src/homework/MyPath.java:26: warning: Associated declaration: src/homework/MyPath.java:65: 
    public int getNode(int index) {
               ^
src/com/oocourse/specs1/models/Path.java:10: warning: Precondition conjunct is false: index >= 0
    /*@ requires index >= 0 && index < size();

检测出调用 MyPath.getNode 方法时没有严格遵循其 preCondition,认为有可能传入非法参数(实际上没这种可能),所以报错。

修改代码为:

...
for (i = 0; i >= 0 && i < thisLength && i < otherLength; i++) {
    if (this.getNode(i) != (o.getNode(i))) {
        return Integer.compare(this.getNode(i), o.getNode(i));
    }
}
...

修改之后重新进行检查,此时 MyPath.compareTo 通过了检查,虽然在我看来这中修改完全没有必要。

鄙人愚钝,其他方法的报错实在看不出什么门道,猜也没猜对。

三、JMLUnitNG使用

JMLUnitNG是一个为使用了 JML 注解的 Java 代码生成自动化测试文件的工具。

1. 环境配置

OS: Ubuntu 18.04.1
openjdk version: "1.8.0_212"

2. 基本用法

  • JMLUnitNG 工具生成相应文件的测试类,参考代码如下:

    java -jar jmlunitg.jar -d $outDir -cp $dependency <source files>
    
  • openjml 工具编译需要进行测试的文件。

    ./openjml -rac <options> <soirce files>
    
  • javac 编译第一步生成的文件。

  • 运行 test 类。

3. 实际运行测试

由于在测试作业文件时出现了各种各样的问题,所以退而求其次使用一个小Demo来体验 JMLUnitNG 的使用。

选取的代码是 《JML Level0 手册》最后一章附的代码。

测试运行结果如下:

[TestNG] Running:
  Command line suite

Passed: racEnabled()
Failed: constructor Student(null)
Passed: constructor Student()
Skipped: <<Student@77be656f>>.addCredits(-2147483648)
Passed: <<Student@69b794e2>>.addCredits(0)
Passed: <<Student@3f200884>>.addCredits(2147483647)
Passed: <<Student@4d339552>>.getName()
Failed: <<Student@5a4aa2f2>>.setName(null)
Passed: <<Student@429bd883>>.setName()

===============================================
Command line suite
Total tests run: 9, Failures: 2, Skips: 1
===============================================

可以看出来 JMLUnitNG 生成了一些边缘数据用于测试,比如整数的上下限,0,null等。

四、架构设计

第一次作业

类图:

hw9_diagram

需求相对简单,MyPathMyPathContainer类直接依赖PathPathContainer接口,实现两个类的完全解耦。

第二次作业

类图:

hw10_diagram

本次作业扩展最大的部分是添加了寻路算法,所以封装了一个图算法的类 AlgorithmGraph 将图算法与其他逻辑隔离。

第三次作业

类图:

hw11_diagram

心路历程

在确定架构前,首先从需求分析出核心问题,然后从核心问题的解决方案出发确定大体架构,最后优化细节。

本次作业的核心问题在于寻路算法以及中间结果的复用。虽然有些问题的表述并不是寻路,但是经过对“换乘”的抽象(本课程的本意不在于算法设计,此处不再详细解释算法抽象过程)之后,都能够将之转化为图间两点最短路径问题,仅仅在具体实现上存在细微的差别。另外需要注意的是,不同的问题虽然存在不同的结果,但是两个问题的输入可能是相同的。

具体架构

使用 GraphInfo 类将 MyRailwaySystem 与具体实现逻辑完全分离,MyRailwaySystem 所有对图的增删查改都通过 GraphInfo 提供的接口实现。增加扩展性。

使用工厂方法针对不同问题创建不同的类。提升代码复用率。

将寻路算法(Floyd_Warshall)单独封装。提升代码复用率。

GraphInfo 类和具体算法类之间共享变量。防止由于不同算法要求同样的输入时重复存储,造成空间和时间上的浪费。

实现细节

为了减少时间复杂度需要将中间结果存储下来,图连通结构或边权值发生变化时只需要对特定的结果进行更新即可。在这里笔者耍了个小聪明,即图连通结构或边权值发生变化时不立刻更新结果,而是在查询之前检查一下 dirty 位,如果 dirty 为 true 则更新结果。这样的好处在于对于先确立图结构(之后不发生变化)然后查询的测试数据,只需要一次更新即可。

结果实际上并没有这样的测试数据,都是modify和query交替的测试样例。更悲催的是在强测的时候还在这个地方栽了两次。

五、BUG修复

自测不努力,强测两行泪!!!

第一次作业使用 Junit 做白盒测试,没有出现bug。

第二次作业使用 Python 脚本生成数据,进行黑盒测试。但是在强测阶段出现了 CPU_TIME_LIMIT_EXCEED 。检查之后发现在更新结果数据之后没有将 dirty 置为 false,以至于每次查询都会更新一遍结果矩阵,造成超时。修改 checkUpdate() 一行代码即可:

public void checkUpdate() {
    if (dirty) update();
}

修改为:

public void checkUpdate() {
    if (dirty) update();
    dirty = false;
}

想来生成的数据量大一点,完全能够发现这个 bug。

第三次作业仍然使用 Python 脚本生成数据,进行黑盒测试。但是强测阶段出现了一个点 error。

原因在于,修改图并对结果矩阵设置脏位时发生错误。在图连通结构发生变化时需要更新所有缓存结果,边权值改变的时候只需要更新部分缓存结果。在写代码时,将此关系弄反了,所以只要出现添加或删除一条 Path 时,仅仅修改了权值,程序便会出现错误。由于测试数据不完备,所以并没有测出这个 bug 。

六、心得体会

规格撰写

经过 openjml 和 JMLUnitNG 的配置和使用,笔者体会到 JML 对于程序设计正确性的巨大帮助,但是对 JML 的实用价值产生了怀疑。

规格实际上是使用数学语言对方法的前置和后置条件进行约束,而数学语言的严谨性又使之区别于自然语言,所以对于设计者而言撰写规格实际上是存在门槛的;即使设计者设计出 JML 书写的规格,代码实现者在解读时也会产生不小的麻烦;经过千辛万苦,写规格,读规格,代码实现之后,检查代码和规格一致性的软件(比如 openjml 和 JMLUnitNG)又如此不友好,消磨的不只是人的心力,更重要的是浪费了大量的效率。

当然在 JML 规格这一章,还是有些积极的思考。JML 实际上是一个设计的过程,做好了设计再进行实现,可以在实现的过程中有很清晰的层次感。而且在详细地规定了各个接口的输入输出之后,也能够增强对于一些边界条件的把控,提升程序的质量。

测试

测试是一门学问。代码写完了实际上仅仅完成了任务的一半,接下来要做充分的测试才能够保证程序的正确性。可以使用 JUnit 一边写代码,一边测试;之后生成大量的尽可能全面的测试数据对代码进行覆盖测试。

posted @ 2019-05-22 19:59  我的名字竟然又被用了  阅读(167)  评论(0)    收藏  举报