标签:队列 架构设计 大量 结果 bug 数据约束 说明 load 动态
类是一个编程单位,类规格定义了类实现者和类使用者之间的契约:类实现者信任使用者能够确保所有前置条件都能被满足,类使用者信任设计者能够有效管理相应的数据和访问安全。通过这样规格契约的连接,系统的较低层次为系统的较高层次提供了服务接口,从而协同系统不同层次的工作。
规格化设计的核心是方法的前置条件,后置条件以及对象的不变式。
高层类型必须对低层类型具有数据和行为上的概括性,凡是使用高层类型引用的对象都可以替换成低层类型所构建的对象,且保证程序行为不会变化,即满足Liskov替换原则,其具体内容是:所有引用基类的地方必须能透明地使用其派生类的对象。由此可以分析出以下关于规格类型层次应该满足的三个方面的更具体的关系:
对象的不变式关系:
I_Sub(c) ==> I_Super(c)
I_Super(c) =!=> I_Sub(c)
重写方法的前置条件:
Requires(Super::f) ==> Requires(sub::f)
Requires(Sub::f) =!=> Requires(Super::f)
重写方法的后置条件:
Ensures(Sub::f) ==> Ensures(Super::f)
Ensures(Super::f) =!=> Ensures(Sub::f)
JML(Java Model Language)就是一种对数据、方法和类进行规格化工具,只不过其针对的对象是Java语言。JML的设计考虑到了将来扩展的需求,因此分为了多个层次,其中level 0是最为核心的,下面将其核心部分关键词作总结:
关键字 | 含义 | 说明 |
---|---|---|
requires | 方法前置条件 | 方法成功调用的前提 |
assignable | 方法约束条件 | 方法执行过程中给可能影响的对象 |
ensures | 方法后置条件 | 在前置条件满足的请款下结果满足的条件 |
normal_behavior | 正常行为 | 正常行为描述 |
exceptional_behavior | 异常行为 | 异常行为描述 |
signals | 抛出异常 | 异常行为描述中抛出异常 |
invariant | 数据不变式 | 数据在所有可见状态下需满足条件 |
constraint | 数据约束式 | 数据一次变化前后需满足条件 |
\result | 方法执行结果 | 方法本次执行return的值 |
\old(expr) | 方法执行前状态 | 标识方法执行前expr的值 |
\forall | 全称量词 | / |
\exists | 存在量词 | / |
\max | 最大值 | / |
\min | 最小值 | / |
OpenJML是一套以OpenJDK构建的依靠JML对Java程序进行规格化和验证的工具,在Java 1.8环境下运行。它将JML规格翻译成SMT-LIB格式并依靠SMT Solver组件进行分析工作。JML的完整工具链可以从JML官网资源下载。此外,我们还可以利用JMLUnitNG/JMLUnit针对类自动生成测试样例并进行测试。接下来的部分将进一步介绍两个工具。
所有用于Java compiler的选项都可以用于openjml.jar的运行,此外openjml的功能执行选项有以下几类:
将从上述JML官网资源下载的OpenJML压缩包解压到待测试代码的目录后,形成如下的文件结构:
在该目录下,命令行输入以下命令运行:
java -jar OpenJML/openjml.jar -check Group.java Person.java MyGroup.java MyPerson.java
结果提示如下:
说明不支持ArrayList,于是,利用OpenJML官网documentations中的样例测试文件A.java进行测试,测试文件和结果分别如下:
// 修改前的A.java文件
public class A {
//@ ensures \result == true;
public void m() {
return true;
}
}
可以看到,openjml提示我们对于返回值为void的函数,在其规格中不能用关键字\result
,依据提示,修改A.java文件如下,并再次运行:
// 修改后的A.java文件
public class A {
//@ ensures \result == true;
public boolean m() {
return true;
}
}
命令行没有提示,说明静态检查通过。
本单元作业迭代实现的是一个社交网络系统,主要由Perosn,Group和NetWork三个类组成
第一次作业中,由于没有性能上的要求,功能较为简单,类之间的功能相对独立,对算法和数据结构要求并不高。较为复杂的一个方法是NetWork中的isCircle方法,查看两个Peron是否处于一个联通分量中。在作业中,采用的是基于队列的WFS,从第一个人作为起点进行广度优先搜索,每搜索到一个Person,则进行判断,若是目标,则返回true;否则,若搜索结束还未遇到目标,则搜索失败,返回false。
第二次作业,有了性能上的要求,需要对数据结构和算法进行优化。首先我们注意到,NetWork中大量的方法是以Person或者Group的id作为参数的,对相关数据的查询或者操作都需要通过其id进行,在这种情况下,如果在NetWork中采用List或者Set这样的容器存储Person和Group,则需要遍历所有对象来寻找某个对应id的对象,效率较低。考虑到Person和Group的id在系统中具有唯一值,因此很自然地想到利用id作为key将相应对象存储到HashMap中。其次,可以注意到Group中多了一些复杂的查询方法如queryRelationSum,queryValueSum,queryConflictSum等,这些方法需要综合组内所有Person的信息,且涉及一定的计算量。为了提高程序的运行速度,采用冗余数据的方式,在每次Group进行addPerson和addRelation时对相关冗余数据改变,从而避免了反复重复相同的计算流程。而在NetWork内,对isCircle方法采取并查集的策略,这实际上也是数据冗余,以空间换效率。
第三次作业更加强调算法,这主要体现在NetWork中的queryMinPath、queryStrongLinked和queryBlockSum三个方法上。queryMinPath涉及到最短路径问题,采用经典的Dijkstra算法来解决;queryStrongLinked求的是两个点之间是否存在两条除起点和终点外不相交的路径,可以转化为直接连接情况需要特判的双连通分量的问题,可以采用Tajan算法,而在作业中,本人将问题转化为了判断是否存在同时包含目标两个顶点的基本环,具体利用栈采用DFS从第一个顶点出发搜索环,若搜索到第二个顶点则进行标记;queryBlockSum结合第二次作业中已经使用的并查集,采用冗余数据的方式解决:每一次addPerson的时候blockSum自增,而addRelation时若并查集合并,则blocksum自减。
这个单元的作业情况可以用惨烈二字形容。由于习惯了前两个单元对中测服务的依赖,而本单元作业中测比较弱,导致前两次作业没有进入互测。第三次作业自己有所警醒,进行了细致的单元测试,终于没有出现bug,但又由于性能原因在强测也表现得不尽人意。
第一次作业是在isCircle中没有考虑到id相同的情况。
第二次作业是在queryAgeVar中采用了简化地算法,然而没有考虑到整数运算中的归约问题,弄巧成拙。另外没有考虑到addRelation对Group的queryRelationSum和queryValueSum的影响。
第三次作业Dijkstra算法中没有结合优先队列,导致效率不够高。
对上述Bug已进行修复。
对规格的学习让我更加深入地体会到了本学期无论是在OO还是在OS课程上都反复提到的层次化和模块化的设计思想。规格作为服务和使用的契约,实际上成为了不同层次和模块之间的接口,这样子,它起到协调整个系统的各个部分共同高效地工作的重要作用。它将具体模块或者层次的设计和实现实现了有效的分离,这一方面提升了系统实现的灵活性:只要保证实现了规格的要求,不同的实现都可以无缝隙地对接到系统中,同时还为程序的测试提供了指导。
说到测试,这个单元着实好好给自己提了个醒。这个单元前两次的bug其实都不是特别隐蔽的,如果进行了认真的测试是不难发现的。其实自己一直以来对测试的都不够重视,很多时候,看到了程序运行实现了预期的效果,心态上就觉得工作已经完成了,然而其实往往还有很多设计逻辑上的缺陷。很多时候,这样的缺陷依靠通过平台的测试服务找出来,这个单元,在测试不严的情况下很自然就翻车了。然而,随着自己以后慢慢走向工作和研究,测试将必然越来越依靠自己,而因自己测试的态度和能力不足而造成的代价也必然远不止会是在某次作业的分数不太高这样简单的事情上。通过这个单元的教训,希望自己有所警醒。
标签:队列 架构设计 大量 结果 bug 数据约束 说明 load 动态
原文地址:https://www.cnblogs.com/eleony/p/12943478.html