OO的奇妙冒险3

Posted shhh2000

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了OO的奇妙冒险3相关的知识,希望对你有一定的参考价值。

OO的奇妙冒险3——JML与设计规范

目录

  • JML理论与工具
  • 验证方法与报告
  • 自动样例及其生成与评判
  • 架构设计与重构
  • bug
  • 心得体会

JML理论与工具

  • JML是一种通过形式验证,在实现代码之前就保证所写代码正确性的有效手段
  • 总的来说,JML实现了这样一种功能,即设计与实现分离。构造JML的与写代码的应该是两路人马,构造JML的人不需要知道实现细节,也不关心性能(只关心正确性),写代码的人只关心如果在性能最高的情况下完成JML所标注的任务
  • 而JML另外一个功能,便是有效的管理设计。虽说模块化开发也适用于直接写代码,但是JML毕竟比代码本身要精简一些。很多时候,只阅读JML便可知晓工程的大体功效
  • JML语法属于硬编码的具体细节,知道思想即可,不再赘述
  • 工具方面,本人体验了OpenJML的命令行版本(IDEA2019并不支持JML插件),感觉总体来说效果一般

验证方法与报告

这一部分已经在最新版的作业要求中被取消,因此不再赘述

自动样例及其生成与评判

  • 首先是传统艺能:对拍器

    即使在JML的大环境中,对拍器仍然相当好用。本次作业的评测系统可以说是十分好写,打好jar包后直接用shell编写若干重定向,并用diff命令即可快速高效的实现对比,困难的部分是数据生成器

    数据生成器的好坏,直接决定了bug的覆盖性,也直接决定了性能(运行时间)的分析。制造一个好的数据生成器的过程,本身也是测试驱动开发的一个过程,这代表着某种策略随机出来的所有样例,所编写的程序都能有效应对。

    总的来说,对拍器的强大之处在于自动化和随机性。不同于Junit手动编写样例,对拍器一次完成终身受益。而随机性的引入可以在概率意义上将分支覆盖率提升到100%-x(x是一个无穷小量)

  • 其次是新晋帮手:Junit

    Junit与对拍器的出发点并不同,并不是比对整个工程的输入与输出,黑盒测试,而是对每一个方法进行测试

    使用Junit的一个最好的利益,就是可以通过少数几组样例迅速的检查分支覆盖率。分支覆盖率边界条件一起,是单线程程序正确性bug的最大出处。分支覆盖率搞定之后,再搞定边界数据,即可大致说明程序没有正确性bug

  • 最后是课程要求使用的JML相关的测试

    OpenJML工具体验很差,因为,他对于JML的语法十分死板。一个最明显的例子就是,JML中利用数组来表示的数据,实现的时候也必须使用静态数组才能通过相关测试

    好在,课程组及时调整了任务,改为针对某个方法进行体验与测试。

    为了完成课程目标,我做出了代码如下:

    //@ ensures \result == a * b;
    public static int mult(int a,int b) {
    return a * b;
    }

    这个代码被JML工具检测出了溢出int的错误

    总体来说,我认为JML及其Solver的主要任务是,解决上文提到的另一个问题:边界条件。

