BUAA_OO 第三单元总结

写在前面

JML(Java Modeling Language)是用于对Java程序进行规格化设计的一种表示语言。一般而言,JML有两种主要的用法:

  • 开展规格化设计。这样交给代码实现人员的将不是可能带有内在模糊性的自然语言描述,而是逻辑严格的规格。
  • 针对已有的代码实现,书写其对应的规格,从而提高代码的可维护性。这在遗留代码的维护方面具有特别重要的意义。

本单元着重训练了对于规格化契约编程的能力,下面对这一单元作业用到的理论知识,实践工具以及感想体会进行总结。

JML语言

一、理论基础

JML(Java Modeling Language)是用于对Java程序进行规格化设计的表示型语言。

从发展源头上看,JML来自Eiffel关于契约设计的理念(Design by Contract)、基于模型的规范化语言(如VDM和Larch方法),并从refinement calculus中添加了一些idea。从这些源头的思想上,我们可以看到JML的理论基础。

对于基于模型的规范语言(Model-Based languages),如Larch一类,起源于Hoare的想法:前后条件描述抽象值

Hoare used pre- and postconditions to describe the semantics of computer programs in his famous article An Axiomatic Basis for Computer Programming

Hoare的前后条件描述实际上与公理系统相关,之后Hoare将这些公理技术应用于抽象数据类型的规范和正确性证明:

  • 类型 -- 数学抽象值
  • 类型对象状态 -- 前后条件

可以看到,Hoare的想法基本上就是JML的雏形,使用数学定义的抽象值编写规范有两个优点:

  • 抽象性:当数据结构发生更改时,不必更改规范
  • 契约性:即契约式描述

基于Hoare的想法,有一系列工作将此进行了进一步推广:

  • Larch Guttag-Horning93:实现了抽象值操作,并缩写接口规范(即前后条件)
  • Z Hayes93 Spivey92:使用元组、集合、关系、函数、列表等构建抽象值

除此之外,考虑到数学符号的复杂性和难可读性,JML还结合了Eiffel语言的优点:

Eiffel is a programming language with built-in specification constructs. It features pre- and postconditions, although it has no direct support for frame axioms.

They can easily read the assertions, which are written in Eiffel's own expression syntax.

即可以以自带语法的形式书写断言

基于上述基础,JML语言诞生:

  • JML使用了Model-Based 规范语言的各种语义概念
    • 使用了抽象值的变体
    • 使用多个模型字段隐式地描述对象的抽象值
  • JML隐藏了Java类外观背后的数学符号
  • JML将用于推理的特定数学逻辑细节分离开

二、应用工具链介绍

  • JMLC:compile JML annotated Java files with runtime assertion checks
    • 作用类似于JAVA编译器,但是增加了自动断言检查功能
    • 即JMLC运行期间进行断言检查,编译器会抛出一个unchecked exception来说明程序违背了某一条规范。
  • JMLUnit:generate file for running JUnit tests on JML annotated Java files
    • 能够根据JML规格和类生成单元测试文件并产生相关数据用例
  • JMLDoc:generate HTML pages from JML and java files
    • 类似于Javadoc的功能,能够自动生成描述类的html文件,同时增加了对JML的解析和生成。

JML形式化验证 -- SMT Solver

SMT(Satisfiability Modulo theories) solver近年来发展迅猛,在形式化方法、程序语言、软件工程以及计算机安全等领域均得到了广泛的应用。本单元的重点是JML规格化设计,其本质是一种形式语言,因此有专门的SMT solver可进行形式化验证,给我们提供了除黑白盒测试、单元测试之外的测试工具。

一、SMT solver部署

openjml使用SMT Solver来对检查程序实现是否满足所设计的规格(specification)。
目前openjml封装了四个主流的solver,可以进行配置。下面记录了openJml的部署过程

  • 运行环境:Java JDK7 或 Java JDK8 (本机不是此版本可以尝试多版本共存)
  • 下载安装:
    • 下载:https://plugins.jetbrains.com/plugin/11072-openjml-esc
    • 安装:IntelliJ IDEA --> Preferences --> Plugins --> Install Plugin from Disk (从官网下载的.zip文件)
    • 配置:
      • 重启IDEA
      • IntelliJ IDEA --> Preferences --> Other Settings --> JML Checker配置 OpenJml解压路径 --> 选择custom solver
      • Attention:路径中不支持空格
    • 使用:Menu Check --> Run Check进行检查 (即可使用部署的SMT Solver)

