BUAA_OO 第三单元总结-JML

一、总览

​ 本单元要求按照给定的JML规格实现一个社交网络,通过指令实现添加Person或Relation,消息收发,查询等功能。这一单元的主要目的大约在于了解并熟悉JML规格描述语言,初步体会这种契约式编程思想。

​ 在作业的完成上,难度相比之前两个单元小一些,大部分代码只需要读懂JML,对要实现的功能有一定的理解,然后翻译即可。可以说,这个单元在架构上是没什么难度的,我个人觉得难度主要在于数据结构与算法方面,包括图的建模以及一些图论算法(涉及到的有最小生成树、最短路径、并查集等)。

​ 这个单元的主要目的是JML的阅读与理解,但是其实我觉得作业中没有过多的体现使用JML的好处。很多方法其实本来只需要一句话就能说清楚,但是JML需要写一大段,不仅阅读起来很耗时间,而且对于编写者是一个很大的挑战(其实课程组可以让我们自己写一些JML,我觉得这样更有助于掌握)。

​ 我个人觉得,JML相比于自然语言的好处更多体现在:

  1. 对于边界条件的描述上。比如什么条件下会引发异常、什么条件下会正常返回。
  2. 对于前置条件的表述。
  3. 描述哪些参数是assignable的。

其实上面这几点使用自然语言也可以描述,但是很难保证在表述过程中不会出现歧义;而使用JML表述更方便简洁也更容易阅读。

​ 但是我觉得JML不是很适合用来描述比较复杂的算法:难于阅读、难于编写、难于保证编写的正确性、难于保证负责编程的程序员会认真阅读JML......

​ 综上,JML不是万能的,在实际使用过程中要因地制宜。

二、实现

2.1 数据结构

​ 需要存储的数据有Network里面的Person、Message、Group,Group里面的Person,由于输入指令查询的时候是用id查询的,可以使用HashMap存储这些对象和他们的ID。对于这个图的建模我在每个Person中存储了一个acquaintance-->socialValue的HashMap,用于实现图的一些算法。

2.2 架构与UML图

​ 由于这个单元的类之间的耦合关系不是很复杂,因此画出来的UML图也没有很优雅的感觉,很平平无奇。

​ 在架构上,需要实现的类课程组已经帮我们设计好了,所以大家的UML图可能都差不多。

image

第三单元第三次作业UML

2.3 性能与算法

2.3.1 查询连通性

​ 用并查集,很简单、很高效,我很喜欢并查集,尤其是路径压缩,很优雅。按秩合并我觉得和路径压缩一起用就会失效(因为难以维护树的高度),按秩合并适用于不能改变树的结构的情况。但是很多并查集教程里面把这两种优化方法并列起来,很容易造成误导。

2.3.2 最小生成树

​ 克鲁斯卡尔算法,先用并查集得到联通子图,再循环遍历person获取这个子图中的边,按照权值排序(或者在获取边的时候直接用优先队列),从小到大在保证不生成圈的情况下把边一条条加入子图,直到 边的数量 == 节点的数量 - 1。

2.3.3 最短路

​ 迪杰斯特拉算法。

2.3.4 计算方差🍌

​ 一个\(O(1)\)的小trick,没什么必要,但是挺有意思,分享一下,仅供参考

首先来证明一个递推公式:

\(\bar x_n = \frac 1n(\sum_{i = 1}^n x_i)\)\(\{x_1,...x_n\}\)的平均值

\(a_n = \sum_{i = 1} ^ n(x_i - \bar{x_n})^2\),则方差\(s_n = a_n / n\)

对于\(\{x_1,...x_n,x_{n+1}\}\)

image

于是我们就得到了

\[a_{n+1} = a_n + \frac{n}{n+1}\cdot(\bar x_n -x_{n+1})^2 \]

​ 再来看看误差(在计算机数值计算里面,如果一个递推公式的误差会随着递推次数指数级增长,那这个公式肯定是废了),由于上面这个式子是求和,因此\(a_{n+1}\)的误差是\(a_n\)和后面这部分的误差的合成,误差不会指数型增长,很好!可以用!(这个公式我在oo作业中没有使用,不过可以从数学推导是保证他是正确的。)

​ 不过对于这种最多\(O(n)\)的方法进行优化好像没多大实际意义))

后来发现可以直接用这个公式来着:

\[S_n(x) = \bar {x^2} - (\bar x)^2 \]

多维护一个 x^2 的总和就行了

三、拓展Network

题目:

假设出现了几种不同的Person

  • Advertiser:持续向外发送产品广告
  • Producer:产品生产商,通过Advertiser来销售产品
  • Customer:消费者,会关注广告并选择和自己偏好匹配的产品来购买 -- 所谓购买,就是直接通过Advertiser给相应Producer发一个购买消息
  • Person:吃瓜群众,不发广告,不买东西,不卖东西

