OO第三单元总结
基于JML规格的测试方法
-
在第三单元的学习中,学习了JML的基本知识,基于JML规格逐步拓展我们要实现的社交网络。而在基于JML的编程可以认为是一种契约式编程,即我们需要读懂JML规格的要求,然后完成我们的任务以满足JML的要求,这里面就涉及到了前置条件,以及后置条件,以及如
ensure
和invariant
等要求。 -
前置条件:基于JML规格编程很重要的一部分就是前置条件,前置条件相当于是要求使用者,也就是“客户”需要保证的。只要方法的调用者保证了前置条件的实现,我们才能保证程序的结果的准确性。而对于不满足前置条件的调用,我们也是无需保证正确性的。所以在做测试的时候,我们生成的数据很重要的一点就是要满足JML给出的前置条件,前置条件可以有很多种情况,我们需要覆盖所有的前置条件的情况。(当然我们也可以构造不满足前置条件的情况,但这样违背了JML规格,所有运行的情况也是没有参考意义)
-
后置条件: 后置条件是我们的测试中检测是否正确,是否满足了JML规格的重要一部分,也是我们实现者在得知了合法的前置条件后,需要保证的结果。后置条件的要求也许是大量的,比如在这个单元中我们会发现
ensures
起头的JML规格占据了一大部分。这是因为JML语言作为一直规格化的语言,它需要通过细致繁琐的描述来保证调用者和实现者两方之间不产生歧义。一个简单例子就是,删除小组内的一个人员。用自然语言来描述这件事情很简单,但这种描述是不全面的。比如删除了这个人员,其他人员是否可以变动了?他们在列表里的顺序是否可以变化呢?这些东西都是很难通过自然语言叙述完整的。所有在JML中通过大量的ensure
关键词来描述后置条件,我们的结果必须也只需保证了这些后置条件,就可以实现两方之间的信息对等,不存在理解上的误差。所以,这对于我们测试而言,就是需要确定每个方法在当前前置条件下,是否保证了后置条件的准确性。
图模型构建和维护策略
- 在JML单元的学习中有一个比较有意思的点是,JML为了描述会“定义”一些数据结构,如
/*@ public instance model int id;
@ public instance model non_null String name;
@ public instance model int age;
@ public instance model non_null Person[] acquaintance;
@ public instance model non_null int[] value;
@ public instance model int money;
@ public instance model int socialValue;
@ public instance model non_null Message[] messages;
@*/
这里就给我们构建模型提供了一个参考思路。最暴力简单的思路就是直接翻译 JML,把它转换成java代码。比如
/*@ public normal_behavior
@ requires (\exists int i; 0 <= i && i < acquaintance.length;
@ acquaintance[i].getId() == person.getId());
@ assignable \nothing;
@ ensures (\exists int i; 0 <= i && i < acquaintance.length;
@ acquaintance[i].getId() == person.getId() && \result == value[i]);
@ also
@ public normal_behavior
@ requires (\forall int i; 0 <= i && i < acquaintance.length;
@ acquaintance[i].getId() != person.getId());
@ ensures \result == 0;
@*/
public /*@ pure @*/ int queryValue(Person person);
在这个方法中,我们很容易由JML相到一种简单的思路,也就是同时维护两个 数组 acquaintance
和value
,调用这个方法的时候直接遍历即可。但是这 样会导致 查询和维护的成本高,效率低。这种情况在后续的group
等类里同 样也可以看 到,直接由JML转换成代码构建模型,导致很多性能上的缺陷。
- 在理论课的学习中我们知道,JML规格和我们内部的实现是没有任何联系的,也就是说我们完全不需要有两个数组
acquaintance
和value
在我们实现的类当中,这两个数组只是在调用者的视角里面有这样两个数组,我们只要实现的效果等效即可。这里我们采取HashMap来进行维护。即用id作为key值,value作为HashMap中的value值即可,这样访问和维护的难度大大降低。这样的思路在后面很多地方同样用得到,因为我们可以利用id唯一的特点,重复利用hashMap的特点来维护等等数据。
性能问题和修复情况
-
算法选择
-
除了上述利用HashMap的特点来维护有关数据,以及想平均值等可以动态维护的数据外,这次作业有三个比较重要的算法需要我们实现,实现不好可能就会tle等。
-
并查集
- 并查集是第一次作业中设计到的算法,如果是利用JML给出的那种方法或者是通过循环遍历的方法,复杂度都在O(n^2)以上,所以不可取。所以在第一次作业中我采取了路径压缩算法,也就是基于c语言改造成java的一个递归算法。在这个算法中,只有第一次查询的复杂度稍微高一点,但是经过第一次查询会转化为一个深度只有1的数,所以之后的查询复杂度都是O(1)这就极大的减少了时间。
-
Prim算法
- 在第二次算法中我们使用的是Prim算法,因为在第二次作业中明确了最多的人,以及这条指令的最大使用次数,所以使用最基本的Prim算法即可。
-
Dijkstra算法
- 第三次作业开始的时候使用的是基本的Dijkstra算法,但是还是超时了。后来改成了基于
PriorityQueue
来实现优化。这是因为PriorityQueue
内部维护了一个堆,每次我们只需要取堆顶部的元素即可,改善了以往算法需要遍历才能找出来最近的点的缺陷,从而降低了复杂度。
- 第三次作业开始的时候使用的是基本的Dijkstra算法,但是还是超时了。后来改成了基于
-
-
除此之外还出现了
query group value sum
指令复杂度过高的情况,原因就是直接翻译了JML规格,而规格是一个O(n^2)的描述方法,在多次查询的情况下很容易超时,所以我们只需要动态维护一个value值即可,在加入删除的时候维护一下这个值即可。
扩展NetWork
-
新增producer、Advertiser、Customer接口,并且继承Person接口
-
新增GoodsMessage继承,继承Message接口。
-
核心业务方法JML规格
-
Producer生成一件商品
-
/*@ public normal_behaviour @ assignable producer.goods, goods.producer, goods.type; @ ensures producer.goods.length = \old(producer.goods.length) + 1; @ ensures (\forall int i; 0 <= i && i < \old(producer.goods.length); @ (\exists int j; 0 <= j && j < producer.goods.length; producer.goods[j] == (\old(producer.goods[i])))); @ ensures goods.producer == producer; @ ensures (\exists int j; 0 <= j && j < producer.goods.length; producer.goods[j] == goods); @ ensures \result.contain(goods) @ ensures \result.money == money @ ensures \result.information = information @*/ goodsMessage produce(Producer producer, int money, String information);
-
-
Advertiser推销一件物品给Customer
-
/*@ public normal_behaviour @ requires advertiser.hasMessages(message) @ assignable customer.messages, message; @ ensures customer.messages.length = \old(customer.messages.length) + 1; @ ensures customer.hasMessages(message); @ ensures goods.advertiser == advertiser; */ void advertise(Advertiser advertiser, Customer customer, GoodsMessage gooodMessage);
-
-
Customer根据兴趣(information)购买一件物品
-
/*@ public normal_behaviour @ assignable customer.money, goodsMessages.advertiser; @ requires (\forall int i; 0 <= i && i < customer.messages.length; messages[i] instanceof gooodMessage && gooodMessage.information == information); @ ensures customer.money = customer.money - goodsMessage.Money; @*/ void purchase(String information);
-
-
学习体会
在第三单元的学习中我们主要学了阅读JML,体会了一会“甲方乙方”,了解了契约化编程的基本思想,这种描述方法有着严谨性,比自然语言在描述问题上提供了一种有利手段。但是在学习的过程中,很容易发现JML语言的缺陷,那就是过于繁复。很简单的一个例子,就是最短路径短小生成树的JML描述,异常冗杂繁复,根本不适合人脑阅读。所以不难思考到一个点,只有JML规格也是不够的,也是不能把任务描述清楚,尤其是当任务复杂的时候,需求者和开发者如果想通过只JML规格来实现交流,这种难度还是很大的。所以这时候又开出了自然语言的优势,那就是易于理解和沟通。也许自然语言会存在盲区,但沟通是人类最基本的手段,再复杂的问题也是能够通过自然语言描述清楚的。我们阅读JML的时候不也还是本能地转化为自然语言去理解hhh。