架构设计与重构

  • 终于结束了JML相关测试的编写。接下来谈一谈架构设计与作业代间的重构

  • 首先,是容器VS数组的问题。从第二次开始,为了复杂度不爆表,题目要求任意时刻存在于容器中的总点数均有一个上限。考虑到静态数组封装方便,使用方便,还计算迅速,我利用操作系统内存管理的观点对nodeId进行了离散化,具体做法如下:

    • 首先,设置一个boolean数组来代表物理点(总数为题目规定的那个上限),其值表示是否被占用
    • 一个HashMap作为页表
    • ADDPATH的时候,对于每一个点(虚拟点),首先在页表中查看其是否已被映射至一个物理点,没有的话,从数组中使用nextfit策略找到一个空闲的物理点,标注为已使用,并将这一对关系add进Hashmap中
    • 除了boolean数组外,还有一个int数组用来储存出现次数,类似于pp_ref
    • 删除的时候,pp_ref--,如果减到了0,就从Hashmap中移除,并设置为空闲

    这样一套机制,类似对拍器,一经建立,无需重构。预见到了可能的改变上限,使用参数化的思想,将各种上限等等写为参数,方便需求更改

  • 第一次作业

    • 第一次作业比较普通。两个Hashmap存id<->Path即可,同时额外一个出现次数相关的Cnt数组方便告诉查询(毕竟一共300000个点,查询必须O(1))
  • 第二次作业

    • 第一次作业有关Container的部分没有重构,直接增加离散化(内存管理思想)机制。在这一步过后,可以对容器中的所有点形成一个maxn*maxn的距离矩阵(int[][])(第二次中maxn为250),利用边集的信息初始化后,每次ADD或REMOVE重新计算即可。考虑到250这个数比较小,我直接采用了好写没bug的Floyd算法一次性解决所有计算
    • 在增加了常数优化与剪枝后,经过大量测试+时间统计,在总运行时间为1.3秒左右的样例中,计算部分约占0.1秒。强测中,受于服务器限制,这两个数据约为3.7/0.4。可以看到,计算部分根本不是性能问题所在。在本地复现强测时,本地的数据约为1.2/0.1,可以看到,强测数据在性能方面与本地随机的大量(10000+组)没有显著区别。经过分析,这大概是因为服务器的加密与并发导致的问题
  • 第三次作业

    • 这次作业有关第一次和第二次的仍然没有重构,全部复用。
    • 利用讨论区gyf大佬和wjy大佬分别独立提出的分层算法,利用Floyd解决了换乘相关的所有问题,本地测试时间平均2.0s,最长3.0。服务器端时间也很充裕(远不到35)。在此在此感谢两位大佬以及其他提出我看得懂或者看不懂的高级算法的大佬们。没有这些算法的帮助,我很难有自信面对35秒的极限

总体来说,这几次作业,我没有经历重构。不过,仍存在设计上的缺陷。为了省事,我没有采用extend来构造新类,而是直接复制粘贴源代码本身。这样做的好处是,可以直接访问“子类”的东西(checkstyle禁止protect变量,这很迷),而不用构造大量的getxxx方法。坏处显而易见,就是造就了一个400行左右的SubwaySystem类。事实上,如果做一些精简与风格优化,大概可以做到300+行,不过时间有限,能力不足,没有做工程性的优化。

