2019OO第三单元作业总结

OO第三单元的作业主题是JML规格化设计,作业以图及图的最短路径相关计算为载体,体现接口的规格化设计。

-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

一、JML语言的理论基础和应用工具链

JML是一种形式化的、面向JAVA的行为接口规格语言(behavioral interface specification language),它混合了Java的语法成分和JML引入的语法成分,能描述一个类的不变式等限制条件,也能描述方法的前置条件、副作用、后置条件等。利用JML,我们可以做到先思考这个类的规格,之后再去考虑具体实现的问题,这样做到了可扩展性强,并且可以根据规格,对方法直接进行单元化测试,避免了大规模的测试可能带来的覆盖不全面的问题,符合面向对象的思想。

JML有相应的的工具链,来自动识别和分析处理JML语言。OpenJML是专门针对JML语言设计的一个验证工具,它可以检查JML规格语法的正确性,也可以根据方法的JML语言和方法具体实现的代码来初步地验证方法实现的是否符合规格。JUnit是一个对代码进行单元测试的插件,它可以针对某一个方法,利用assert断言语句,实现自动化测试,使用时,可以根据该方法的规格构造测试用例,这样可以帮助我们快速定位到bug的位置,不用看着一大段一大段的程序去调试了。SMT Solver是一个用来静态检查程序的代码实现是否符合JML规格的插件,与JUnit不同的是,它不需要用户提供测试用例,而是执行静态的检查,来进行形式化的验证。

二、利用JMLUnit和JUnit进行单元测试

1、利用JMLUnit进行JML语言的单元测试

JMLUnit是一个针对JML语言的单元测试工具,它能根据我们写的JML语言,提供一些包括边界数据在内的测试用例,来检查JML语言的正确性。在写了一个简单的整数比大小方法compare之后,对其进行JMLUnit的检查,compare方法的代码如下:

对其进行JMLUnit单元检查后,检查结果如下:

检查结果说明compare方法的JML描述通过了所有的测试点。

2、利用JUnit进行针对Graph接口的单元测试

由于Graph接口是从PathContainer接口继承而来,因此只测试Graph之中与PathContainer不同的接口,这些接口的实现也是第二次作业中的重点。针对不同的接口的测试代码如下:

addPath接口:

removePath接口:

removePathById接口:

getShortestPath接口:

在构造测试用例时,应该注意既要考虑规格中的正常情况(normal_behavior),也要考虑异常情况(exceptional_behavior)。同时,也应该加入一些特殊的边界样例,如Path(3,3,3),全部是自环且合法的Path,以验证程序的鲁棒性。

三、我的架构设计

显然,这三次作业的架构是很有逻辑上的继承关系的,因此我首先考虑使用继承,但是在使用的过程中遇到了子类无法访问父类的private属性的问题,而checkstyle又要求类的属性必须是private,当时我没有想到好的解决办法,就放弃了继承。

第一次作业的类图如下:

第二次作业的类图如下:

第三次作业的类图如下:

这三次作业中,对于最短路径的计算的要求是逐次递进的:第一次作业没有要求计算最短路径,第二次作业要求计算最短路径,第三次作业要求计算最短路径、最少换乘、最低票价、最小不满意度。因此对于边的信息要求也是越来越细致的,鉴于CPU时间限制了时间复杂度,这就要求在每次add或removePath的时候要对图的相关信息进行计算,将最短路径等信息直接计算出来并保存,在查询最短路径时直接输出即可,这是一种牺牲空间换取时间的思想。在我的架构中,存在很多HashMap来存储这些边和节点的信息,有的信息甚至以多种方式存了多遍,这就是利用存储冗余的信息来加快程序运行速度的方法。在每次增加新的需求时,都要考虑如何应该保存图的什么信息、如何保存这些信息,以达到简便计算的效果。这是每次修改代码或重构时应该考虑的很重要的一点。

四、代码中的bug

本单元的三次作业在需求上并没有前两个单元复杂,难点主要在于算法,如何以较低的时间复杂度的算法完成对最短路径的计算是考虑的重点。下面将就三次作业来分析我的代码中的bug。

第一次作业:代码中出现了严重的逻辑bug,在ComparePath时并没有严格按照字典序比较,导致在比较两个不同长度(节点序列长度不同)的路径时会出现bug。这个严重的bug在强测和互测中都被找了出来。

第二次作业:最短路径的计算采用基于邻接矩阵的Floyd算法,强测和互测中均未发现bug,也没有出现CPU超时的情况。

第三次作业:最短路径、最少换乘、最低票价、最小不满意度的计算仍然采用Floyd算法,在计算不同类型的最短路径时,生成不同的权值矩阵传入Floyd算法中即可。强测中未发现bug,互测被hack了一组CPU_TLE,是由于我的算法每次add和removePath的时候都要重新计算所有的最短路径,并且未采用缓存的机制,导致计算太慢,CPU超时,可以考虑采用更高效率的算法,或者使用更好的图的结构来减少计算,以避免这个问题。

-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

心得体会

本单元的主题是JML规格化设计,通过本单元的学习,我初步掌握了JML有关的知识。在拿到一个要求的时候,首先应该想到的不是用什么算法去实现,而是应该先想规格怎么写,在很大程度上,规格写的准确与否决定了这个类的设计会不会出现问题,如果规格中就有模糊不清的地方,或者是没有考虑全面的地方,那么照着这个规格实现出的类或方法肯定是有问题的,而且这种错误不好去定位。正所谓“磨刀不误砍柴工”,在实现一个类之前,如果先把规格写好,那么之后的具体代码实现就不是大问题了,或者说,即使代码实现中有问题,也能通过单元测试的方式快速定位并改正问题;但如果没有一个好的规格就去直接实现类的话,那么在实现的过程中就可能会遇到各种模糊不清的地方,甚至出现“拆东墙补西墙”的情况,de掉一个bug的同时又多出好几个新的bug,这样的写代码体验是极差的,而出现这些问题的根本原因,就是在写代码的时候我们其实并不那么清楚这个类要干什么、这个方法有哪些前置条件、会修改哪些值,规格化语言就可以在一定程度上帮助我们避免这个问题。

原文地址:https://www.cnblogs.com/yangjiuchun/p/10888649.html