BUAA OO Unit3 Summary

BUAA OO Unit3 Summary

零、写在前面

百度JML结果如下(doge

一、基于JML规格的自测

本单元的中心就是JML规格的理解到实现,所以自然的,我们的测试也应该基于JML规格进行展开。

白盒测试

白盒测试对于我的意义更多在与对正确性的检测。JML给出规格的最大好处就是不会出现谜语,我们只需要针对每一个方法,根据所有requires分支设计测试,再根据对应的ensures进行检查即可,最重要的是覆盖所有的情况,一些要点如下:

  • Normal Behavior

    • 数学计算是否正确:精度损失问题
      • 是否考虑以指导书为准
      • 如果在优化过程中出现可以与基准方法的结果进行对比
    • 复杂方法:首先保证正确性,然后检查时间复杂度
      • queryBlockSum + isCircleap : qbs = 1 : 1
      • queryGroupValueSum1111 * ap + 1111 * atg + xxxx * qgvs
      • ……

    这时候会反映出利用JML自测的一些缺点。以大面积出现的"Group人数不超过1111"为例,看到了就不会写错、看不到也测不出来。所以有时候真不如多看几遍规格或指导书。

  • Exceptioal Behavior

    • 是否能检测到异常
    • 多异常的情况下抛出异常的顺序
    • 异常的参数是否设置正确:personId or emojiId

同时,课程组还向我们推荐了JUnit工具进行单元测试,可以得知覆盖率。在后两次作业中,我结合JUnit工具进行了比较基础性的正确性测试,感觉就是上手很快,但是并没有所说的那般“方便快捷”。一开始以为是可以基于JML规格自动生成数据测试,最后发现其实基本都是自己在写😄。大致过程如下:

  • 调用一些基本方法进行初始化,比如:ap, ar, atg, am
  • 在不同情况下,调用待测试的方法
  • 使用assert进行检验

黑盒测试

黑盒测试主要基于抱大腿开展(,在石哥哥高覆盖率的数据生成器支持下进行随机测试与压力测试、多人运动(对拍)检测正确性和运行时间,体验极佳。阅读数据生成的源码之后,发现在测试几个用到图论算法的指令(qlc, sim)时,可以先生成关系图(进一步可以设计稀疏图、稠密图、类似链状等各种情况),然后再测试其他指令,小有所获。同时,在有基础的人和群组的条件下,后续也能更好地覆盖非异常和异常的检测。

二、图模型的构建和维护策略

设计架构没什么可说的,在规格的约束下应该都大同小异,不然你可能就寄了(。

图的构建

对于所谓的”图模型“,我在本单元实现中并没有显式地构建一个Graph类进行管理,因为整体把握JML规格后,我们可以发现:

\[Network \rightarrow Graph \\ Person \rightarrow Node \\ Relation \rightarrow Edge \]

所以,每个PersonAquaintance集合就构成了一个邻接表,需要实现图论算法的时候在此基础上实现即可。

图的维护

容器选择

  • Hashmap<K, V> & Hashset<T>:大部分都采用哈希表进行储存,原因如下:
    • 基于 唯一id 的查询模式
    • 均摊 \(O(1)\) 的查询时间复杂度(数据应以查询为主)
  • Linkedlist<T>:用于储存消息队列,因为Message是在头部插入,此时复杂度为 \(O(1)\)
  • Counter:异常计数器
  • DisjointSets<T>:并查集,储存了每个连通分支(block)
  • Relation<T1, T2, T3>, Pair<T1, T2>:前者相当于带权边,在最小生成树算法中使用;后者储存人-距离,在最短路算法中使用

信息维护

思想其实很简单,维护一个变量,将查询的复杂度均摊到增删上面,以达到最后 \(O(1)\) 查询的目的。注意,在没有完全按照JML规格实现的情况下,需要明确会修改/影响变量取值的所有操作,并进行更新。具体在下一部分详述。

三、代码性能问题分析

本次代码正确性和性能都通过了课程组的测试,下面主要谈一下性能的优化。其实,我认为这一部分最大的难点其实在于冗长复杂JML规格的阅读理解,即从JML抽象的描述中识别出我们需要使用的数据结构或者算法。然而,很遗憾的是,学长学姐的博客中已经将成果直接展现了(连带可以进行的优化),所以仿佛跳过了最重要的一步、直接进入了实现过程。

我想,这也是助教一般到周五才在讨论区放出相关算法介绍的初心吧。

简而言之,性能优化的总纲就是:

消灭 \(O(n^2)\),优化 \(O(nlogn)\)

qgav & qgvs —— 维护变量

  • 前者维护ageSumageSqrSum两个变量,后者维护valueSum一个变量,方差计算公式如下:

    \[ageMean = ageSum / n \\ ageVar = ageSqrSum - 2 * ageMean * ageSum + n * ageMean ^ 2 \]

    为了保证精度,不能化到数学上的最简形式。

  • addPersondelPerson时进行维护,此外,addRelation的时候也要更新valueSum

    // update valueSum of Group
    for (int gid : ((MyPerson)getPerson(id1)).getMutualGroups((getPerson(id2)))) {
      	((MyGroup) getGroup(gid)).addValueSum(value);
    }
    

    getMutualGroups()顾名思义。

qbs & qci —— 并查集

  • qbs翻译过来就是查询连通分支数,qci翻译过来就是查询是否连通
  • 对并查集实现的优化,优化后查询复杂度为 \(O(1)\)
    • 路径压缩:搜索时,将该结点及其所有祖先结点直接指向根结点
    • 按秩合并:在合并(有新连接)时,总是让秩小节点所在的(矮)树加到秩较大节点所在的(高)树,秩相等时没有办法,深度只能改变
  • 设置blockSum变量记录连通块数,addPersonaddRelation时维护,查询复杂度也降到 \(O(1)\)

qlc —— Kruskal求最小生成树

  • 由于已经实现了并查集,遂选择使用Kruskal算法,连通性的判断复用dsu中方法即可
  • 需要对边集进行排序,加边的时候可以使用优先队列。算法本身复杂度 \(O(n)\),加上排序以后 \(O(nlogn + n)\)
  • 考虑到没有删边的操作,可以对最小生成树得出的MinValue进行缓存,比较trivial的奇技淫巧了😓
    • (根节点)没算过的:重新计算
    • 一个连通块内加边:重新计算
    • 两个连通块之间加边:NewMinValue = minValue1 + minValue2 + weightOfEdge
    • 其他情况:查询缓存

sim —— Dijkstra求最短路

  • 需求为单源最短路而且无负权边,遂考虑经典算法
  • 根据总纲,应采用堆优化将复杂度进一步降到 \(O(nlogn)\)
  • 终止条件为visit到了receiver,不用把距离全算出来

sm —— 其他设计

  • sendMessage的结构设计如下:

    \[Network.sm \rightarrow message.send \rightarrow message.sendP2PMessage |sendP2GMessage \]

    public void send(Network network) throws RelationNotFoundException, PersonIdNotFoundException {
        if (type == 0) {
            assert person2 != null;
            if (!person1.isLinked(person2)) {
                throw new MyRelationNotFoundException(person1.getId(), person2.getId());
            } else if (!person1.equals(person2)) {
                sendP2PMessage(network);
            }
        } else {    // type == 1
            assert group != null;
            if (!group.hasPerson(person1)) {
                throw new MyPersonIdNotFoundException(person1.getId());
            } else {
                sendP2GMessage(network);
            }
        }
    }
    
  • 对每种 message 重写 sendP2PMessage()sendP2GMessage() 即可

  • 优点:自认为优化了结构,增加了一点可拓展性,不用写一串 instanceof。缺点:message 直接访问了Network,很怪

四、对Network的扩展

假设出现了几种不同的Person:

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

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

接口及方法设计

  • AdvertiserProducerCustomer 继承自 Person 接口
  • 新增 AdMessageProductMessage 和。 PurchaseMessage 继承自 Message 接口。
  • 新增 Product 接口,储存:产品id,价格,库存,生产商,销售额,销售路径等
  • Network 主要的接口方法:
    • addProduct:添加商品,并更新 Producer 的商品列表
    • advertise:发送广告,描述比较抽象,具体实现可以如下
      • AdMessage 为单位,通过sendMessage 方法一对一或向群组发送
      • 按 Advertiser 为单位,一次直接向所有 Customers 发广告,内容包括自身绑定的所有产品
    • 更新sendMessage:使其支持销售信息和购买信息
      • sendProductMsg:Producer 向 Advertiser 发送,告知其自己要卖的 Product,之后 Advertiser 可推销该 Product
      • sendPurchaseMsg:Advertiser 向 Producer 发送,转钱 + 更新 Product 列表
    • querySalesVolume & querySalesPath:顾名思义。
    • ……

JML规格撰写

addProduct(Product product)

//@ public instance model non_null Product[] products;

/*@ public normal_behavior
  @ requires (\exists int i; 0 <= i && i < people.length;    
  @              people[i].getId() == id && people[i] instanceof Producer); 
  @ assignable products, getProducer(id).sellingProducts;
  @ 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]))));
  @ ensures getProducer(id).sellingProducts.length == \old(getProducer(id).sellingProducts.length) + 1;
  @ ensures (\exists int i; 0 <= i && i < getProducer(id).sellingProducts.length;   
  @             getProducer(id).sellingProducts[i] == product);
  @ ensures (\forall int i; 0 <= i && i < \old(getProducer(id).sellingProducts.length);
  @             (\exists int j; 0 <= j && j < getProducer(id).sellingProducts.length;   
  @       	 getProducer(id).sellingProducts[j] == (\old(getProducer(id).sellingProducts[i])));       
  @ also
  @ public exceptional_behavior
  @ signals (EqualProductIdException e) (\exists int i; 0 <= i && i < products.length; products[i].equals(product));
  @ signals (ProducerIdNotFoundException e) !(\exists int i; 0 <= i && i < products.length; 
                products[i].equals(product)) && (\forall i; 0 <= i && i < people.size; !(people[i].getId() == id && people[i] instanceof Producer))
  @*/
public void addProduct(Product product, int id) throws EqualProductIdException, ProducerIdNotFoundException;

sendMessage(int id)

仅展示新增部分

/*@ public normal_behavior
  @ requires containsMessage(id) && getMessage(id).getType() == 0 &&
  @          getMessage(id).getPerson1().isLinked(getMessage(id).getPerson2()) &&
  @          getMessage(id).getPerson1() != getMessage(id).getPerson2();
  @ assignable products, ((Advertiser) getMessage(id).getPerson2()).adProducts;
  @ ensures (\old(getMessage(id)) instanceof ProductMessage) ==>
  @		 ((\old((Advertiser) getMessage(id).getPerson2()).adProducts.length) == \old((Advertiser) getMessage(id).getPerson2()).adProducts.length - 1))
  @ ensures (\old(getMessage(id)) instanceof ProductMessage) ==>
  @ 		 (\exists int i; 0 <= i && i <= (\old((Advertiser) getMessage(id).getPerson2()).adProducts.length;
  @	         \old((Advertiser) getMessage(id).getPerson2()).adProducts[i])) == ((ProductMessage) \old(getMessage(id)))).product);
  @ ensures (\old(getMessage(id)) instanceof ProductMessage) ==>
  @ 		 (\forall int i; 0 <= i && i <= (\old((Advertiser) getMessage(id).getPerson2()).adProducts.length); 
  @ 		 (\exists int j; 0 <= j && j <= (\old((Advertiser) getMessage(id).getPerson2()).adProducts.length); 
  @ 		 \old((Advertiser) getMessage(id).getPerson2()).adProducts[i]))) == \old((Advertiser) getMessage(id).getPerson2()).adProducts[j])));
  @ ensures (!(\old(getMessage(id)) instanceof ProductMessage) && (\old(getMessage(id)).getPerson2() instanceof Advertiser)) ==>
  @ 		 \not_assigned(((Advertiser) getMessage(id).getPerson2()).adProducts);
  @ ensures (\old(getMessage(id)) instanceof PurchaseMessage) ==> (\old(getMessage(id)).getBuyer().getMoney() ==
  @             \old(getMessage(id).getBuyer().getMoney()) - ((PurchaseMessage) \old(getMessage(id))).getProduct().getPrice() * ((PurchaseMessage) \old(getMessage(id))).getNum() &&
  @             \old(getMessage(id)).getPerson2().getMoney() == 
  @             \old(getMessage(id).getPerson2().getMoney()) + ((PurchaseMessage) \old(getMessage(id))).getProduct().getPrice() * ((PurchaseMessage) \old(getMessage(id))).getNum());
  @ ensures (\old(getMessage(id)) instanceof PurchaseMessage) ==>
  @             (\exists int i; 0 <= i && i < products.length && products[i].equals(((PurchaseMessage) \old(getMessage(id))).getProduct()) &&
  @             products[i].sales == \old(products[i].sales) + 1) && products[i].inventory == \old(products[i].inventory) - 1));
  @ ensures (!(\old(getMessage(id)) instanceof PurchaseMessage)) ==> \not_assigned(products);
  @ ensures (!(\old(getMessage(id)) instanceof PurchaseMessage)) ==> (\not_assigned(people[*].money))
  @*/
  public void sendMessage(int id) throws RelationNotFoundException, MessageIdNotFoundException, PersonIdNotFoundException;

querySalesVolume(int id)

/*@ public normal_behavior
  @ requires containsProduct(id);
  @ ensures (\exists int i; 0 <= i < products.length; products[i].getId() == id && 
  @ 		 \result == (products[i].getSales() * products[i].getPrice()));
  @ also 
  @ public exceptional_behavior
  @ signals (ProductIdNotFoundException e) !containsProduct(id);
  @*/
  public int querySalesVolume(int id) throws ProductIdNotFoundException;

五、学习体会与心得

  • 种种迹象表明(指开头),JML语言可能在实际中并没有广泛的运用,idea也不支持JML的高亮捏。而且,JML语言在可读性和严谨性间做了trade off——导致个人读起来十分难受、实验中写的时候更是如此。总的来说,我认为本单元更大的意义在于让我对契约式编程有了接触和认识,也对规格约束和制定的重要性和艰难程度有了更深的理解。
  • \(规格 \ne 实现\) ,写规格已经很惨了,不用管别的了。实现规格是很容易的,这时候 \(效率 = 竞争力\)
  • 回顾并熟悉了一些图论算法,学习了java各种的容器特性,浅尝了lambda表达式(下个单元应该好用)
posted @ 2022-06-06 10:04  ezmoneysniper  阅读(23)  评论(0编辑  收藏  举报