Design by Contract
JML语言的理论基础
JML设计目的
契约式设计是一种开发软件的方法,其背后的主要思想是一个类及其客户之间有“契约”。 客户必须在调用类定义的方法之前保证某些条件,而作为回报,类保证在调用过程中,调用结束后将保留的某些属性。
书写规范
- JML以特定格式的注释形式写入程序中,分为块注释和行注释
- 块注释形式为/*@annotation@*/,注释中每一行以@开头
- 行注释形式为//@annotation
主要表达式
原子表达式:
- esult: 它的值是方法返回的值,它的类型是方法的返回类型
- old(expr)或pre(expr): 用来表示一个表达式expr在相应方法执行前具有的值
- ot_assigned(store-ref-list): 用来表示store-ref-list中的变量是否在方法执行过程中被赋值,若没有被赋值,返回为true,否则返回false
- ot_modified(store-ref-list): 用来表示store-ref-list中的变量是否在方法执行过程中取值发生变化,若没有发生变化,返回为true,否则返回false
- only_accessed(store-ref-list): 用来表示在方法执行过程中是否只读取store-ref-list中的数据,若没有读取其他数据组的数据,返回为true,否则返回false
量化表达式:
此处用expr指代spec-quantified-expr
spec-quantified-expr::=(quantifier quantified-var-decls ;[ [ predicate ] ; ]spec-expression)
- (max expr) (min expr) (product expr) (sum expr): 分别返回expr给定范围的最大值,最小值,乘积或总和
- (forall expr): 全称量词修饰的表达式,表示对于给定范围内的元素都满足相应的约束
- (exist expr): 存在量词修饰的表达式,表示对于给定范围内的元素存在某个元素满足相应的约束
**运算符: **
- E1<:E2: 若E1是E2的子类型,则该表达式的结果为真,否则为假
- <==>、<=!=>: 分别与==和 !=具有相同的含义,但比==和!=优先级低
- ==>、<==: 正向和反向蕴涵运算符
- othing: 表示一个空集
- everything: 表示一个全集
**方法规格: **
- pre-condition: 前置条件,通过requires子句来表示,形式为requires expr,即要求调用者确保expr为真
- post-condition: 后置条件,通过ensures子句来表示,ensures expr,即要求方法实现者确保方法执行后expr为真
- side-effects:副作用,通过assignable子句或modifiable子句来表示,指明方法在执行过程中会修改对象的属性数据或者类的静态成员数据
- pure: 若方法不需要调用者输入参数,也不会有任何副作用,且执行一定会正常结束,则可用pure修饰
- expcetional_behavior: 异常行为规格
- also: JML关键词,它的意思是除了正常功能规格外,还有一个异常功能规格
- signals子句: 结构为 signals (***Exception e) b_expr ,意思是当 b_expr 为 true 时,方法会抛出括号中给出的相应异常e
**类型规格: **
- invariant: 不变式,表示 invariant expr 在所有可见状态下都必须满足expr的特性
- constraint: 修改约束,用constraint epxr来对前序可见状态和当前可见状态的关系进行约束
JML应用工具链情况
- JML compiler(jmlc): JML编译器是对Java编译器的扩展,可以将JML规范标注的Java程序编译为相应的Java字节代码,其编译后的字节码包括运行时断言检查指令用于检查前置条件、后置条件、异常行为、不变式等
- JML: 检查JML规范的另一个工具,其不需要编译代码即可执行静态检查
- jmlunit: JML单元测试工具,将jmlc与当下流行的单元测试工具Junit结合起来
- jmldoc: JML文档生成工具,可生成包含Javadoc注释和任何JML规范的java文档
部署JMLUnitNG
被测试代码:
package demo;
public class Demo {
/*@ assignable
othing;
@ ensures
esult >= a &&
@
esult >= b &&
@
esult >= c;
@*/
public static int getMax(int a,int b,int c) {
int max;
if (a >= b && a >= c) {
max = a;
} else if (c >= a && c >= b) {
max = c;
} else {
max = b;
}
return max;
}
public static void main(String[] args) {
getMax(-2147483648,2147483647,0);
}
}
测试结果:
分析:
在这个简单测试中,JMLUnitNG生成了边界数据和非法数据进行测试,极大简化了编程人员的测试任务。
架构设计与两次迭代
第一次作业
MyPath:
-
ArrayList<Integer>: 按次序记录Path中各结点
-
HashSet<Integer>: 记录Path中的不同结点,用于getDistinctNodeCount()方法直接返回path不同结点的个数
-
Int: 记录该Path的hashcode值
-
构造方法
public MyPath(int... nodeList) { for (int temp :nodeList) { nodeArrList.add(temp); tempSet.add(temp); } hashCode = nodeArrList.hashCode(); }
因为Path是不变类,在构造方法中直接算出hashCode,在hashCode()方法中直接返回。
MyPathContainer:
- HashMap<Path,Integer>: Path方法重写了hashCode()方法,可以将Path作为key建立Path与PathId的对应关系,理论可以在O(1)复杂度下通过Path得到PathId
- HashMap<Integer,Path>: 将PathId作为key建立PathId与Path的对应关系,理论可以在O(1)复杂度下通过PathId得到Path
- HashMap<Integer,Integer>: 记录nodeId在Container中出现的次数,便于从container移出一个Path对象能正确地将结点从container中移除,确保了getDistinctNodeCount()的正确性
第二次作业
UML:
MyGraph:
-
继承:
在第二次作业中,MyGraph作为MyPathContainerde子类,继承了第九次作业的MyPathContainer
-
private HashMap<Integer,HashMap<Integer,Integer>> graphEdgeMatrix;
记录两结点间边的条数,便于从Path移出Path时,对Graph的维护
-
private HashMap<Integer,HashMap<Integer,Integer>> graphLengthMatrix;
记录两结点最短路长度,在每次对图进行变动时(addPath,removePath),利用BFS算法更新结点间最短路的长度
-
求最短路:BFS:
这次作业的图是无向无权图,BFS是比较简洁的选择之一,即便第三次作业引入有向有权图,将其改为Dijkstra也比较方便
第三次作业
UML:
WeightGraph:
-
继承:
本次作业需要三种有权图分别用于求最短换乘、最低票价、最低不满意度,但三者均是有权无向图,只是每个图中边的权不同
因此,
TransGraph
、PriceGraph
、UnpleasantGraph
继承自WeighGraph
,各自重写了获得边权的方法。 -
求最短路:
意料之中,这次作业添加了有权无向图,直接改写第二次作业的BFS方法实现堆优化的Dijkstra的算法
MyRailwaySystem:
-
继承:
MyRailwaySystem继承自第二次作业的MyGraph,再添加三个WeightGraph成员变量以实现要求的方法
bug分析
自己在公测和互测的bug:
三次未被找出bug
互测:
hack策略:黑盒测试
对方将各nodeId映射到数组时有溢出错误。
心得体会
模块设计:
使用JML,我们不得不面临一个问题——程序员在开始写码之前,必须对类和方法进行很好地抽象、设计。当然这三次作业中,这些是Client提供的,我们只是作为供应者实现需求。但仅是如此,我也感受到契约式编程将架构设计与具体实现分离的好处,这有助于在具体实现前思考出更优秀的架构(如果架构不清晰,快速写出规格也是不可能的),而在实现时不受约束。
契约式编程带来的效率:
防御性检查会减慢方法运行速度,而契约式编程避免了无效的防御性检查,增加了代码的效率。
对于供应者而言,一份好的JML极大释放了我的编程前期准备时间,只需专注于按JML完成一个方法的正确实现。在测试和debug阶段,因为很好的模块化架构,让供应者很好地进行单元调试。
自动化测试:
与对拍不同,junit单元测试能更好地测试方法和模块的逻辑是否正确。它在一定程度上确保了,在未来,我重构代码后该模块依然能够正常工作,并且JMLunit的引入更好地实现了契约式编程的单元测试。