BUAA OO Unit3 Summary
BUAA OO Unit3 Summary
零、写在前面
百度JML结果如下(doge
一、基于JML规格的自测
本单元的中心就是JML规格的理解到实现,所以自然的,我们的测试也应该基于JML规格进行展开。
白盒测试
白盒测试对于我的意义更多在与对正确性的检测。JML给出规格的最大好处就是不会出现谜语,我们只需要针对每一个方法,根据所有requires
分支设计测试,再根据对应的ensures
进行检查即可,最重要的是覆盖所有的情况,一些要点如下:
-
Normal Behavior
- 数学计算是否正确:精度损失问题
- 是否考虑以指导书为准
- 如果在优化过程中出现可以与基准方法的结果进行对比
- 复杂方法:首先保证正确性,然后检查时间复杂度
queryBlockSum + isCircle
—ap : qbs = 1 : 1
queryGroupValueSum
—1111 * ap + 1111 * atg + xxxx * qgvs
- ……
这时候会反映出利用JML自测的一些缺点。以大面积出现的"Group人数不超过1111"为例,看到了就不会写错、看不到也测不出来。所以有时候真不如多看几遍规格或指导书。
- 数学计算是否正确:精度损失问题
-
Exceptioal Behavior
- 是否能检测到异常
- 多异常的情况下抛出异常的顺序
- 异常的参数是否设置正确:
personId
oremojiId
同时,课程组还向我们推荐了JUnit工具进行单元测试,可以得知覆盖率。在后两次作业中,我结合JUnit工具进行了比较基础性的正确性测试,感觉就是上手很快,但是并没有所说的那般“方便快捷”。一开始以为是可以基于JML规格自动生成数据测试,最后发现其实基本都是自己在写😄。大致过程如下:
- 调用一些基本方法进行初始化,比如:
ap, ar, atg, am
- 在不同情况下,调用待测试的方法
- 使用
assert
进行检验
黑盒测试
黑盒测试主要基于抱大腿开展(,在石哥哥高覆盖率的数据生成器支持下进行随机测试与压力测试、多人运动(对拍)检测正确性和运行时间,体验极佳。阅读数据生成的源码之后,发现在测试几个用到图论算法的指令(qlc, sim
)时,可以先生成关系图(进一步可以设计稀疏图、稠密图、类似链状等各种情况),然后再测试其他指令,小有所获。同时,在有基础的人和群组的条件下,后续也能更好地覆盖非异常和异常的检测。
二、图模型的构建和维护策略
设计架构没什么可说的,在规格的约束下应该都大同小异,不然你可能就寄了(。
图的构建
对于所谓的”图模型“,我在本单元实现中并没有显式地构建一个Graph
类进行管理,因为整体把握JML规格后,我们可以发现:
所以,每个Person
的Aquaintance
集合就构成了一个邻接表,需要实现图论算法的时候在此基础上实现即可。
图的维护
容器选择
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 —— 维护变量
-
前者维护
ageSum
和ageSqrSum
两个变量,后者维护valueSum
一个变量,方差计算公式如下:\[ageMean = ageSum / n \\ ageVar = ageSqrSum - 2 * ageMean * ageSum + n * ageMean ^ 2 \]为了保证精度,不能化到数学上的最简形式。
-
在
addPerson
和delPerson
时进行维护,此外,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
变量记录连通块数,addPerson
和addRelation
时维护,查询复杂度也降到 \(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规格模式)。
接口及方法设计
Advertiser
,Producer
和Customer
继承自Person
接口- 新增
AdMessage
,ProductMessage
和。PurchaseMessage
继承自Message
接口。 - 新增
Product
接口,储存:产品id,价格,库存,生产商,销售额,销售路径等 Network
主要的接口方法:addProduct
:添加商品,并更新 Producer 的商品列表advertise
:发送广告,描述比较抽象,具体实现可以如下- 按
AdMessage
为单位,通过sendMessage
方法一对一或向群组发送 - 按 Advertiser 为单位,一次直接向所有 Customers 发广告,内容包括自身绑定的所有产品
- 按
- 更新
sendMessage
:使其支持销售信息和购买信息sendProductMsg
:Producer 向 Advertiser 发送,告知其自己要卖的 Product,之后 Advertiser 可推销该 ProductsendPurchaseMsg
: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表达式(下个单元应该好用)