• 面向对象第三单元总结


    一、JML语言

    1.理论基础

    1.规格化设计

    类是一个编程单位,类规格定义了类实现者和类使用者之间的契约:类实现者信任使用者能够确保所有前置条件都能被满足,类使用者信任设计者能够有效管理相应的数据和访问安全。通过这样规格契约的连接,系统的较低层次为系统的较高层次提供了服务接口,从而协同系统不同层次的工作。

    规格化设计的核心是方法的前置条件,后置条件以及对象的不变式。

    类规格的组成
    • 数据规格:类所管理的数据内容,及其有效性条件(invariant, constraint)
    • 方法规格:类所提供的操作,前置条件(权力)、后置条件(义务)和副作用(注意事项)
    规格的类型层次

    高层类型必须对低层类型具有数据和行为上的概括性,凡是使用高层类型引用的对象都可以替换成低层类型所构建的对象,且保证程序行为不会变化,即满足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)

    2.基于JML的规格模式

    JML(Java Model Language)就是一种对数据、方法和类进行规格化工具,只不过其针对的对象是Java语言。JML的设计考虑到了将来扩展的需求,因此分为了多个层次,其中level 0是最为核心的,下面将其核心部分关键词作总结:

    关键字 含义 说明
    requires 方法前置条件 方法成功调用的前提
    assignable 方法约束条件 方法执行过程中给可能影响的对象
    ensures 方法后置条件 在前置条件满足的请款下结果满足的条件
    normal_behavior 正常行为 正常行为描述
    exceptional_behavior 异常行为 异常行为描述
    signals 抛出异常 异常行为描述中抛出异常
    invariant 数据不变式 数据在所有可见状态下需满足条件
    constraint 数据约束式 数据一次变化前后需满足条件
    esult 方法执行结果 方法本次执行return的值
    old(expr) 方法执行前状态 标识方法执行前expr的值
    forall 全称量词 /
    exists 存在量词 /
    max 最大值 /
    min 最小值 /

    3.JML工具链

    OpenJML是一套以OpenJDK构建的依靠JML对Java程序进行规格化和验证的工具,在Java 1.8环境下运行。它将JML规格翻译成SMT-LIB格式并依靠SMT Solver组件进行分析工作。JML的完整工具链可以从JML官网资源下载。此外,我们还可以利用JMLUnitNG/JMLUnit针对类自动生成测试样例并进行测试。接下来的部分将进一步介绍两个工具。

    二、部署SMT Solver并进行验证

    所有用于Java compiler的选项都可以用于openjml.jar的运行,此外openjml的功能执行选项有以下几类:

    • -check:缺省值,运行JML的解析和类型检查
    • -esc:进行静态检查
    • -rac:结合运行时断言进行编译(动态检查)
    • -doc:运行jmldoc工具(还没有实现)

    将从上述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 
    esult == true;
      public void m() {
      	return true;
      }
    }
    

    可以看到,openjml提示我们对于返回值为void的函数,在其规格中不能用关键字 esult,依据提示,修改A.java文件如下,并再次运行:

    // 修改后的A.java文件
    public class A {
    
      //@ ensures 
    esult == true;
      public boolean m() {
      	return true;
      }
    }
    

    命令行没有提示,说明静态检查通过。

    三、部署JMLUnitNG/JMLUnit并测试MyGroup类

    为了进行测试,对MyGroup和MyPerson类进行了微小的修改,包括添加包名方便编译,修改引用类型List和Map等为ArrayList和HashMap从而避免使用JMUNitNG时因类型不匹配报错,其他内容均与task3保持相同。
    进行测试的目录结构如下:

    其中src目录目的是保存源文件备份,方便操作失误时回滚,实际测试中并未使用到。
    接下来,依次运行以下的指令:

    其中第一条指令是运行jmlunitng.jar针对被测试类生成测试用例及测试类,第二条指令是将所有源文件编译成类文件,经过这两个步骤以后,测试目录如下:


    可以看到,目录结构中多出了针对MyGroup和MyPerson的两个测试用例文件夹以及测试类。
    最后通过第三条指令运行MyGroup的测试类MyGroup_JML_Test,结果如下:

    可以看到,失败的都是添加或移除的Person是null的情况。而对于本次作业的整个系统来说,这样的情况都是不存在的。不过如果仅仅是从类的角度来看这个问题,其实说明了Group的类规格有一定的不合理性。

    四、作业架构设计

    本单元作业迭代实现的是一个社交网络系统,主要由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和修复情况

    这个单元的作业情况可以用惨烈二字形容。由于习惯了前两个单元对中测服务的依赖,而本单元作业中测比较弱,导致前两次作业没有进入互测。第三次作业自己有所警醒,进行了细致的单元测试,终于没有出现bug,但又由于性能原因在强测也表现得不尽人意。

    第一次作业是在isCircle中没有考虑到id相同的情况。

    第二次作业是在queryAgeVar中采用了简化地算法,然而没有考虑到整数运算中的归约问题,弄巧成拙。另外没有考虑到addRelation对Group的queryRelationSum和queryValueSum的影响。

    第三次作业Dijkstra算法中没有结合优先队列,导致效率不够高。

    对上述Bug已进行修复。

    六、心得体会

    对规格的学习让我更加深入地体会到了本学期无论是在OO还是在OS课程上都反复提到的层次化和模块化的设计思想。规格作为服务和使用的契约,实际上成为了不同层次和模块之间的接口,这样子,它起到协调整个系统的各个部分共同高效地工作的重要作用。它将具体模块或者层次的设计和实现实现了有效的分离,这一方面提升了系统实现的灵活性:只要保证实现了规格的要求,不同的实现都可以无缝隙地对接到系统中,同时还为程序的测试提供了指导。

    说到测试,这个单元着实好好给自己提了个醒。这个单元前两次的bug其实都不是特别隐蔽的,如果进行了认真的测试是不难发现的。其实自己一直以来对测试的都不够重视,很多时候,看到了程序运行实现了预期的效果,心态上就觉得工作已经完成了,然而其实往往还有很多设计逻辑上的缺陷。很多时候,这样的缺陷依靠通过平台的测试服务找出来,这个单元,在测试不严的情况下很自然就翻车了。然而,随着自己以后慢慢走向工作和研究,测试将必然越来越依靠自己,而因自己测试的态度和能力不足而造成的代价也必然远不止会是在某次作业的分数不太高这样简单的事情上。通过这个单元的教训,希望自己有所警醒。

  • 相关阅读:
    win7下的vxworks总结
    ubuntu 无法获得锁 /var/lib/dpkg/lock
    项目中用到了的一些批处理文件
    win7下安装 WINDRIVER.TORNADO.V2.2.FOR.ARM
    使用opencv统计视频库的总时长
    January 05th, 2018 Week 01st Friday
    January 04th, 2018 Week 01st Thursday
    January 03rd, 2018 Week 01st Wednesday
    January 02nd, 2018 Week 01st Tuesday
    January 01st, 2018 Week 01st Monday
  • 原文地址:https://www.cnblogs.com/eleony/p/12943478.html
Copyright © 2020-2023  润新知