如此Network可以支持市场营销,并能查询某种商品的销售额和销售路径等 请讨论如何对Network扩展,给出相关接口方法,并选择3个核心业务功能的接口方法撰写JML规格(借鉴所总结的JML规格模式)

在项目中新增Advertiser、Producer、Customer三个类,分别继承Person类。为了实现广告收发和购买,需要新增Message的子类AdvertiseMessage和PurchaseMessage。

JML

添加广告


/*@ public normal_behavior
      @ requires !(\exists int i; 0 <= i && i < messages.length; messages[i].equals(message)) &&
      @ message.getType() == 2 && (message.getPerson1() instanceof Advertiser);
      @ assignable messages;
      @ ensures messages.length == \old(messages.length) + 1;
      @ ensures (\exists int i; 0 <= i && i < messages.length; messages[i].equals(message));
      @ ensures (\forall int i; 0 <= i && i < \old(messages.length);
      @          (\exists int j; 0 <= j && j < messages.length; messages[j].equals(\old(messages[i]))));
      @ also
      @ public exceptional_behavior
      @ signals (EqualMessageIdException e) 
      @ (\exists int i; 0 <= i && i < messages.length; messages[i].equals(message));
      @ signals (AdvertiserNotFoundException e) 
      @ !(\exists int i; 0 <= i && i < messages.length; messages[i].equals(message)) 
      @ && !(messages[i].getPerson1() instanceof Advertiser);
      @*/
public void addAdvertisement(Message message) throws EqualMessageIdException, AdvertiserNotFoundException;

添加产品

/* public normal_behavior
      @ requires !(\exists int i; 0 <= i && i < products.length; products[i].equals(product));
      @ assignable products;
      @ ensures products.length == \old(products.length) + 1;
      @ ensures (\exists int i; 0 <= i && i < products.length; products[i] == product);
      @ ensures (\forall int i; 0 <= i && i < \old(products.length);
      @          (\exists int j; 0 <= j && j < products.length; products[j] == (\old(products[i]))));
      @ also
      @ public exceptional_behavior
      @ signals (EqualProductIdException e) (\exists int i; 0 <= i && i < products.length;
      @                                     products[i].equals(product));
      @*/
public void addProduct(/*@ non_null @*/Product product) throws EqualProductIdException;

查询销售额

/*@ public normal_behavior
      @ requires (\exists int i; 0 <= i && i <= people.size; 
      @                     people[i].equals(producer) && people[i] instanceof Producer)
      @ ensures \result == producer.getMoney
      @ also
      @ public exceptional_behavior
      @ signals (PersonIdNotFoundException e) 
      @ (\forall i; 0 <= i && i < people.size; !people[i].equals(producer))
      @*/
public int querySaleVolume(Person producer) throws PersonIdNotFoundException;

四、bug与测试

4.1 bug记录

​ 三次强测均未发现bug,第二次作业的互测被测出了一个bug,是qgvs超时的问题。

​ 出bug的原因是最初版本的代码用的方法是\(O(n^2)\)遍历group里面的所有人,如果一个group里面有10 000人,就需要进行100 000 000次查询。修改起来也很简单,只需要对于group里面的每个人的acquaintance进行遍历就可以,这样就能把复杂度控制在\(O(n)\)(其中n为这个图中边的数量)。

4.2 测试

4.2.1 黑盒测试

​ 这一单元的测试我觉得主要有两个方面:1.正确性;2.性能

​ 在正确性测试上,完全随机数据有很好的效果,只要跑的数据量比较大,大多数问题都能够发现。(感谢臻哥写的评测机!

​ 在性能测试上,应当重点关注一些有\(O(n^2)\)复杂度的算法或者比较复杂的算法。单独写一个针对这种算法的数据生成器,考虑最边界的情况。

​ 这种方法在第三次作业中让我发现迪杰斯卡算法如果不用堆优化是会超时的。我构造的数据是在数据限制下将尽可能多的人连成一条链,再多次对链两端的人sim(send_indirect_message),通过调整参数就能让一个不好的程序超时很多。(其实这个已经不算黑盒测试了)

​ 要想在性能上测试得更全面,可以构造稀疏图,再比较随机地查询。

4.2.2 JUnit

​ 说实话我没有尝试用这个来测。要是自己编写单元测试的话,只能测出来“我脑子想的和代码跑的不一样”,而不能测出来我对指导书的理解有问题。JUnit测试的效果很大程度上取决于编写测试的人,自己测自己的代码总感觉没什么效果。

五、心得体会

​ 这个单元挺轻松的,收获大抵有

  • 学会阅读JML
  • 在之后和别人合作的时候应该提前制定好接口和规则。哪些对象assignable很重要
  • 复习并实践了图论算法
posted @ 2022-06-03 14:30  Banana889  阅读(29)  评论(0编辑  收藏  举报