面向对象第三单元个人总结

  第三单元也落下了帷幕,这一单元主要学习的是JML规格。说来惭愧,这一单元恰恰是从成绩上看最不理想的一单元。尽管它考查的细节不多,但重在对于规格的概念理解。换言之,我的第一二次作业主要败在了对于题意的理解上,没有考虑cpu时间和没有仔细阅读JML要求导致的致命错误。

 (1)梳理JML语言的理论基础、应用工具链情况

1,理论基础:

JML以javadoc注释的方式来表示规格,每行都以@起头。有两种注释方式,行注释和块注释。
注释结构:

(1)前置条件:requires子句定义该方法的前置条件(precondition)。

(2)副作用范围限定:assignable列出这个方法能够修改的类成员属性,\nothing是个关键词,表示这个方法不对

任何成员属性进行修改,所以是一个pure方法。

(3)  后置条件:  ensures子句定义了后置条件。

原子表达式:

(1)\result表达式:表示一个非 void 类型的方法执行所获得的结果,即方法执行后的返回值。

(2)\old( expr )表达式:用来表示一个表达式 expr 在相应方法执行前的取值。作为一般规则,任何情况下,都应该使用\old把关心的表达式取值整体括起来。

(3)\not_assigned(x,y,...)表达式:括号内是否被赋值。

(4)\not_modified(x,y,...)表达式:括号内元素取值是否有被改变。

扫描二维码关注公众号,回复: 6264166 查看本文章
(5)\type(type)表达式:返回类型type对应的类型(Class)。

量化表达式:

(1)\forall表达式:全称量词修饰的表达式。例:(\forall int i,j; 0 <= i && i < j && j < 10; a[i] < a[j]) 为真,则数列从0-9递增。

(2)\exists表达式:存在量词修饰的表达式。例:(\exists int i; 0 <= i && i < 10; a[i] < 0)  表示针对0<=i<10,至少存在一个a[i]<0。

(3)\sum表达式:返回给定范围内的表达式的和。(\sum int i; 0 <= i && i < 5; i)

(4)\product表达式:返回给定范围内的表达式的连乘结果。 (\product int i; 0 < i && i < 5; i)

(5) \max表达式:返回给定范围内的表达式的最大值。 (\max int i; 0 <= i && i < 5; i)

  (6) \min表达式:返回给定范围内的表达式的最小值。 (\min int i; 0 <= i && i < 5; i)

方法规格:

(1) public normal_behavior 表示接下来的部分对该方法的正常功能给出规格。

(2)public exceptional_behavior 下面所定义的规格是异常功能。

(3)signals子句:简化:signals_only  @ signals_only OverFlowException;

2,工具链应用:

  与规格化设计相关的工具主要有:OpenJML,JUnit,JUnitNG等等。Openjml主要帮助我们检查规格化语言JML的语法以及基本逻辑正确性;Junit以及JunitUG则是对JML规格进行测试,前者主要用于单元测试以及一定的自动化测试,后者则主要测试一些边界情况。

(2)部署JMLUnitNG/JMLUnit,实现自动生成测试用例

  根据zwc dalao在讨论区发的博客,安装JMLUnit。

  

   我尝试仿照写了MyPath的test, 测试equals()方法是否可行,如下:

public class MyPathTest {
  private MyPath path1, path2, path3;

  @Before
  public void before() {
      path1 = new MyPath(1, 2, 2, 4);
      path2 = new MyPath(1, 2, 2, 4);
      path3 = new MyPath(1, 2, 3, 5);
  }
   
  @Test
  public void testEquals() throws Exception {
      Assert.assertTrue(path1.equals(path1));
      Assert.assertTrue(path1.equals(path2));
      Assert.assertFalse(path1.equals(path3));
      Assert.assertTrue(path2.equals(path1));
      Assert.assertTrue(path2.equals(path2));
      Assert.assertFalse(path2.equals(path3));
      Assert.assertFalse(path3.equals(path1));
      Assert.assertFalse(path3.equals(path2));
      Assert.assertTrue(path3.equals(path3));
  }
}
运行结果与预期相符。
(3)按照作业梳理自己的架构设计,并特别分析迭代中对架构的重构

第一次作业:(bug修改后):

 第一次作业的框架比较简单,我相信大家也比较统一,没有太多需要分析的地方,就是依照规格实现各个接口就好。

第二次作业:

第二次主要是利用已有的NodeTable和BFS算法来计算最短路径,没有对BFS方法进行分装,就放在MyGraph类。可以看出就多了一层graph到pathContainer的封装。

这次的迭代也比较简单,不需要重构。

第三次作业:

上面是包括所有的方法的一张结构图,比较琐碎,下面是抽离方法的,仅看构造。

这一次,因为涉及到最少换乘,最少票价和最低不满意度的问题,进行了一定的重构,重点是抽离出Method类,就是一个方法的集合。

我的思路是利用邻接矩阵,然后Dijkstra算权,根据这个思路,最少换乘权为1,其余都要建不同的图,我建的priceMap和unsatMap就是为了计算最少票价和最低不满意度。

从逻辑和扩展性的角度讲,我不认为这两张图是MyRailwaySystem的性质,规格中也没有要求提供接口,提供该图的性质,可视为实现getLeastTicketPrice等等的中间结果。所以我将这两个图都视为Method类中的private变量,在path_add和path_remove 时会进行更新,而获取最低票价时就到Method类中调用图计算。同时也把使用的Dijkstra视为Method的内部方法,method类只向MyRailwaySystem类返回计算结果。

这次是因为我们要实现的接口比较多,功能也比较复杂,所以把count_block, get least ticket price 和get least unsatisfied value都封装在method类中实现。

