OO_2019_第三单元总结——规格化设计
通过实践,了解到规格化设计对代码实现和代码维护都有指导意义,有必要掌握并灵活运用。
JML语言的理论基础、应用工具链
理论基础
JML( Java Modeling Language)是用于对Java程序进行规格化设计的一种表示语言。JML是一种行为接口规格语言,可以作为java的注释写入源文件,为严格的程序设计提供了一套行之有效的方法。通过JML及其支持工具,可以基于规格自动构造测试用例,并整合了SMT Solver等工具可以以静态方式来检查代码实现对规格的满足情况。
一般而言,JML有两种主要的用法:
(1)开展规格化设计。这样交给代码实现人员的将不是可能带有内在模糊性的自然语言描述,而是逻辑严格的规格。
(2)针对已有的代码实现,书写其对应的规格,从而提高代码的可维护性。这在遗留代码的维护方面具有特别重要的意义。
下面梳理一下本单元主要用到的一些JML语法:
原子表达式
\result表达式:方法执行后的返回值
\old(expr
)表达式:表示一个表达式expr
在相应方法执行前的取值
量化表达式
\forall表达式:全称量词修饰的表达式
\exists表达式:存在量词修饰的表达式
操作符
等价关系操作符: b_expr1<==>b_expr2
或者 b_expr1<=!=>b_expr2
推理操作符: b_expr1==>b_expr2
或者 b_expr2<==b_expr1
方法规格
requires:
前置条件
ensures
:后置条件
assignable
:副作用范围限定
pure
:声明一个方法是没有副作用的
类型规格
不变式(invariant):要求在所有可见状态下都必须满足的特性
状态变化约束(constraint):对前序可见状态和当前可见状态的关系进行约束
应用工具链
OpenJML用于检查JML语法问题,是否符合规格。
JMLUnitNG根据JML语言自动生成TestNG测试。
部署JMLUnitNG
1 // demo/Demo.java 2 package demo; 3 4 public class Demo { 5 /*@ public normal_behaviour 6 @ ensures \result == lhs - rhs; 7 */ 8 public static int compare(int lhs, int rhs) { 9 return lhs - rhs; 10 } 11 12 public static void main(String[] args) { 13 compare(114514,1919810); 14 } 15 }
运行jmlunitng.jar,产生如下系列文件
运行Demo_JML_Test.java,产生如下结果:
JMLUnitNG可以根据规格自动生成测试样例,可以看出,生成的测试样例全面覆盖,注重边界数据的测试。
梳理架构设计,分析迭代中对架构的重构
三次作业层层递进,因为选择了较为简洁的结构,所以每次都可以在前一次作业的基础上扩充,没有进行大规模重构,这是一大进步。
但是三次作业内部都没有按需求构建新的类,只是将所有数据结构和方法都写在一起,在这个方面需要重新思考,重新组织各个类之间的关系。
第一次:
第二次:
第三次:
分析代码实现和bug情况
代码实现
按照规格写对应方法,每次作业需要根据不同情况选取存储数据的容器和算法。
第一次:
路径管理系统,通过各类输入指令来进行数据的增删查改等交互
对于DISTINCT_NODE_COUNT
指令,可能会因为复杂度高导致超时,所以我进行了改进:最开始全部使用arraylist,每次进行O(n^2)复杂度的查询,后增加所有节点的hashset,但发现不能处理删除路径的情况,最后采用hashmap<结点编号,出现次数>,在每次增加或删除路径时变更hashmap,在查询DISTINCT_NODE_COUNT
时只需要返回该hashmap的size即可。
使用两个hashmap存所有路径:<id,path> <path,id>这样可以实现双向根据key查询,降低复杂度。
第一次作业尽可能降低复杂度,使结构清晰,为后续作业打下良好基础。
第二次:
无向图系统,在第一次的基础上构建无向图结构,进行基于无向图的一些查询操作
PathContainer相关完全沿用上一次作业,无需重构。
因为删除路径会导致图的变更较为复杂(是否要删除某一条边或删除某个节点),并且图变更指令总数少,所以我选择每次增加或删除路径后重新构建图,用二维数组储存邻接矩阵,使用floyd算法,存储多源最短路,这样所有图相关的查询操作复杂度都可以接近O(1),提高效率,避免超时。
第三次:
地铁系统,在前两次的基础上将无向图看做地铁图,进行对最小票价,最少换乘,最小不满意度等的查询操作
本次作业较为困难,需要清晰的架构和高效算法。以最小票价为代表:最小票价和最短路线和换乘次数都有关,就需要想出一种有效的办法解决这个问题,首先可以用拆点法,将不同地铁线路上的同一节点看成不同的站,这样就会导致站数可能非常多,计算负担重。
所以我借鉴了一种比较好的思路:对于最小票价,先使用floyd对每条线路构建完全图,边权为最小距离,对每个权重加2存到整个地铁图的票价矩阵中,最后再对该矩阵进行Floyd,每两个站对应的矩阵中的数字减2即最小票价。其他情况同理可得。
这种算法可以化繁为简,每次处理图变更指令时,针对需要查询的不同情况构建不同的矩阵,查询时从矩阵中读出相应的数据即可。
bug情况
本单元三次作业都没有在强测或互测中被发现bug,我认为这得益于规格化设计和单元测试,逐个击破。本地自测时,找到各个指令对应的方法,根据规格构建测试数据,进行单元测试,即可大大降低出错率,互测也采用同样的方法,没有发现问题。
对规格撰写和理解的心得体会
规格撰写是一套新的体系,用一种全新的方式来看待Java的类和方法。我们能够描述一个方法的预期的功能而不管它如何实现。它有以下好处:
-
能够更为精确地描述这些代码是做什么的
-
能够高效地发现和修正程序中的bug
-
可以在应用程序升级时降低引入bug的机会
-
可以提早发现客户代码对类的错误使用
-
可以提供与应用程序代码完全一致的JML格式的文档
规格十分符合面向对象的思想,显式地规定了类和方法的前置条件和后置条件。对于一个具体问题,应该先思考架构和规格,之后代码只要按照规格写,写出来的一定是有逻辑的程序,同时可以为debug带来很多便利,值得继续深入理解和学习。