二、方法验证举例

  • public int hashcode()

    specs-homework-1-opensource\src\main\java\com\oocourse\specs1\MyPath.java:94: 警告: The prover cannot establish an assertion (ArithmeticOperationRange) in method hashCode:  int multiply overflow
            return 31 * codeNodes + codeSet;
                      ^
    specs-homework-1-opensource\src\main\java\com\oocourse\specs1\MyPath.java:94: 警告: The prover cannot establish an assertion (ArithmeticOperationRange) in method hashCode:  underflow in int sum
            return 31 * codeNodes + codeSet;
                                  ^
    specs-homework-1-opensource\src\main\java\com\oocourse\specs1\MyPath.java:94: 警告: The prover cannot establish an assertion (ArithmeticOperationRange) in method hashCode:  overflow in int sum
            return 31 * codeNodes + codeSet;
                                  ^
    specs-homework-1-opensource\src\main\java\com\oocourse\specs1\MyPath.java:94: 警告: The prover cannot establish an assertion (Postcondition: C:\Users\Roger\Desktop\openjml\openjml.jar(specs/java/lang/Object.jml):63: 注: ) in method hashCode
            return 31 * codeNodes + codeSet;
            ^
    C:\Users\Roger\Desktop\openjml\openjml.jar(specs/java/lang/Object.jml):63: 警告: Associated declaration: specs-homework-1-opensource\src\main\java\com\oocourse\specs1\MyPath.java:94: 注: 
        //-RAC@ ensures \result == theHashCode; 
                ^
    

    即存在溢出的可能。

  • public int compareTo(Path o)

    oo document/openjml-0.8.42/openjml.jar(specs/java/util/Collection.jml):71: warning: The prover cannot establish an assertion (Invariant) in method compareTo
        //-RAC@ public invariant content.theSize >= 0;
                       ^
    oo document/openjml-0.8.42/openjml.jar(specs/java/util/Collection.jml):70: warning: The prover cannot establish an assertion (Invariant) in method compareTo
        //-RAC@ public invariant content.owner == this;
                       ^
    
  • public MyPath(int... nodeList)

    specs-homework-1-opensource\src\main\java\com\oocourse\specs1\MyPath.java:76: 警告: The prover cannot establish an assertion (Precondition: specs-homework-1-opensource\src\main\java\com\oocourse\specs1\MyPath.java:46: 注: ) in method compareTo
            if (this.equals(o)) {
                           ^
    specs-homework-1-opensource\src\main\java\com\oocourse\specs1\MyPath.java:46: 警告: Associated declaration: specs-homework-1-opensource\src\main\java\com\oocourse\specs1\MyPath.java:76: 注: 
        public boolean equals(Object obj) {
                       ^
    C:\Users\Roger\Desktop\openjml\openjml.jar(specs/java/lang/Object.jml):77: 警告: Precondition conjunct is false: this == obj
          @     requires this == obj;
                              ^
    C:\Users\Roger\Desktop\openjml\openjml.jar(specs/java/lang/Object.jml):81: 警告: Precondition conjunct is false: \typeof(this) == \type(Object)
          @     requires obj != null && \typeof(this) == \type(Object);
                                                      ^
    C:\Users\Roger\Desktop\openjml\openjml.jar(specs/java/lang/Object.jml):85: 警告: Precondition conjunct is false: obj == null
          @     requires obj == null;
                             ^
    specs-homework-1-opensource\src\main\java\com\oocourse\specs1\MyPath.java:52: 警告: The prover cannot establish an assertion (Postcondition: C:\Users\Roger\Desktop\openjml\openjml.jar(specs/java/lang/Object.jml):78: 注: ) in method equals
                    return false;
                    ^
    C:\Users\Roger\Desktop\openjml\openjml.jar(specs/java/lang/Object.jml):78: 警告: Associated declaration: specs-homework-1-opensource\src\main\java\com\oocourse\specs1\MyPath.java:52: 注: 
          @     ensures \result;
                ^
    specs-homework-1-opensource\src\main\java\com\oocourse\specs1\MyPath.java:51: 警告: The prover cannot establish an assertion (ExceptionalPostcondition: C:\Users\Roger\Desktop\openjml\openjml.jar(specs/java/lang/Object.jml):76: 注: ) in method equals
                if (tmpObj.size() != this.size()) {
                               ^
    C:\Users\Roger\Desktop\openjml\openjml.jar(specs/java/lang/Object.jml):76: 警告: Associated declaration: specs-homework-1-opensource\src\main\java\com\oocourse\specs1\MyPath.java:51: 注: 
          @   public normal_behavior
                     ^
    C:\Users\Roger\Desktop\openjml\openjml.jar(specs/java/util/Collection.jml):71: 警告: The prover cannot establish an assertion (Invariant) in method equals
        //-RAC@ public invariant content.theSize >= 0;
                       ^
    C:\Users\Roger\Desktop\openjml\openjml.jar(specs/java/util/Collection.jml):70: 警告: The prover cannot establish an assertion (Invariant) in method equals
        //-RAC@ public invariant content.owner == this;
                       ^
    C:\Users\Roger\Desktop\openjml\openjml.jar(specs/java/util/Collection.jml):71: 警告: The prover cannot establish an assertion (Invariant) in method equals
        //-RAC@ public invariant content.theSize >= 0;
                       ^
    C:\Users\Roger\Desktop\openjml\openjml.jar(specs/java/util/Collection.jml):70: 警告: The prover cannot establish an assertion (Invariant) in method equals
        //-RAC@ public invariant content.owner == this;
    

    ​ 可以看到在前后置条件,invariant/constraint还不够完善。

JML UnitNG 单元测试

一、JML UnitNG部署

  • 下载 JML UnitNG jar放在openjml目录下

  • 制作脚本 junitng

    #!/bin/bash
    java -jar ~/Desktop/openjml/jmlunitng.jar "$@"
    
  • 生成测试文件

    jmlunitng -cp specs-homework-2-1.2-raw-jar-with-dependencies.jar ···/xx.java
    
  • 编译

    此处可以将上一步生成的测试文件连同源代码在IDEA中一起build

    注意:在IDEA中编译需要添加jar包依赖:jmlunitng.jar, ...

  • 运行xx_JML_Test.java

二、测试举例 -- Graph Interface

[TestNG] Running:
  Command line suite

Failed: racEnabled()
Passed: constructor MyGraph()
Passed: <<MyGraph@6956de9>>.containsEdge(-2147483648, -2147483648)
Passed: <<MyGraph@769c9116>>.containsEdge(0, -2147483648)
Passed: <<MyGraph@6aceb1a5>>.containsEdge(2147483647, -2147483648)
Passed: <<MyGraph@12bc6874>>.containsEdge(-2147483648, 0)
Passed: <<MyGraph@de0a01f>>.containsEdge(0, 0)
Passed: <<MyGraph@4c75cab9>>.containsEdge(2147483647, 0)
Passed: <<MyGraph@1ef7fe8e>>.containsEdge(-2147483648, 2147483647)
Passed: <<MyGraph@6f79caec>>.containsEdge(0, 2147483647)
Passed: <<MyGraph@67117f44>>.containsEdge(2147483647, 2147483647)
Passed: <<MyGraph@5d3411d>>.containsNode(-2147483648)
Passed: <<MyGraph@2471cca7>>.containsNode(0)
Passed: <<MyGraph@5fe5c6f>>.containsNode(2147483647)
Failed: <<MyGraph@1bc6a36e>>.getShortestPathLength(-2147483648, -2147483648)
Failed: <<MyGraph@1ff8b8f>>.getShortestPathLength(0, -2147483648)
Failed: <<MyGraph@387c703b>>.getShortestPathLength(2147483647, -2147483648)
Failed: <<MyGraph@224aed64>>.getShortestPathLength(-2147483648, 0)
Failed: <<MyGraph@c39f790>>.getShortestPathLength(0, 0)
Failed: <<MyGraph@71e7a66b>>.getShortestPathLength(2147483647, 0)
Failed: <<MyGraph@2ac1fdc4>>.getShortestPathLength(-2147483648, 2147483647)
Failed: <<MyGraph@5f150435>>.getShortestPathLength(0, 2147483647)
Failed: <<MyGraph@1c53fd30>>.getShortestPathLength(2147483647, 2147483647)
Failed: <<MyGraph@50cbc42f>>.isConnected(-2147483648, -2147483648)
Failed: <<MyGraph@75412c2f>>.isConnected(0, -2147483648)
Failed: <<MyGraph@282ba1e>>.isConnected(2147483647, -2147483648)
Failed: <<MyGraph@13b6d03>>.isConnected(-2147483648, 0)
Failed: <<MyGraph@f5f2bb7>>.isConnected(0, 0)
Failed: <<MyGraph@73035e27>>.isConnected(2147483647, 0)
Failed: <<MyGraph@64c64813>>.isConnected(-2147483648, 2147483647)
Failed: <<MyGraph@3ecf72fd>>.isConnected(0, 2147483647)
Failed: <<MyGraph@483bf400>>.isConnected(2147483647, 2147483647)

===============================================
Command line suite

Total tests run: 32, Failures: 19, Skips: 0

可以看到,JUnitNG主要是通过规格对数据的约束进行了遍历式生成数据,在MyGraph中测试了所有的方法,并通过对边界值和0遍历进行测试。

但是比较奇怪的是,failed的数据样例实际上在本次作业中是可以通过的,具体failed原因个人猜想有两点:

  • JML规格不符
  • 数据样例有错

除了上述样例之外,可能由于MyGraph功能比较单一,因此并没有生成意想不到的用例,不过还是可以看到自动化生成测试的优越性。

单元作业架构设计

一、第九次作业 -- MyPathContainer

1.1 需求分析与策略选择

第九次作业基本按照规格给出的Path.javaPathContainer.java两个接口实现了MyPath.javaMyPathContainer.java。唯一需要注意的是数据结构的选择。

  • Path.java

    • 序列性 --> ArrayList
    • 查询功能 --> HashSet (配合ArrayList
      • index --> Node
      • size
      • distinct nodes count
  • PathContainer.java:

    • 查询功能
      • index --> Path
      • Path --> index
      • global distinct nodes count
    • 增删功能

    由于上述功能要求和时间复杂度要求,因此使用了双向HashMap来维护path相关信息;同时增加一个HashMap记录distinct nodes的信息。

二、第十次作业 -- MyGraph

2.1 需求分析

第十次作业基于第九次作业的基本功能进行了扩展:

  • 新增了Graph类作为PathContainer的扩展
  • 新增了对边和连通性的查询功能
  • 新增了对最短距离的查询功能

基于Liskov原则,本次作业继承了上一次作业实现的MyPathContainer类,并对新增的功能进行了层次化封装:

利用组合原则,将与图计算有关的功能封装在了GraphInfo类中,该类具有的数据结构如下所示:

public class GraphInfo {
    private final int distinctMax = 255;
    private boolean edgeFlag;
    private int[][] distMap;
    private int[][] edgeMap;
    private HashSet<Integer> indexSet;
    private HashMap<Integer, Integer> nodeIndexMap;
    private HashMap<Integer, Integer> allDistinctNodes;
    
    public GraphInfo(){}
    ······
}

2.2 策略选择

分析新增功能需求,一般来说有如下的思考过程:

  • (1) 计算最短路、边与连通性查询均与图论算法相关 --> 算法计算需求
    • 图存储结构:
      • 邻接矩阵:操作便捷,但省空间
      • 邻接链表:省空间,但操作可能较繁琐
    • 图计算:
      • 多源最短路:Floyd
      • 单源最短路:Dijkstra
  • (2) 时间约束,空间约束以及数据限制条件:
    • 时间20s:较为充裕,但明显不能每次查询都重新算
    • 数据限制条件:任意时刻不超过distinct nodes的数量不超过250

基于上述简单分析,各式算法不难想出,主要是选择适合自己情况的(实现时间成本,测试成本等),本人选择了以下策略:

  • 存储:使用邻接矩阵,并利用静态数组

    • 使用静态数组利用了数据限制条件,但由于输入的Path中nodeId的范围与任意性,因此需要进行id映射转换:

      private int allocIndex(int nodeId) {
          Iterator<Integer> iter = indexSet.iterator();
          if (iter.hasNext()) {
              int index = iter.next();
              nodeIndexMap.put(nodeId, index);
              indexSet.remove(index);
              return index;
          } else {
              return -1;
          }
      }
      
      private int freeIndex(int nodeId) {
      
          int index = nodeIndexMap.getOrDefault(nodeId, -1);
          if (index != -1) {
              nodeIndexMap.remove(nodeId);
              indexSet.add(index);
          }
          return index;
      }
      
  • 计算:使用floyd,同时在每次Path增删时根据情况更新距离矩阵distMap

将图功能相关操作封装后,对于新增的Graph接口,只需要在实现MyGraph类时使用GraphInfo的功能即可。

三、第十一次作业 -- MyRailwaySystem

3.1 需求分析

第十一次作业基于第十次作业进一步进行了扩展:

  • 新增连通块个数查询功能

  • 新增换乘概念,并基于该概念新增三类实际功能模拟:

    • 最少票价查询

      简单来说,在同一条Path中,一条边的票价是1,如果经过了换乘(即从一条Path经过与另一条Path的重合点到达了另一条Path),则换乘票价为2。

    • 最少换乘次数查询

      换乘概念同上。

    • 最少不满意度查询

      \(F(x)=(x\%5+5)\%5\)\(H(x)=4^x\)

      \(E(u,v): U_e(E)=H(max(F(u),F(v)))\)--边不满意度

      \(U_s=32\)--换乘不满意度

同样地,基于Liskov原则,本次要求实现的MyRailwaySystem类继承上一次作业完成的MyGraph类,因此设计原则也与上一次作业相似。

3.2 策略选择

观察上述需求可以发现,除了连通块个数求解以外,其他的功能仍然与图论算法中的最短路径算法相关,只需要考虑换乘与图边权的关系即可:

  • 最少票价:换乘次数计入

  • 最少换乘:显然

  • 最少不满意度:换乘不满意度

  • 策略

    • 同一条路径:不存在换乘,可以计算最短路
      • 特殊处理:同一条路径中不同两点的边权计算:
        • 该路径中最短距离
        • 换乘代价(票价,不满意度)
    • 不同路径:存在换乘,计算任意两点最短距离

    特殊处理后,使用最短路径算法(floyd,dijkstra等)计算,将结果减去换乘代价即可。

3.3 架构分析

本次作业由于某些原因,仅在最后一天白天开始编码,因此在架构上没有太多考虑,回溯时可以看到,部分类的实现可以使用继承,部分方法的安全性和隐蔽性可以使用protected、private加强限制,等等

  • 原始架构

    继承上一次作业进行了扩展,MyPathContainer类中含有:

    • GraphInfo
      • 统计distinct node数量
      • 进行nodeId映射(分配与回收)
      • 计算最短距离、连通性等
    • PriceInfo
      • 计算最少票价
    • UnpleasantInfo
      • 计算最少不满意度
    • TransferInfo
      • 计算最少换乘

    可以看到,上述四个类的功能实际上非常相似,均与图计算有关,且计算最短距离、最少票价、最少不满意度、最少换乘均可直接使用最短路算法解决即可。因此上述代码结构明显是不合理的:

    • 存在功能耦合:
      • GraphInfo将统计distinct Node和Id映射,与图计算相关耦合在一起,应当进一步拆分逻辑。
    • 存在共性功能--使用继承
      • 将共性操作(如floyd算法,数据结构初始化等等)放在父类
      • 将特性操作在子类中重写
      • 合理使用作用域修饰
        • public:add、remove、get
        • protected:父类中定义的子类特性操作
        • private:私有方法
  • 重构

    如图所示,将耦合的逻辑分离,并使用继承抽取类之间的共性:

    • GraphInfo

      • 统计distinct nodes
      • 进行index映射(alloc与free)
    • BasicInfo -- 图计算父类:

      public class BasicInfo{
          ······
          public BasicInfo(){}
          
          public void addPath(···){}
          
          public void removePath(···){}
          
          public void getInfo(···){}
          
          ······
              
          protected void buildInfoArray(){}
          
          ······
              
          private void floydCalculate(int[][] tmpArray, int length){}
      }
      
      • TransferInfo:最少换乘,继承BasicInfo
      • UnpleasantInfo:最少不满意度,继承BasicInfo
      • PriceInfo:最少票价,继承BasicInfo
      • DistanceInfo:最短距离,继承BasicInfo

经过重构后,类之间的功能耦合度降低,且扩展性较强。除此之外,还可以使用工厂模式对图计算层进行包装,尽可能增加扩展性。

测试技术与bug分析

一、黑盒测试

本单元作业在算法上与图计算关系密切,因此在测试时可以充分利用高级语言封装的算法库,如Python中就有networkx等图计算库。使用已有的算法库可以方便地生成数据、计算数据并进行对拍验证。

以第十次作业为例,这一次作业主要需要验证的是最短距离的计算,而networkx中正好有以下方法:

def add_edge(self, u_of_edge, v_of_edge, **attr):
    """Add an edge between u and v.

    The nodes u and v will be automatically added if they are
    not already in the graph.

    Edge attributes can be specified with keywords or by directly
    accessing the edge's attribute dictionary. See examples below.

    Parameters
    ----------
    u, v : nodes
        Nodes can be, for example, strings or numbers.
        Nodes must be hashable (and not None) Python objects.
    attr : keyword arguments, optional
        Edge data (or labels or objects) can be assigned using
        keyword arguments.
def all_pairs_shortest_path(G, cutoff=None):
    """Compute shortest paths between all nodes.

    Parameters
    ----------
    G : NetworkX graph

    cutoff : integer, optional
        Depth at which to stop the search. Only paths of length at most
        `cutoff` are returned.

    Returns
    -------
    lengths : dictionary
        Dictionary, keyed by source and target, of shortest paths.

    Examples
    --------
    >>> G = nx.path_graph(5)
    >>> path = dict(nx.all_pairs_shortest_path(G))
    >>> print(path[0][4])
    [0, 1, 2, 3, 4]

基于上述方法,显然我们可以在Python中实现一个简单的Graph类,并设置好输出,再利用文本对拍即可测试本次作业的功能。

二、bug分析

本单元作业基于规格进行功能实现,整体难度较上一单元下降了一些,但是仍然有一些细节的地方需要注意。以本人这单元作业为例,虽然后两次都拿到了满分,但是第一次作业出现了一个比较细节的bug:

@Override
    public int compareTo(Path o) {
        if (this.equals(o)) {
            return 0;
        }
        int minSize = Math.min(this.size(), o.size());
        for (int i = 0; i < minSize; i++) {
            if (this.getNode(i) < o.getNode(i)) {
                return this.getNode(i) - o.getNode(i);
            } else if (this.getNode(i) > o.getNode(i)) {
                return this.getNode(i) - o.getNode(i);
            }
        }
        return this.size() - o.size();
    }

可以看到,在比较node时错误地使用了this.getNode(i)-o.getNode(i)作为返回值。实际上,由于nodeId的范围,这样的做法会导致计算溢出而产生错误答案。

之所以不直接使用Integer.compareTo(i,j)来作为返回值,是因为在实现过程中参照了String类的compareTo实现(毕竟Path与String都是序列值,且元素可重复)。但没有注意到的是字符值的范围不会导致溢出。

The devil was in the details,以后无论是从事科研还是走向工程岗位,都应该时刻保持对细节的审视态度。

心得体会

一、规格理解

本单元始终围绕规格编程展开,无论是从初次实验的规格撰写与理解,还是三次作业的规格契约编程实现,都从各方面锻炼我们规格编程的能力。于我而言,在以下方面本单元作业让我有较大的收获:

  • 规格 -- 契约编程

    首先是“契约”思维。接触一个新的概念与工具,很常见的思维就是,这个东西的好处是什么?很显然,规格相当于一种契约。类规格定义了开发者之间的契约,方法规格定义了开发者与用户之间的契约。我认为契约本质上仍然体现了面向对象的抽象思维,即是在功能上,类封装了特定的功能,显示出调用接口;在开发上,规格封装了具体的代码实现,显示出功能逻辑,其本质上均是抽象,不过是抽象的目的不同。规格--契约式编程更加利于开发的协作,我认为这是本单元让我体会较深的一点。

    当然,oo课程都是个人作业,我认为要进一步体会规格的契约式编程,还是要进一步在大型软件项目中进行实践以加深印象。

  • 规格设计思维

    关于规格设计方面,以下几个方面是我印象较为深刻的:

    • 抽象性(如只需给出所管理数据的最简表达)
    • 有效性/完备性(如类的invariant和constraint,方法完备条件)
    • *层次化规格

    具体来说,首先对于抽象性,显然无论是根据规格实现代码还是根据需求撰写规格,抽象化规格都是我们始终在练习的,进一步加强对抽象的认识。

    除此之外,更加有挑战性的是完备性的保证,以实验课上的规格举例,要实现集合交换与对称差功能,不仅仅需要通过数理逻辑中的集合运算对行为进行描述,还需要考虑到集合的所有状态与行为影响。考虑范围和行为状态演变始终不是一件容易的事情,这一点在规格设计中也体现出来了,值得注意。

    还需要进一步体会的是层次化规格描述与设计,这一点其实已经在第十一次作业中有所体现。随着代码的不断扩展,PathContainer-->Graph-->RailwaySystem,规格也在不断的扩展层次,并复用前面实现的方法规格。不过我们在实践上没有在这方面做较多的训练,需要在日后进一步加强对层次化规格设计的理解。

  • 规格模式 -- 组合机制

    这一点主要是从规格的复杂性考虑的。在第十一次作业中可以看到,RailwaySystem接口的各个方法在实现上已经较为复杂,而要用JML语言去合理完备地描述行为显然是一件非常困难的事情,即使实现了也会导致规格的可读性非常差,从而失去了其作为编程契约的意义。

    可以看到,课程组提供的规格引入了组合机制(逐层构造数据结构并进行组合)。具体来说,可以概括为以下几点:

    • 共性中间数据抽象:多个方法规格需要相同的中间数据完成逻辑
    • 共性数据限制:多个方法规格需要对相同数据内容进行限制

    显然,和代码实现一样,具有共性的地方可以提取,增加这一单元并进行复用。

    规约这种设计思维在很多地方使用,递归设计、函数式编程、约束求解模型设计等。
    其核心是首先提出并规定预期结果所满足的约束,然后才会有过程式算法实现。

  • 从规格到代码实现

    最后是实现部分,在最初接触规格即第一次作业时,会有一个非常傻的想法,规格定义的数据结构是不是就是我们必须使用的。显然,规格是契约,而不是说明。灵活地根据算法逻辑、语言特性、功能库等进行功能实现是规格所允许并支持的,只要能符合契约即可。

二、规格撰写

至于规格的撰写方面,由于实践的次数并不多,只能简单地谈一谈仅有的一点收获。JML的语法包含了诸如谓词逻辑等基本的数理逻辑运算,而数据结构的抽象就是按照特定次序管理的元素集合,因此规格撰写离不开数理逻辑的知识。

此外,需要进一步加强的仍然是更多层次的规格实践,前述提到的层次化设计在本单元中基本没有实践,需要日后进一步加深体会。

技术总结

一、JML Reference

Java Modeling Language

二、JUnit

JUnit

三、OpenJml

OpenJML

四、Maven项目管理

推荐同学的博客:用Maven+IDEA+Eclipse组合获得最好的OpenJML体验

posted @ 2019-05-22 15:36  Roger24  阅读(194)  评论(0编辑  收藏  举报