JML语言概述(Level 0)
概念定义
JML(Java Modeling Language)是用于对Java程序进行规格化设计的一种表示语言。JML是一种行为接口规格语言(Behavior Interface Specification Language,BISL),基于Larch方法构建。
由上述定义可知,JML诞生的初衷就是对代码进行形式化描述,试图在用自然语言表达的需求与用代码语言表达的实现之间构建一座桥梁,一方面可以消除自然语言可能存在的歧义与模糊,另一方面也可以屏蔽具体的代码实现,实现另一种形式的抽象。
JML基本语法
含义 | |
---|---|
\result | 方法的返回值(非void类型) |
\old(expr ) |
表达式expr 在方法执行前的取值 |
\not_assigned(x, y, ...) | 括号中的变量是否在方法执行过程中未被赋值 |
\not_modified(x, y, ...) | 限制括号中的变量在方法执行期间的取值未发生变化 |
(\forall T x; R(x); P(x)) | 全称量词 |
(\exists T x; R(x); P(x)) | 存在量词 |
(\sum T x; R(x); expr ) |
返回给定范围内的表达式的和 |
(\max T x; R(x); expr ) |
返回给定范围内的表达式的最大值 |
(\min T x; R(x); expr ) |
返回给定范围内的表达式的最小值 |
<==> | 等价操作符 |
==> | 蕴含操作符 |
\nothing |
含义 | |
---|---|
requires | 前置条件,表达的意思是“要求调用者确保P为真”。 |
ensures | 后置条件,表达的意思是“方法实现者确保方法执行返回结果一定满足谓词P的要求,即确保P为真”。 |
assignable | 副作用指方法在执行过程中会修改对象的属性数据或者类的静态成员数据,从而给后续方法的执行带来影响。 |
public normal_behavior | 正常功能,一般指输入或方法关联this对象的状态在正常范围内时所指向的功能。 |
public exceptional_behavior | 异常功能,与正常功能相对 |
signals | 结构为signals (***Exception e) b_expr; |
含义 | |
---|---|
invariant | 不变式(invariant)是要求在所有可见状态下都必须满足的特性 |
constraint |
JML工具链
通过上文,我们已经看到,JML在形式化描述上就语言规范来说已经非常完善了,可以说是把谓词逻辑的那一套完完整整地搬了过来,很好地保证了严谨性,也相对代码要更加易读一些。
但是!!!这里面有两个问题。
首先,JML并不对方法的复杂度有任何限制。于是我们可以看到,在某些方法里,比如addRelation()
,方法本身复杂度并不高,但JML却特别长,让人看得很不耐烦,还不如直接看代码来得简洁明了;而对另一些场景,比如返回的对象比较复杂不是一个简单基本类型的话,就会出现关键字嵌套的情况,这就使得单条JML语句的复杂度也会很高,还不如拆成多行的代码看的清楚。特别地,我尤其对于如何用JML来写递归感到好奇,毕竟我依稀记得一阶逻辑似乎是不完备的哦?
所以这时候可能就有人会说,JML不是写给程序员看的,程序员看代码就好了。那么问题又来了,JML的理想服务对象是谁呢?是掌握谓词逻辑的产品经理?不过,如果真是这样,那我也可以接受。但似乎,JML工具链却为我们指出了另一个潜在服务对象:机器。
简单来说,JML工具链似乎就是为了能够让机器基于规格以形式化验证的方式来代替人类对方法进行完备的测试而诞生的。怎么说呢,光是看这句话就挺心疼人家机器的——这要求也忒高了吧。别说AI了,就是人,你能保证他做到绝对完备吗?他能考虑到一个顾客进来问现在几点了这种操作吗?
为什么说让机器来代替人类进行测试是不现实的呢?很简单,因为机器也是人造的,要想实现基于形式化语言的可泛化测试,而不基于具体的需求本身,就意味着你必须要仅仅凭借上面的那些关键字,就能枚举出它们所有潜在可能组合,而且这个组合中的每一种情况所包含的测试样例还必须是有穷,有阶梯性的。你觉得,如果我随手写一行一阶谓词逻辑,你有信心告诉我这玩意的所有边界情况吗?更何况,你知道的只是一个谓词集合呢?
或许,可能开发者的初衷是只要我把每个前置条件与后置条件还有副作用写清楚,机器针对不同的behavior生成不同的测试样例这样。但是,需要特别指出的是,同一个requires语句下的不同的边界数据才是构造测试的关键,特别是面对非欧拓扑模型(很不巧,我们的Unit3就长这样)。
所以啊,别难为人家开发者和机器了。指望靠JML工具实现自动测试还是等到强AI出来并且还没来得及毁灭人类的时候吧。
吐槽到此结束,对于JML工具链的具体介绍,请参见这个传送门。下文仅涉及openjml与JMLUnitNG的极少数方法。
OpenJML形式化验证
openjml是目前相对较为完备的一套形式化验证Kit,但是遗憾的是它在jdk1.8之后就停止更新了,于是对于我这个一开始用jdk12.0的人就很不友好(上来就把人劝退的那种)。
下面我们以task2中的实例化类RealNetwork为例,演示该工具的使用与其相应结果。假设IDEA项目所在目录为$PATH。
Parsing and Type checking
运行命令:
java -jar openjml.jar -check $PATH\src\network\RealNetwork.java -sourcepath $PATH\src -encoding utf-8
程序正常返回,无任何报错信息。
Extended Static Checking
运行命令:
java -jar openjml.jar -prover z3_4_7 -exec .\Solvers-windows\z3-4.7.1.exe -esc $PATH\src\network\RealNetwork.java -sourcepath $PATH\src -encoding utf-8
其中,-prover参数用于指定Solver类型,-exec参数用于指定Solver可执行程序,-esc参数指定检查类型为Extended Static Checking。
注: Not implemented for static checking: ensures clause containing \not_assigned
错误: An internal JML error occurred, possibly recoverable. Please report the bug with as much information as you can. Reason: Internal exception: class java.lang.NullPointerException
警告: The prover cannot establish an assertion (ExceptionalPostcondition:
警告: Associated declaration:
警告: The prover cannot establish an assertion (UndefinedCalledMethodPrecondition:
警告: Precondition conjunct is false:
错误: ESC could not be attempted because of a failure in typechecking or AST transformation: queryNameRank
至于为什么只找到2个错误,我也不知道
总之,给人一种不明觉厉的感觉吧。(输出信息有近450行,我就不放了)
Runtime Assertion Checking
运行命令:
java -jar openjml.jar -rac $PATH\src\network\RealNetwork.java -sourcepath $PATH\src -encoding utf-8
其中,-rac参数指定检查类型为Runtime Assertion Checking。
运行结果如下:
1 The operation symbol ++ for type java.lang.Object could not be resolved 2 org.jmlspecs.openjml.JmlInternalError: The operation symbol ++ for type java.lang.Object could not be resolved 3 at org.jmlspecs.openjml.JmlTreeUtils.findOpSymbol(JmlTreeUtils.java:291) 4 at org.jmlspecs.openjml.JmlTreeUtils.findOpSymbol(JmlTreeUtils.java:282) 5 at org.jmlspecs.openjml.JmlTreeUtils.makeUnary(JmlTreeUtils.java:739) 6 at com.sun.tools.javac.comp.JmlAttr.createRacExpr(JmlAttr.java:4465) 7 at org.jmlspecs.openjml.ext.QuantifiedExpressions$QuantifiedExpression.typecheck(QuantifiedExpressions.java:214) 8 at com.sun.tools.javac.comp.JmlAttr.visitJmlQuantifiedExpr(JmlAttr.java:4070) 9 at org.jmlspecs.openjml.JmlTree$JmlQuantifiedExpr.accept(JmlTree.java:2685) 10 at com.sun.tools.javac.comp.Attr.attribTree(Attr.java:577) 11 at com.sun.tools.javac.comp.Attr.visitParens(Attr.java:2995) 12 at com.sun.tools.javac.tree.JCTree$JCParens.accept(JCTree.java:1661) 13 at com.sun.tools.javac.comp.Attr.attribTree(Attr.java:577) 14 at com.sun.tools.javac.comp.Attr.attribExpr(Attr.java:619) 15 at com.sun.tools.javac.comp.JmlAttr.attribExpr(JmlAttr.java:6209) 16 at com.sun.tools.javac.comp.JmlAttr.visitJmlMethodClauseExpr(JmlAttr.java:3117) 17 at org.jmlspecs.openjml.JmlTree$JmlMethodClauseExpr.accept(JmlTree.java:2332) 18 at com.sun.tools.javac.comp.JmlAttr.visitJmlSpecificationCase(JmlAttr.java:3361) 19 at org.jmlspecs.openjml.JmlTree$JmlSpecificationCase.accept(JmlTree.java:2837) 20 at com.sun.tools.javac.comp.JmlAttr.visitJmlMethodSpecs(JmlAttr.java:3423) 21 at org.jmlspecs.openjml.JmlTree$JmlMethodSpecs.accept(JmlTree.java:2539) 22 at com.sun.tools.javac.comp.JmlAttr.checkMethodSpecsDirectly(JmlAttr.java:1560) 23 at com.sun.tools.javac.comp.JmlAttr.visitMethodDef(JmlAttr.java:1121) 24 at com.sun.tools.javac.comp.JmlAttr.visitJmlMethodDecl(JmlAttr.java:6053) 25 at org.jmlspecs.openjml.JmlTree$JmlMethodDecl.accept(JmlTree.java:1261) 26 at com.sun.tools.javac.comp.Attr.attribTree(Attr.java:577) 27 at com.sun.tools.javac.comp.Attr.attribStat(Attr.java:646) 28 at com.sun.tools.javac.comp.JmlAttr.attribStat(JmlAttr.java:558) 29 at com.sun.tools.javac.comp.Attr.attribClassBody(Attr.java:4378) 30 at com.sun.tools.javac.comp.JmlAttr.attribClassBody(JmlAttr.java:536) 31 at com.sun.tools.javac.comp.Attr.attribClass(Attr.java:4286) 32 at com.sun.tools.javac.comp.JmlAttr.attribClass(JmlAttr.java:414) 33 at com.sun.tools.javac.comp.JmlAttr.completeTodo(JmlAttr.java:492) 34 at com.sun.tools.javac.comp.JmlAttr.attribClass(JmlAttr.java:458) 35 at com.sun.tools.javac.comp.Attr.attribClass(Attr.java:4215) 36 at com.sun.tools.javac.comp.Attr.attrib(Attr.java:4190) 37 at com.sun.tools.javac.main.JavaCompiler.attribute(JavaCompiler.java:1258) 38 at com.sun.tools.javac.main.JmlCompiler.attribute(JmlCompiler.java:479) 39 at com.sun.tools.javac.main.JavaCompiler.compile2(JavaCompiler.java:898) 40 at com.sun.tools.javac.main.JmlCompiler.compile2(JmlCompiler.java:712) 41 at com.sun.tools.javac.main.JavaCompiler.compile(JavaCompiler.java:867) 42 at com.sun.tools.javac.main.Main.compile(Main.java:553) 43 at com.sun.tools.javac.main.Main.compile(Main.java:410) 44 at org.jmlspecs.openjml.Main.compile(Main.java:581) 45 at com.sun.tools.javac.main.Main.compile(Main.java:399) 46 at com.sun.tools.javac.main.Main.compile(Main.java:390) 47 at org.jmlspecs.openjml.Main.execute(Main.java:417) 48 at org.jmlspecs.openjml.Main.execute(Main.java:375) 49 at org.jmlspecs.openjml.Main.execute(Main.java:362) 50 at org.jmlspecs.openjml.Main.main(Main.java:334) 51 错误: An internal JML error occurred, possibly recoverable. Please report the bug with as much information as you can. 52 Reason: com.sun.tools.javac.code.Type$BottomType cannot be cast to com.sun.tools.javac.code.Type$ArrayType 53 java.lang.ClassCastException: com.sun.tools.javac.code.Type$BottomType cannot be cast to com.sun.tools.javac.code.Type$ArrayType 54 at org.jmlspecs.openjml.JmlTreeUtils.copyArray(JmlTreeUtils.java:1546) 55 at org.jmlspecs.openjml.esc.JmlAssertionAdder.visitJmlMethodInvocation(JmlAssertionAdder.java:15456) 56 at org.jmlspecs.openjml.JmlTree$JmlMethodInvocation.accept(JmlTree.java:2229) 57 at com.sun.tools.javac.tree.TreeScanner.scan(TreeScanner.java:49) 58 at org.jmlspecs.openjml.vistors.JmlTreeScanner.scan(JmlTreeScanner.java:67) 59 at org.jmlspecs.openjml.esc.JmlAssertionAdder.convertExpr(JmlAssertionAdder.java:1504) 60 at org.jmlspecs.openjml.esc.JmlAssertionAdder.visitParens(JmlAssertionAdder.java:10686) 61 at com.sun.tools.javac.tree.JCTree$JCParens.accept(JCTree.java:1661) 62 at com.sun.tools.javac.tree.TreeScanner.scan(TreeScanner.java:49) 63 at org.jmlspecs.openjml.vistors.JmlTreeScanner.scan(JmlTreeScanner.java:67) 64 at org.jmlspecs.openjml.esc.JmlAssertionAdder.convertExpr(JmlAssertionAdder.java:1504) 65 at org.jmlspecs.openjml.esc.JmlAssertionAdder.visitBinary(JmlAssertionAdder.java:11862) 66 at com.sun.tools.javac.tree.JCTree$JCBinary.accept(JCTree.java:1785) 67 at com.sun.tools.javac.tree.TreeScanner.scan(TreeScanner.java:49) 68 at org.jmlspecs.openjml.vistors.JmlTreeScanner.scan(JmlTreeScanner.java:67) 69 at org.jmlspecs.openjml.esc.JmlAssertionAdder.convertExpr(JmlAssertionAdder.java:1504) 70 at org.jmlspecs.openjml.esc.JmlAssertionAdder.convertJML(JmlAssertionAdder.java:1647) 71 at org.jmlspecs.openjml.esc.JmlAssertionAdder.convertJML(JmlAssertionAdder.java:1664) 72 at org.jmlspecs.openjml.esc.JmlAssertionAdder.convertNoSplit(JmlAssertionAdder.java:1685) 73 at org.jmlspecs.openjml.esc.JmlAssertionAdder.visitJmlQuantifiedExpr(JmlAssertionAdder.java:16233) 74 at org.jmlspecs.openjml.JmlTree$JmlQuantifiedExpr.accept(JmlTree.java:2685) 75 at com.sun.tools.javac.tree.TreeScanner.scan(TreeScanner.java:49) 76 at org.jmlspecs.openjml.vistors.JmlTreeScanner.scan(JmlTreeScanner.java:67) 77 at org.jmlspecs.openjml.esc.JmlAssertionAdder.convertExpr(JmlAssertionAdder.java:1504) 78 at org.jmlspecs.openjml.esc.JmlAssertionAdder.visitParens(JmlAssertionAdder.java:10686) 79 at com.sun.tools.javac.tree.JCTree$JCParens.accept(JCTree.java:1661) 80 at com.sun.tools.javac.tree.TreeScanner.scan(TreeScanner.java:49) 81 at org.jmlspecs.openjml.vistors.JmlTreeScanner.scan(JmlTreeScanner.java:67) 82 at org.jmlspecs.openjml.esc.JmlAssertionAdder.convertExpr(JmlAssertionAdder.java:1504) 83 at org.jmlspecs.openjml.esc.JmlAssertionAdder.convertJML(JmlAssertionAdder.java:1647) 84 at org.jmlspecs.openjml.esc.JmlAssertionAdder.convertJML(JmlAssertionAdder.java:1664) 85 at org.jmlspecs.openjml.esc.JmlAssertionAdder.convertNoSplit(JmlAssertionAdder.java:1685) 86 at org.jmlspecs.openjml.esc.JmlAssertionAdder.visitJmlQuantifiedExpr(JmlAssertionAdder.java:16233) 87 at org.jmlspecs.openjml.JmlTree$JmlQuantifiedExpr.accept(JmlTree.java:2685) 88 at com.sun.tools.javac.tree.TreeScanner.scan(TreeScanner.java:49) 89 at org.jmlspecs.openjml.vistors.JmlTreeScanner.scan(JmlTreeScanner.java:67) 90 at org.jmlspecs.openjml.esc.JmlAssertionAdder.convertExpr(JmlAssertionAdder.java:1504) 91 at org.jmlspecs.openjml.esc.JmlAssertionAdder.visitParens(JmlAssertionAdder.java:10686) 92 at com.sun.tools.javac.tree.JCTree$JCParens.accept(JCTree.java:1661) 93 at com.sun.tools.javac.tree.TreeScanner.scan(TreeScanner.java:49) 94 at org.jmlspecs.openjml.vistors.JmlTreeScanner.scan(JmlTreeScanner.java:67) 95 at org.jmlspecs.openjml.esc.JmlAssertionAdder.convertExpr(JmlAssertionAdder.java:1504) 96 at org.jmlspecs.openjml.esc.JmlAssertionAdder.convertJML(JmlAssertionAdder.java:1647) 97 at org.jmlspecs.openjml.esc.JmlAssertionAdder.addPostConditions(JmlAssertionAdder.java:4758) 98 at org.jmlspecs.openjml.esc.JmlAssertionAdder.convertMethodBodyNoInit(JmlAssertionAdder.java:1195) 99 at org.jmlspecs.openjml.esc.JmlAssertionAdder.visitJmlMethodDecl(JmlAssertionAdder.java:15127) 100 at org.jmlspecs.openjml.JmlTree$JmlMethodDecl.accept(JmlTree.java:1261) 101 at com.sun.tools.javac.tree.TreeScanner.scan(TreeScanner.java:49) 102 at org.jmlspecs.openjml.vistors.JmlTreeScanner.scan(JmlTreeScanner.java:67) 103 at org.jmlspecs.openjml.esc.JmlAssertionAdder.visitJmlClassDecl(JmlAssertionAdder.java:13784) 104 at org.jmlspecs.openjml.JmlTree$JmlClassDecl.accept(JmlTree.java:1174) 105 at com.sun.tools.javac.tree.TreeScanner.scan(TreeScanner.java:49) 106 at org.jmlspecs.openjml.vistors.JmlTreeScanner.scan(JmlTreeScanner.java:67) 107 at org.jmlspecs.openjml.esc.JmlAssertionAdder.convert(JmlAssertionAdder.java:1414) 108 at com.sun.tools.javac.main.JmlCompiler.rac(JmlCompiler.java:610) 109 at com.sun.tools.javac.main.JmlCompiler.desugar(JmlCompiler.java:454) 110 at com.sun.tools.javac.main.JavaCompiler.compile2(JavaCompiler.java:898) 111 at com.sun.tools.javac.main.JmlCompiler.compile2(JmlCompiler.java:712) 112 at com.sun.tools.javac.main.JavaCompiler.compile(JavaCompiler.java:867) 113 at com.sun.tools.javac.main.Main.compile(Main.java:553) 114 at com.sun.tools.javac.main.Main.compile(Main.java:410) 115 at org.jmlspecs.openjml.Main.compile(Main.java:581) 116 at com.sun.tools.javac.main.Main.compile(Main.java:399) 117 at com.sun.tools.javac.main.Main.compile(Main.java:390) 118 at org.jmlspecs.openjml.Main.execute(Main.java:417) 119 at org.jmlspecs.openjml.Main.execute(Main.java:375) 120 at org.jmlspecs.openjml.Main.execute(Main.java:362) 121 at org.jmlspecs.openjml.Main.main(Main.java:334) 122 $PATH\src\network\RealNetwork.java:193: 注: Runtime assertion checking is not implemented for this type or number of declarations in a quantified expression 123 return groups.get(id).getRelationSum(); 124 ^ 125 $PATH\src\network\RealNetwork.java:248: 注: Runtime assertion checking is not implemented for this type or number of declarations in a quantified expression 126 GraphnewGraph = new Graph<>(personSet); 127 ^ 128 $PATH\src\network\RealNetwork.java:249: 注: Runtime assertion checking is not implemented for this type or number of declarations in a quantified expression 129 return newGraph.isReachable(oriPerson, dstPerson); 130 ^ 131 $PATH\src\com\oocourse\spec2\main\Group.java:33: 注: Runtime assertion checking is not implemented for this type or number of declarations in a quantified expression 132 @ (\sum int j; 0 <= j && j < people.length && people[i].isLinked(people[j]); 1)); 133 ^ 134 $PATH\src\com\oocourse\spec2\main\Group.java:38: 注: Runtime assertion checking is not implemented for this type or number of declarations in a quantified expression 135 @ (\sum int j; 0 <= j && j < people.length && 136 ^ 137 $PATH\src\com\oocourse\spec2\main\Group.java:45: 注: Runtime assertion checking is not implemented for this type or number of declarations in a quantified expression 138 @ ensures (\exists BigInteger[] temp; 139 ^ 140 1 个错误
目测似乎是openjml程序内部什么地方出现了一些奇怪的bug,不是很清楚,因为没有去看源码。但有一点应该可以确定,那就是这个工具的开发者尽早弃坑是灰常明智的选择。
JMLUnitNG测试
本测试针对RealGroup类,依次运行以下3条命令:
java -jar ..\jmlunitng.jar network\RealGroup.java -cp . javac -cp ".;..\jmlunitng.jar" network\RealGroup_JML_Test.java java -cp ".;..\jmlunitng.jar" network.RealGroup_JML_Test
1 [TestNG] Running: 2 Command line suite 3 4 Failed: racEnabled() 5 Passed: constructor RealGroup(-2147483648) 6 Passed: constructor RealGroup(0) 7 Passed: constructor RealGroup(2147483647) 8 Failed: <>.addPerson(null) 9 Failed: < >.addPerson(null) 10 Failed: < >.addPerson(null) 11 Passed:< >.equals(null) 12 Passed:< >.equals(null) 13 Passed:< >.equals(null) 14 Passed: < >.equals(java.lang.Object@574caa3f) 15 Passed: < >.equals(java.lang.Object@64cee07) 16 Passed: < >.equals(java.lang.Object@1761e840) 17 Passed: < >.getAgeMean() 18 Passed: < >.getAgeMean() 19 Passed: < >.getAgeMean() 20 Passed: < >.getAgeVar() 21 Passed: < >.getAgeVar() 22 Passed: < >.getAgeVar() 23 Passed: < >.getConflictSum() 24 Passed: < >.getConflictSum() 25 Passed: < >.getConflictSum() 26 Passed: < >.getId() 27 Passed: < >.getId() 28 Passed: < >.getId() 29 Passed: < >.getPeopleSum() 30 Passed: < >.getPeopleSum() 31 Passed: < >.getPeopleSum() 32 Passed: < >.getRelationSum() 33 Passed: < >.getRelationSum() 34 Passed: < >.getRelationSum() 35 Passed: < >.getValueSum() 36 Passed: < >.getValueSum() 37 Passed: < >.getValueSum() 38 Passed: < >.hasPerson(null) 39 Passed: < >.hasPerson(null) 40 Passed: < >.hasPerson(null) 41 Passed: < >.hashCode() 42 Passed: < >.hashCode() 43 Passed: < >.hashCode() 44 Passed: < >.updateRelation(-2147483648) 45 Passed: < >.updateRelation(-2147483648) 46 Passed: < >.updateRelation(-2147483648) 47 Passed: < >.updateRelation(0) 48 Passed: < >.updateRelation(0) 49 Passed: < >.updateRelation(0) 50 Passed: < >.updateRelation(2147483647) 51 Passed: < >.updateRelation(2147483647) 52 Passed: < >.updateRelation(2147483647) 53 54 ====================================== 55 Command line suite 56 57 Total tests run: 49, Failures: 4, Skips: 0 58 59 ======================================
可以看到,所有的测试该通过的都通过了,没通过的那几个是因为addPerson()
本来就没指望输入会有null的情况。但是另一方面,仔细观察它的这些测试样例,我们不难发现,它好像测了个寂寞?
首先,方法与方法之间的测试是割裂的,jmlunitNG显然并不具备构造复合测试用例的能力,也就不可能适应的了动态更新缓存的场景,而这正是RealGroup类为了性能优化而做出的改进之一。此外,作为面向对象的语言,测试却是一副面向过程的做派。C可忍,J不可忍!
其次,这货对于测试边界数据的定义显然非常可爱:基本数据类型就直接取边界数据(MAX_VALUE, MIN_VALUE, 0),自定义对象就来个null
,真的是很傻很天真,蠢萌蠢萌的那种。只是可惜了,程序员的世界可没你想象的那么美好,还要加油啊,少年。
模型架构设计
本单元的核心任务是构建一个社交网络模型,对于这一模型,需要在基于规格实现的准确性基础上做到尽可能高的性能优化。同时我们注意到,社交网络的本质是一个图结构,本单元则是加权无向图,因此在架构上,一个基本的出发点就是做到算法层与应用层的最大解耦,算法层负责性能的优化,应用层负责进行简单的初始化与异常判断。这样做的结果,不仅仅是做到了降低类的复杂度,同时也提高了算法与模型的复用性,毕竟谁知道以后会不会再用到呢,就让轮子一直是轮子好了。
于是乎,我在底层的模型类中,大量使用了泛型与内部类,参考HashMap的实现,将Unit3中用到的Graph, UnionFindSet, Heap乃至均值方差的动态更新都单独拎了出来构造类,并保证在之后的作业中可以随时复用之。
整体架构如下图所示:
这张看的可能反而有点乱,再来个清楚的:
│ Main.java
│
├─com
│ └─oocourse
│ └─spec3
│ ├─exceptions
│ │ EqualGroupIdException.java
│ │ EqualPersonIdException.java
│ │ EqualRelationException.java
│ │ GroupIdNotFoundException.java
│ │ PersonIdNotFoundException.java
│ │ RelationNotFoundException.java
│ │
│ └─main
│ Group.java
│ Network.java
│ Person.java
│ Runner.java
│
├─network
│ AbstractGroup.java
│ AbstractPerson.java
│ RealGroup.java
│ RealNetwork.java
│ RealPerson.java
│
└─utils
Graph.java
Heap.java
HeapNode.java
Node.java
Pair.java
StatKit.java
UnionFindSet.java
可以看到,所有的模型类全部被放到了utils包中,且由于采用泛型的思想,因此其复用性极高,而不只是限于本次作业。但是,这在一定程度上也带来了一些隐患,因为在某种意义上,调用者与被调用者的关系,就像是需求方与实现方之间的关系。如果出现了上层以为下层考虑到了,下层以为上层给它屏蔽了这样的尴尬情况的话,就有点贻笑大方了。
除此之外,为了在不改变课题组提供的代码的前提下将模型解耦,我专门设计了Abstract*.java
接口,用于封装一些原接口中未声明但需要的方法,以及将Graph对应的节点Node特征在不经过`Person
接口的前提下令RealPerson.java
继承之,从而减少不必要的Graph的冗余存储,而是利用Node提供的getNeighbors()
方法以邻接表的形式对节点进行访问和遍历。
算法分析
Task3中,由于涉及到对连通性、最短路、割点等内容的应用,因此必要的算法是必不可少的。由于点连通分量与割点我也是第一次接触其具体应用,因此也经历了一个逐渐迭代的过程,在此将所有尝试的算法列举如下:
假设节点数为n,边数为m
isCircle(): 采用并查集实现(路径压缩+秩优化),摊还复杂度为,其中为阿克曼函数的逆函数,现实中值一般不超过4
queryMinPath(): 采用Dijkstra算法实现,最终版由于使用了堆优化,因此复杂度降为,尤其适合本单元的稀疏图
isStrongLinked(): 最终版采用tarjan算法,复杂度为O(n+m),通过将DFS中的边压栈来保证不会漏掉割点
测试与bug分析
Unit3对我而言有着特殊的意义,这种意义很大程度上不是因为jml,不是因为算法,而是因为测试。它为我提供了一个契机,让我重新去思考测试的意义,思考究竟怎样的测试是有效的,怎样的测试是合理的。
首先,测试应该包含以下三个部分:
-
构造测试样例
-
批处理获取用户程序输出
-
检验输出正确性
这三部分,对于不同的场景,挑战性各不相同。
对于第一单元,由于课程组提供了标准输出scipy与基于抽样的正确性检测方法,因此2和3部分的难度可以忽略不计,关键在于如何构造测试样例。而对于表达式这种模型,整体上规律还是很强的(毕竟可以写成正则),因此大规模随机就可以有很好的效果,再加上一些极边界情况的数据,正确性就能够覆盖个十之八九了。
但即使如此,自己在task2的时候还是错了一个点,原因就是没有考虑到不合法的空白符。试想如果每一个测试点都加上这么一个不合法的空白符,那么自己task2不就直接0分了?
对于第二单元,由于是强制在线,因此第2部分的难度陡然增加,毕竟单纯的循环等待效率实在太低,这就需要我们使用多进程并发的方式进行测试;但是由于乘客的请求数据结构也非常之简单,并且输出可以通过流程验证来检查整个电梯系统的状态迁移与输入之间是否合法,因此第1部分与第3部分的难度并不高,所以就还算OK。
但是因为用户程序也是多线程的,而多线程本身具有一些非常隐蔽的如死锁一样的极难复现的bug,因此这也客观上增加了100%debug的难度。可以说,在多线程的世界里,你永远无法保证自己的程序是绝对正确的。
那么现在,故事兜兜转转来到了第三单元。
第三单元的任务具有什么特征呢?
首先,由于是社交网络的模拟,因此方法种类很多,这就体现在输入种类的极大丰富上;其次,单方法的内容相对简单,基本可以靠人眼判断。但是,这些方法之间的组合所诞生的可能性却很多,因此乍一眼看去往往会给人一种无从下手的感觉。最后,在结果的判定下,由于不存在第一单元的标程与第二单元的流程验证,因此总体正确性很难进行判断。
也就是说,第三单元的测试在第1部分与第2部分的挑战性都比较大。正是这一特征,使得黑盒测试的优势不再,基于Junit单元测试的白盒测试才有了它的用武之地。
但事实上,单元测试虽然能够一定程度上简化1和3,但不可能将其简化到0的地步。具体而言,输出的正确性还是需要你进行assert*()
验证,而输入更是要你基于不同的可能场景构造相应的输入样例。
此外,单元测试也不应停留在以单个方法为基本单元的测试上,它也必须要有一定的组合能力。事实上,对于非常简单的方法,形式化验证要比单元测试更有效果。
单元测试能够起效的另一个关键之处在于构造合适的测试样例。或者不如说,构造样例永远是任何测试最重要的一环:如果你构造测试数据的时候就没有考虑到全部的情况,那么无论你再怎么测你的程序都永远会是对的。而Unit1,Unit2自己在这上面严重依赖于随机生成,因此并未及时认识到手动构造样例的重要性。
综上,我们可以对第三单元的测试得出以下结论:
-
总体应以单元测试为主,形式化验证为辅
-
单元测试应只是提供一个框架,有意义的测试数据才是灵魂
-
要考虑多种方法组合的复杂情况
那么,现在回到Unit3本身。
在Unit3中,自己在task1, task3均未出现bug,在task2却出现了一个极为致命的bug:除零异常导致的RE。也就是在创建一个Group但还没有向其中加人之前就询问该组的平均年龄,我的方法就会报错。
之所以出现此问题,从事后诸葛亮的角度来看,大概可以归结为以下几点:
-
直接原因:读规格设计的时候对三元运算符不敏感,被public_nomal_behavior的定式套路限制了,下意识地以为同一个normal_behavior可以在同一个分支逻辑下完成,无需再进行特判。(而且我来回读了那么多遍tm愣是一次都没发现我也是醉了)
-
间接原因:在task2的时候,自己还没有大幅度引入单元测试,而是仅用其进行单方法的压力测试,对于方法的正确性检查自己主要依靠形式化验证,也就是肉眼比对代码与规格;这一方法在task1证明了其很强的高效性,由于task2单方法的复杂度没有显著上升,因此我也直接拿来用了。但这就为1中问题的出现埋下了隐患。
-
间接原因:在task1的时候,自己就有意识地对应用与算法进行解耦合,task2中,这一思想更是发挥到了极致——我选择了通过分包来表现这样的区分关系。但这么做,某种意义上也割裂了上层与下层之间直接交流的通道。于是,对于这种特判情况,我在看Group的规格时对照的是RealGroup中的那一行get方法,真正的实现却藏在算法层。两者之间无法有效衔接。这种情况下,如果每一个normal_behavior对应的情况相同,那就不会出现什么问题;但对于1中的场景,难免就贻笑大方了。
-
根本原因:在task2里,我仍然没能认识到测试的关键所在,总觉得如果自己能够通过比对规格判断代码逻辑是否正确的话,再用Junit框架写一遍这个过程又有什么必要呢?但事实上,如前所述,junit的意义在于通过构造合适子集的测试样例检验程序在局部的正确性,局部不意味着单个方法,子集不意味着一种情况。而自己出现的这个bug正是自己没能将多个方法进行真正意义上的结合,没能够构造出区域性的测试样例的结果。
那么,这样的经验教训对于今后自己在写代码的时候又有什么启发式的意义呢?
简单来说的话,就是一定要考虑全面,审题一定要再仔细些这样。具体来讲的话呢,
-
基于数据的测试与基于形式的测试在任何场景下都很重要,后者适用于对局部的流程进行正确性检查;前者则尤其适用于对程序整体,或者是一个子集,也就是对多个独立方法的组合进行正确性检查,当然还有压力检测。
-
规格最大的用处是写给人看的,所以如果这玩意不是自己亲自写出来的东西的话,还请务必一行一行给我看清楚,一个字都不许漏掉。
-
请不要轻视每一件事。错误永远不会发生在你最重视的那个山头上,反倒是康庄大道更容易给你使绊子。
感想与总结
再谈规格
规格啊,真的挺有趣的,我并不否定它存在的意义,但我同时也认为不应该赋予其过多的意义。
联系我们平时的学习场景,究竟什么时候需要用到规格呢?
如果自己既是产品经理又是程序员,那么只要用自己能够理解的一套符号把所有的需求全部表达清楚就好了,根本没有使用那么严谨的语言的必要,写着还劳神劳力,第二天看的时候还极有可能对自己发出灵魂三问,实在是算了算了。
但将来,自己可能不会同时扮演这些角色了,而这时候规格的用处就体现出来了。我想,这可能是课程组在第三单元选择规格设计为主题的初衷吧。在一个大型项目中,我们如果不是首席架构师,就没必要考虑的那么面面俱到,但我们必须准确地完成上层所布置的任务。为了使这个过程不出现二义性,就必须要又这样一种既屏蔽实现又绝对准确的形式化语言来作为媒介。因此,规格就显得至关重要了。
不过怎么说呢,重新审视第三单元,虽然自己的总体成绩不如前两个单元。但说实在的,我认为这个单元的总体挑战度确实要低于前两个单元。我想,这可能是课程组为了突出规格的重要性而不小心忽略掉了整个项目作为一个整体的自身的复杂度的构建吧。
复杂度主要体现在纵向的层次感,但反观第三单元,为了可以增大规格的篇幅,核心的方法完完全全是并行的,是一个非常宽非常shallow的架构,那么多的query,本质上却都是在询问一个图模型的各种信息而已,纵向的层次趋近于0。而且,这一单元的指导书虽然大道至简,但是规格给出的具体规约却比前两个单元的指导书管的还要宽,连Person类的具体细节都不放过。这两个原因,直接使得同学们作为具体的实现者的操作空间非常有限,算是提前体验了一把在大公司的那种感觉?
那么,要想做出改进,该怎么做呢?
我觉得一个可行的思路就是继续延续前两个单元指导书的风格。首先,现在Unit3的这种情况给人一种好像只有Unit3配拥有规格的感觉一样。但显然,Unit1,Unit2中指导书的那些要求也完全应该可以用规格实现才对(否则规格的泛化性体现在哪里)。因此,Unit3应该在选择场景时保持不低于前两个单元的水准,然后在给出要求的时候以润物无声的方式将规格融入到核心方法的要求上去,替代原先要在指导书中表达的内容。(你想想如果把现在Unit3的规格翻译成人话写成指导书,那篇幅)。而对于一些间接用到的内部方法,就完全没必要给出规格了吧讲道理。
或者,还有一个简单粗暴的思路。那就是把Unit3移到Unit1去。因为说白了,规格就是离散数学里逻辑的那一套东西,如果对于模型复杂度要求不高的话,可能相比于现在的Unit1,对于大部分同学而言反而要更加友好一些,又没有性能的压力,又没有来回嵌套的复杂模型,岂不美滋滋?也许课程组现在的思路是想首先展示面向对象的核心,也就是类的继承层次等关系。但,怎么说呢,这些思想其实在整个OO中都是一直贯彻的,Unit3里体现的反而不是很强烈,所以,慢慢来或许会更好。
最后的最后,是自己的一点小感想,请课程组利益相关的人士忽略。
嘛,都说了忽略了我干嘛还要写在这里啊?真是傻的不行。