整体上就是使接口类中除了接口实现函数以外,没有别的方法函数,这样对于用户来讲,不需要暴露的计算过程没有暴露,而接口功能一目了然。

总结:三次作业的封装不是很彻底,第一二次封装意义不大,第三次在考虑扩展性的基础上,method类可以根据需求多装几个,但是也要考虑一次遍历时想要更新所有的权图实现的遍历性。如果由于过度封装,导致需要多次遍历更新,就得不偿失了。实际上,因为有的用了BFS方法,有些用了Dijkstra,很多代码方法还有重用的空间,这次的架构还有很多没有合并彻底的地方。

(5)按照作业分析代码实现的bug和修复情况

第一次作业:

  评测和互测都出在cpu时间超时上。主要有两个问题,一个就是通过arraylist来装path和pathid,导致add_path和remove_path时复杂度极高。

这也就涉及到对HashMap和ArrayList的理解问题:Arraylist本质上是一个动态的数组,还是数组。数组与链表是不同的,一次add(index, E)操作实际上是要加入后让所有后面的数据向后移一位,所以复杂度是O(n),remove相同,数据都要向后依次移动。而Hashmap是直接索引表后接链表的结构,也就是当无hash冲突的时候,直接装入该key对应的地方,复杂度是O(1)。这是产生cpu爆点的第一个问题。

  第二个问题,我认为更加不可饶恕,就是没有在每次更新pathContainer时记录当前图的结点数,甚至也没有给每个path一个private变量存储结点类型数。这是一个非常常见的中间量存储问题,而似乎题目中没有给这个存储要求,也没有暗示要自设个变量pathNodeslist和NodeTable时,就完全忘了。这种每次都强行遍历得出结果的代码编写耗费大量无用计算。还是缺乏对时间复杂度的意识,简言之,学得太少,打的代码,做得工程都太少,完全是菜鸟型的送人头。

  改了以上两点,就全部修复了。

第二次作业:

  如果说第一次是菜鸟型的不可饶恕,第二次就是不看规格的自杀式行为。

  第二次作业爆了一半以上的WA,而究其原因,就是没有看规格。自己当时还疑惑相同点有没有edge的问题,问了同学是有“1 2 2 3”2,2间就有edge,没有这种就没Edge。自己想想,不就是自圈吗?于是很满意了,看了指导书,交了代码。然后再IsConnected,和shortestPath上相同点全爆了,有没有edge和有没有connected是两个概念的,我直接把没edge的当不相连了。重点是:规格中写得非常之明白。

  

 /*@ normal_behavior
      @ requires (\exists Path path; path.isValid() && containsPath(path); path.containsNode(fromNodeId)) &&
      @          (\exists Path path; path.isValid() && containsPath(path); path.containsNode(toNodeId));
      @ assignable \nothing;
      @ ensures (fromNodeId != toNodeId) ==> \result == (\exists int[] npath; npath.length >= 2 && npath[0] == fromNodeId && npath[npath.length - 1] == toNodeId;
      @                     (\forall int i; 0 <= i && (i < npath.length - 1); containsEdge(npath[i], npath[i + 1])));
      @ ensures (fromNodeId == toNodeId) ==> \result == true;
      @ also
      @ exceptional_behavior
      @ signals (NodeIdNotFoundException e) (\forall Path path; containsPath(path); !path.containsNode(fromNodeId)); 
      @ signals (NodeIdNotFoundException e) (\forall Path path; containsPath(path); !path.containsNode(toNodeId));
      @*/
 public boolean isConnected(int fromNodeId, int toNodeId) throws NodeIdNotFoundException;

结果就是,加了fromNodeId == toNodeId一共6行的判断后,所有bug都修复了。

第三次作业:

  第三次作业比较复杂,但是总算没有犯很愚蠢的错误,最终只错了第一个点,是在count_block中,这里的并查集是我自己改的,所以算法上可能有些小问题。

  依照正确算法改过,就可以修复了。

(6)阐述对规格撰写和理解上的心得体会

  心得是一方面享受规格提供的遍历,它会给你一个提纲挈领的要求,并且帮你考虑了可能的输入情况和可能产生的异常,能让你的代码更加的规范适用;另一方面,在有规格的情况下,就要写出完全符合规格的代码,这又是一个比较高的要求。

  同时,基于规格,可以自动生成测试,对自己各个方法进行单元测试,看自己是否符合规格。这种基于评测的写法,是非常有利于进行覆盖性的测试的。换句话说,它定义了一个正确的代码是什么,不仅仅是通过黑盒测试。如果一个规格是完善和正确的,满足真实条件和要求,那一个完全满足这个规格的,不会产生不满足规格的结果的代码就是完全正确的。

  我的个人体会是,尽管在规格单元栽的很惨,但这也间接反映我的规格意识和工程意识欠佳,需要培养。我认为了解JML规格是很有必要的。我们以后做一个工程,就是要完成一个个要求,比如地铁线路查询系统。首先要把这些要求细分,定义为接口,这里课程组已经帮我们做好了,然后一点点完成每个功能实现的要求。拿到JML其实是一个很规整的东西,这样一个工程可以给很多人去做,具体每个人怎么实现不用管,大家都公用这些接口,都知道这些接口输入和输出的规定就可以了。自己也可以写好规格,然后把实现的任务交给别人完成。做测试的人也可以不管具体代码的bug,而是基于规格要求做测试。一个简单的公有规格,就可以把一个项目的各个阶段分散开了,我认为这是个很神奇的事情。

  这个单元过后,我已经可以写出基本的可以被理解的规格文本,虽然可能有点语法小错或啰嗦;而也养成了阅读规格的习惯(实在栽的太惨了)。整体来讲,是颇有收获的一个单元。

  

猜你喜欢

转载自www.cnblogs.com/jura/p/OO_unit3_juracera.html