转眼第三单元的学习就结束了,这学期oo的余额剩余也不多了,且行且珍惜
这个单元的主题是规格,依赖于JML来展开的学习,首先不得不吐槽一下JML是真的商业化产品有点少,不过毕竟JML不是重点,规格才是重点,所以也就不多说了
相比于之前单元,这个单元并没有让大家去全套实现一个系统,而是去根据规格,也就是"契约"去实现一些偏底层的组件,满足相应的要求,同时,作业也是没有了之前指导书上的长篇文字描述,取而代之的是JML的规格描述。
不急着说作业,谈谈个人理解吧。个人认为,规格的使用主要有两大好处,一个是便于测试的展开,不论是javadoc
还是JML
,它们都比较全面的表述了一个方法应该达到什么样的效果,并对于一些特殊情况或是边界情况又该如何处理,可以说是一次全面的思考,经过这番思考,写出规格,我们就可以根据规格做出相应的测试,特别是单元测试;第二是便于沟通和协调,在一个多人团队合作开发的项目中,有一个共用的规范是很有意义的,这样可以直接统一大家的思路,每个人按规格实现方法,规格就是成员之间的承诺,而JML
的准确表达的能力又是让这个效果进一步放大,虽然说不是很好读。
好,那么下面说说作业吧。
首先先说一说JML相关的生态
JML语言和工具链
JML语言其实对于复杂一些的逻辑,描述其相应的规格本身就是一件比较难的事情,会把规格写的很多很复杂,也因此,阅读难度就更是可想而知了,虽然我们鼓励写简单函数,写短函数,但是很多时候,一些复杂的业务逻辑情景必须要很多的内容来支持才能写出相应的规格,这件事情还是没法避免的
JML的语法这里就不多说了,指导书写的很详细,网上的资料也很多,所以这里说一说JML,或者说是规格所体现出的思想和用法
我们的规格包括下面几个部分:
- 前置条件
- 可改变的元素和属性
- 后置条件
通过规定这几个部分,一个函数的行为基本就被框住了,也就是,调用者从这个规格可以知道,这个函数需要什么样的数据,然后作用后会产生哪些副作用,最终的结果会是怎么样,这就基本完全概括了一个函数对于调用者的需要内容
同时,实现者通过阅读规格,可以知道自己的函数要处理什么样的情形的数据,对于不符合的数据要怎么处理,处理过程中要只能改变哪些变量和属性的值,最终应该达到什么样的效果
通过规格,调用者和实现者的思想就统一了,形成了一种契约,而JML只是以一种无二意性的符号语言的形式将规格的内容展现了出来,JML本身没什么神奇的,厉害的是规格和契约的概念
至于JML的工具链是真的没啥,从IDEA这样的比较知名的IDE上都没有相关的开发工具这件事上也可见一斑,但是聪明的大家还是找到了很多相关的工具,基本可以最大化地发挥JML的作用
这里将工具链一一列出,下面会详细介绍到相关的内容:
OpenJML
可以静态检查JML语法的正确性,同时也可以作为编译时的依赖包,生成可以运行时检查程序是否符合JML规格的class
文件JMLUnitNG
可以根据JML自动生成TestNG
测试文件的工具(Junit大法好),亲测对于边缘测试等的支持还是不错的SMT Solver
这个好像和JML没啥关系,是用来证明程序逻辑等价的,也就是可以用来从形式上证明两个函数的效果是等价的
JMLUnitNG 试用报告
不实名感谢伦佬的博客
首先是先进行相应的编译和测试文件生成
~/Desktop java -jar openjml-0.8.42-20190401/jmlunitng.jar demo/Demo.java
~/Desktop javac -cp openjml-0.8.42-20190401/jmlunitng.jar demo/**/*.java
~/Desktop ./openjml-0.8.42-20190401/openjml -rac demo/Demo.java
生成和编译出来的文件中,Demo_JML_Test就是最终的测试类
用jmlunitng
来运行这个主类,可以得到如下的结果
~/Desktop java -cp openjml-0.8.42-20190401/jmlunitng.jar: demo.Demo_JML_Test
[TestNG] Running:
Command line suite
Passed: racEnabled()
Passed: constructor Demo()
Passed: static compare(-2147483648, -2147483648)
Failed: static compare(0, -2147483648)
Failed: static compare(2147483647, -2147483648)
Passed: static compare(-2147483648, 0)
Passed: static compare(0, 0)
Passed: static compare(2147483647, 0)
Failed: static compare(-2147483648, 2147483647)
Passed: static compare(0, 2147483647)
Passed: static compare(2147483647, 2147483647)
Passed: static main(null)
Passed: static main({})
===============================================
Command line suite
Total tests run: 13, Failures: 3, Skips: 0
===============================================
可以看到测试对于边界条件的支持还是做得很好的,对于int类型数据的边界测试做得是比较完全的
可惜这个工具的能力还不是很强,很多场景还不能胜任,不得不说是这个工具的一大遗憾
作业分析
我不打算一次一次的分析了,这么做可能有点违规,但是我觉得第三次作业就能完全涵盖我前两次作业的设计
那么先上UML图
首先夸一波这个单元作业的迭代开发设计,每次的迭代完全不会影响之前系统的功能,完全不存在像迭代过程中,原来对的时候后来又不对了的情况。
这三次作业我都是用的继承下来实现的,可以看UML图中间的那两根曲折的蓝线,MyRailWaySystem
继承MyGraph
继承PathContainer
,这样的好处是,不用复制粘贴,之前的实现是经过考验的,我们直接继承下来,然后在此基础上做扩展,就可以在丝毫不影响原来功能的情况下,实现新的功能。
然后每次的迭代因为都有新的功能加入,所以可以每次都设计新的一组专用的数据结构来专门负责实现相应的功能,彼此之间不会有任何影响,解耦一级棒
特别这里重点说一下第三次作业,第三次作业表明上是实现了四个新功能,但是细看,除了连通块以外,其他三个功能的实现方式都基本是一样的,这里笔者的做法是Dijstra+拆点,我们可以发现,其实计算三种维度下的最优路线的主体方式基本都是一致的,就是Dijstra的算法框架,唯一不同的是路径权值的计算方式,所以,可以将这些计算逻辑分离出来,单独形成一个类,这里对应笔者的ComputeCore
,而三个继承得来的实体类只需要实现计算路径权值的方法就可以了,这样做到了代码最大程度的复用,nice!
在此特别插播一件笔者本人的亲身经历,笔者最开始使用Dijstra算法的时候,没有使用堆优化,导致随机6000条指令的运行时间可以达到50s+,后来在加堆优化的时候,因为笔者的设计将所有的公共运算逻辑都分离了出来,所以只用在ComputeCore
类中进行添加堆优化算法,就顺利解决了这个问题,没有引入任何乱七八糟的神奇bug,这就是这样设计的好处,笔者也是真正亲身体验了一把什么叫做易于修改和维护。
然后差不多就是这样了,不行,这样太水了,会被骂的,下面就说说一些设计细节吧。
首先是拆点,首先感谢xwl大师提出的Pair
概念,笔者在此稍微提供一点小技巧。
在笔者实现和使用Pair
类的时候,发现反复new
新的Pair
会出现同路线上的同个站点拥有多个不同的Pair
的情况,在此,笔者强推单例模式,同个在Pair
类中维护所有已生成的Pair
对象的缓存,从而避免产生多个重复的Pair
,保证了Pair
的唯一性
其次就是建议单独分离出建模层,也就是数据层,然后计算层使用建模层的数据来进行相关的计算。
具体体现就是上面的BaseGraph
和SplitPointGraph
这两个类,这两个类分别服务于第二次和第三次作业中新添加的功能,而使用者只需从中获取相应的数据而无需关心数据是如何被组织起来的,只用知道数据已经被组织好了就足够了,依然是可以进一步解耦,更加满足单一职责原则。
而关于缓存,笔者这里单独实现了一个GraphCache
类,专门用来实现缓存,其本质就是一个HashMap
,加了一些判断缓存是否命中,添加缓存以及缓存失效的方法,实现起来是很简单的,但是用起来还挺好使的。
至于别的,特别是算法方法,笔者连堆优化都不知道加就不在这里卖丑了。
下面给出整个设计的度量
因为类和方法太多,就不一一列举了,这里只给出整体结果
ev(G) | iv(G) | v(G) | OCavg | WMC | |
---|---|---|---|---|---|
Total | 169.0 | 167.0 | 215.0 | 209 | |
Average | 1.58 | 1.56 | 2.01 | 1.95 | 12.29 |
可以看到,整体的复杂度都是很低的,在详细的统计中,也只有个别的方法复杂度较高,不过也往往是由于经典算法本身不可避免的原因导致,所以整体结果还是比较可观的。
Bug相关
三次作业中第一次作业强测过程中出现过bug
主要问题出现在TLE上,原因是每次查询不同点的个数的时候都要暴力遍历整个容器,在频繁查询的压力测试下,时间上的表现并不好
解决方法也很简单,就是添加一个HashMap来维护每个点出现的次数,每次询问的时候,直接返回HashMap的键的个数即可
又到了我瞎BB的时间了~~
这三次作业其实整体难度不算难,但也不算简单,特别是对于像笔者这样算法水平比较差的人,第三次作业没有讨论区是不可能的,这辈子做作业都不能没有讨论区的,确实是比较有挑战。
不过这个系列作业的迭代设计确实是好,尤其是第三次如果使用方法得当的话,代码复用的效果更好,但是建议或许可以给大家一些算法上的启示,这样应该更有利于大家搭建出架构良好的系统。
不过说了这么多,好像这个单元的主题是规格额,不过这也没法说,毕竟也不能让同学手写规格,然后助教大大们目力给分不是,不过既然是一个团队开发技术,那么来年尝试一下组队开发大一些的项目是不是会更好,让大家可以切身体会到用规格而不是用嘴撕逼的好处。
差不多就是这样了,怎么说是又学了一个从未接触过的领域,收获颇丰。
来源:oschina
链接:https://my.oschina.net/u/4351395/blog/3530605