bug(互测)

  • 与其讨论bug,不如讨论互测心得,原因有二:

    • 这几次作业难度并不大,至少远低于多线程电梯和WF求导,因此A组全部是100分强测的人,不存在强测bug
    • 即使互测没有bug,也有一些值得探讨的地方
  • 第一次作业

    • 不管怎样,第一次作业还是很无聊(互测方面)。这一次作业我认为主要在于理解,应用,体验JML与设计实现分离编程、契约式编程等等。防止出现bug也比较简单,根本不需要JML相关边界测试或者Junit覆盖性测试,直接对拍1000组就OK了
    • 互测中,同屋人的数据结构与实现类似,没有发现bug
  • 第二次作业

    • 第二次作业只要正常使用入门级,裸的图算法进行运用,就不会出现bug。没有做离散化处理的话,用hash套Hash直接计算也无所谓。可能是考虑到Java的Hash最差情况会被卡成红黑树,屋里有人甚至使用的不是Hashmap而是Treemap。虽然对拍的时候比我的程序慢了3倍左右,但仍然很安全
    • 互测中,同屋人面对离散化这一点态度不同。有的为了工程型牺牲了常数性能与代码实现的方便,有人(比如我)为了方便的实现选择了离散化,这无可厚非。没有发现bug
  • 第三次作业

    • 这次作业就有一些难度了。主要矛盾还是在换乘相关的计算上

    • 课后看来,标程采用的是没有什么优化(除了一个类似cache的缓存)的dij,屋里有拆点dij,有标程dij,也有分层Floyd。

    • 之所以采用离散化+Floyd,是因为者可以在扩展性并不差的前提下,很简洁的完成任务。算上各种空行和定义,以及为了checkstyle一行80字符做出的换行,我总代码量也不到500行,实际工作量很少。有些dalao使用拆点+cache+dij获得了很高的效率,在题目限定数据规模下,大概比我平均快一倍不到。组内有人因为神秘原因,比我的裸Floyd暴力算法还要慢3倍

    • 这次互测我抓到了很奇葩的bug。在某一种策略的生成器制造的符合要求的样例条件下,有一个同学的代码出bug率为100%(拍一组爆一组,连爆十几组后我把它沉默了),可能是强测对于边界条件卡的不够严格。问题出在静态数组的边界,想来这也是一个离散化的同仁,不过可能是细节考虑不周,出现了bug

    • 除了bug,我还遇到了一位神人的代码。我现在也不知道这个神人究竟是真神还是反话。他的神举动如下:

      • 一个400行左右的小工程,他总共写了1700+行
      • 包结构嵌套7层(文件树深度为7),总共十几个包,29个.java文件。结构,抽象类,类应有尽有
      • 封装了dij算法和Floyd算法(把算法抽象成对象这个就很神秘了)
      • 没有检测出bug

      不管怎么看,他都有过度封装的嫌疑,甚至不是嫌疑。毕竟一个包里只放一个文件和另一个包,这样的举动很奇葩。

      不过,这么复杂的工程能在1周内搞到没bug,也是很强。我接手一个1700+的工程很难在1-2天内完善并测试。

      最后,他源代码在statistic检查后,总共有1706行代码。1706这个数具有很特别的意义,所以我甚至怀疑过这位dalao是故意的。

      不过,不管这位同学是否有意如此,我都不太能理解如此复杂的包结构。他能做的大概有:算更多的指标(比如满意度,票价,换乘这些都算指标),运用更多的算法\策略(比如dij,Floyd,spfa。。。),不过在我的架构下,想要完成这些也不困难,甚至代码量还会少些。可读性的话,反正我读了半天是不知道他怎么回事儿,可能也有先入为主的因素吧。总之,我的代码风格不好,几乎没有封装类。他的代码风格在这一方面做的确实比我好,不过一个小工程需要如此大动干戈吗?这一点我保留怀疑态度。

心得与体会

  • 有关互测和架构相关的体会前文已经论述,不再赘述,这里谈一谈我对JML的理解与看法,并结合实际课程环境谈一谈我的感想

  • 首先,JML作为一种相对严格的规范(相比于javadoc或者设计文档),他能做出的正确性判断要精确的多。正如指导书所说,只要你确保满足了JML的需求,那么你的程序一定是正确的。在真实开发中,我推测JML类似规范的编写要比开发困难得多(从课程组助教组精英云集但仍反复修改JML可见一斑)。想要写出完美的JML规范可以说是很困难的。

    在JML的加持下,如果仅以完成作业为目标,那么本单元毫无疑问就是数据结构巩固与提高+图算法初探。事实上,在作业过程中实际编写代码的时候,我主要思索的也并不是JML或者什么规范,工程,而是算法与数据结构。毕竟理想不能当面包吃。规范没搞透可以日后再来,三次无效作业的风险还是挺大的。

    然而,正如今年的改革一样,去年的JSF据说是自己写规格,互测抓规格,因此把规范设计的课程变成了语文基本语法与细节博弈论社会关系等等,今年电梯改革很有成效,因此JML单元不应堕落成数据结构补课。因此,在课程空闲时间,穿插着还是要看一些规范设计与架构的知识。从我自身来看,就java这门语言来说,面对1000行左右的工程已经不是十分头疼,相比于学期初有了一定的提升。然而,架构设计也一直是我的弱点。每次都提交default package下面三个孤零零的类,我都有点对不起同屋的大哥们。

    但是,具体问题具体分析。这几周的OO在客观上没有了开学时的霸气(因为神奇的OS更加神奇了),因此投入时间基本上也只剩1-2天。面对短暂的时间与较小的代码量(100-200-400),真的没有经历,甚至没有必要去苦心孤诣钻研架构与设计。有些同学提出过将架构与设计作为评判标准以鼓励钻研这一部分内容,对于他的目的与动机,我完全支持。但他说的方法我认为一不一定有效二不一定实际。

    从被赋予学分的那一刻起,面向对象设计与构造这九个字就和成绩,分数死死地绑在了一起,也就和利益绑在了一起。谁都不是神,面对与利益绑定的东西,真的很难理想主义的去实践,体验。事实上,今年OO改革之所以成功,我认为很大一部分不在于课程目标或者讲授形式(这些甚至都没有变),而在于助教给出了一种新的思路,将利益引向了相对正确的方向。我认为,面对与利益绑定的作业,真的很难全身心的投入、学习那些有用但是对于完成作业意义不大的事物。这就好比OS课程,在github往届代码的引导下,我自己真的做不到完全独立开发。

  • 说完了一些关于实际问题的感想,我最后谈一谈我对于JML的一些心得。也算是一个首尾呼应

    • 我认为最重要(我的理解不一定正确)的一点,是设计与实现分离,而不是确保正确性。至少在课程中,我还并没有遇见过非JML不可的正确性判断方法,至少黑盒测试(对拍)和Junit的双重火力网可以在实际资源允许的限度和bug的容忍限度下很出色的完成要求(大概)。然而,作为面向对象初学者,我对于设计与实现分离的理解并不好。

    • 最早接触设计与实现分离是在计组P7,那也是我第一次接触千行规模的代码。面对复杂的工程,仅依赖大脑是完全不够用的。尽管计组的实验报告要求了设计文档,但那种文档仅仅聊胜于无,观赏意义大于实际意义。我在P7的时候完成了第一份设计文档。虽然很丑陋,很初级,确是我第一次真正先顶层设计再分块开发。之后的P8也不必说,我延续了这一思维
    • OO伊始,面对WF大战,并不复杂的架构带来的设计实现分离的一波低潮。在递归下降\parser的入侵之下,这一思维被我遗弃了。然而电梯中,尤其是后两次电梯,这一工具被我重新拾起。面对三部电梯的优化,没有一个顶层的大局设计,是很难完成的。尽管我最后分数很低,但我认为那是我的能力问题,不是思路问题
    • 而JML这一单元,可以说是“强迫”引入这一工具,毕竟我认为完整看完并理解甚至复演了整个Apprunner的同学应该会相当少(我比较摸,我连完整看完都没能做到)。可以说,在实际工程中,我认为我会遇到很多这样的问题(接手一个完成了一半的工程),试想,如果没有JML这类设计规范,只给出指导书(需求)和Apprunner的源代码,那完成这几次作业将会多么痛苦。可以说,没有设计实现分离的思路,就没有我只完成两个类及其算法的从容与淡定,更没有本单元就是个数据结构和图算法作业的感想。能把一个不算小的工程(千级别),退化成一个图算法作业,设计实现分离的威力可见一斑。

后记

写完博客回头阅读的时候,发现自己有些话可能说的不是那么符合自己的能力与身份,可能欠妥,不过,我也不想成为为了分数而圆滑的人。希望大家不要在意那些感到不舒服的字眼,在此提前致歉。

我不喜欢贴图,因为过大的图片很影响阅读体验,而且,很多图片表达的内容大同小异,无非是完成课程的要求,所以我尽量使用文字进行描述

最后老惯例,作为一门6系核心专业课和一门对未来有影响的课,真心祝愿OO课程越来越好

以上是关于OO的奇妙冒险3的主要内容,如果未能解决你的问题,请参考以下文章

JOJOの奇妙前端冒险(第一部) | 寻找C站宝藏

谷歌地图不显示在片段中

谈谈自己

第六章.解决大问题

Scala的面向对象与函数编程

鸿蒙的绿野仙踪