BUAA_OO_2022_Unit3总结
一、架构设计和性能优化
1.1 第九次作业
目标是实现MyGroup
,MyNetwork
,MyPerson
三个类,实现简单社交关系的模拟和查询,并且实现六个抽象异常类,要求具有计数功能。
图结构可以自然地由JML抽象得出:
每个Person
对象视为图中的点,Person
之间的acquaintance
关系视为图中的带权边,其中value
视为权。
由于JML中的规格属性大多都以<id, object>
的形式成对出现,且在查询时均未涉及任何对象的顺序要求,如Person.acquaintance
,Person.value
,Network.people
,Network.groups
.故采取HashMap
维护这些属性,以达到O(1)的查找效率。
1.1.2 并查集优化
本次作业存在MyNetwork.isCircle()
和MyNetwork.queryBlockSum()
这两个高复杂度的图方法,由于二者均涉及图连通性的查询,而不涉及具体路径的获取。这与并查集的作用非常契合,故可在MyNetwork
类中维护一个并查集。
使用独立的MyUnionFindSet
类实现并查集数据结构。并以该类的一个对象作为MyNetwork
类的属性。在实现时使用了按秩合并和路径压缩两种优化策略避免时间复杂度的退化。路径压缩时为避免爆栈采取了效果稍差但更安全的非递归写法。
这样就将是否连通转化为了查询两节点的父节点是否相等的问题,MyNetwork.isCircle()
复杂度降为O(log_2n).
public boolean isConnected(int a, int b) {
return find(a) == find(b);
}
此外,在并查集类中使用一个独立属性treeCount
维护连通块的数量,初始化为0,每次加入点时自增,每次合并操作后自减,实现了MyNetwork.queryBlockSum()
的O(1)优化。
public int getTreeCount() {
return treeCount;
}
1.1.3 异常计数器类的实现
新建一个Counter
类用于异常的计数,每个异常类均配有一个静态的Counter
对象作为属性。
Counter
类内部维护一个key
为id
,值为计数值的HashMap
和一个totalCount
属性用于总计数。每次抛出异常时在HashMap
的对应id
的值和总计数值自增即可。
1.2 第十次作业
主要迭代开发的内容为增加私聊与群发消息、查询最小关系(对应最小生成树算法)等功能,并实现新的异常类。
1.2.1 最小生成树算法的处理
对于方法MyNetwork.queryLeastConnection()
的实现,我使用了Kruskal算法。为规避算法的高复杂度,我使用了先前定义的并查集类做优化。这时独立的并查集类发挥了它的重用性,使得判断连通分支时的复杂度得以优化。另外,为了维护算法中按权值大小取边的要求,我创建了一个Edge
类表示权和两点id的三元组,实现Conparable
接口用来抽象地封装每条边。在取最小边时,我使用了一个有序的ArrayList<Edge>
来维护一个有序的边集,每次addRelation
时维护,调用算法时遍历即可。
1.2.2 变量的维护
在MyNetwork
中,我使用了两个变量存储上一次调用qlc时查询的点和上一次的查询结果,这是为了优化在一个连通分支连续调用多次qlc的性能。若每次查询的点和上一次连通,则可以实现 O(1) 查询。
另外在MyGroup
类中,维护了ageSum
, ageSquaredSum
和valueSum
三个变量,实现了MyGroup.getValueSum()
,MyGroup.getAgeMean()
和MyGroup.getAgeVar()
三个方法的O(1)优化。
1.3 第十一次作业
主要迭代开发的内容为增加消息的子类红包消息和emoji消息,对emoji的热度排序,并实现收发间接消息(对应最短路径算法),并实现新的异常类。
1.3.1 最短路径算法的处理
我使用了堆优化的Dijkstra
算法。利用PriorityQueue<Edge>
来实现堆排序,避免了性能问题。复用了Edge
类来实现路径,其中的weight
属性用来表示最短路径,而不是权。
二、测试策略
2.1 JUnit
本单元中我初步尝试了JUnit测试工具对我的图算法进行简单测试,但由于操作不够熟练,没有大规模使用,仅仅作为初步测试用。
2.2 同学对拍
和同学对拍是我发现bug的主要手段。我编写了一个随机数据的生成程序,和同学编写的运行对比程序配合使用来进行大量数据点的对拍,包括图的稀疏度变化、异常数据、高复杂度测试(上千条qlc,sim和链状图的查询)和高覆盖性测试(十万条随机指令数据)。另外,针对一些易错的边界情况,通过读JML代码,构造了一些边界数据,例如Group中1111人以上的情况。
三、bug和hack情况
3.1 自身bug
我在第九次和第十一次公测和互测均未出现bug,但在第十次作业出现了bug,在sendMessage
方法的异常处理之前就删除了message。究其原因是因为自己的随机数据覆盖性不够导致的,在第十一次作业中着重对这一点进行了修复。
3.2 hack情况
使用链状图的qlc成功hack到TLE,其余bug还有几个,但由于我使用大量的随机数据进行hack,具体的bug发生点无法明确。
四、Network扩展
假设出现了几种不同的Person
Advertiser:持续向外发送产品广告
Producer:产品生产商,通过Advertiser来销售产品
Customer:消费者,会关注广告并选择和自己偏好匹配的产品来购买 -- 所谓购买,就是直接通过Advertiser给相应Producer发一个购买消息
Person:吃瓜群众,不发广告,不买东西,不卖东西
如此Network可以支持市场营销,并能查询某种商品的销售额和销售路径等 请讨论如何对Network扩展,给出相关接口方法,并选择3个核心业务功能的接口方法撰写JML规格(借鉴所总结的JML规格模式)
将Advertiser
, Producer
, Customer
均作为Person
的子类。增加AdvertiseMessage
,saleMessage
, purchaseMessage
作为Message
的子类。
public interface Network {
/*...
@ public instance model non_null int[] productIdList;
@ public instance model non_null int[] productHeatList; 销量
@*/
...
...
}
//Network.java
/*@ public normal_behavior
@ requires containsMessage(id) && (getMessage(id) instance of AdvertiseMessage) &&
@ getMessage(id).getType() == 0 &&
@ getMessage(id).getPerson1().isLinked(getMessage(id).getPerson2()) &&
@ getMessage(id).getPerson1() != getMessage(id).getPerson2();
@ assignable messages;
@ assignable getMessage(id).getPerson1().socialValue;
@ assignable getMessage(id).getPerson2().messages, getMessage(id).getPerson2().socialValue,
@ assignable getMessage(id).getPerson2().ads;
@ ensures !containsMessage(id) && messages.length == \old(messages.length) - 1 &&
@ (\forall int i; 0 <= i && i < \old(messages.length) && \old(messages[i].getId()) != id;
@ (\exists int j; 0 <= j && j < messages.length; messages[j].equals(\old(messages[i]))));
@ ensures \old(getMessage(id)).getPerson1().getSocialValue() ==
@ \old(getMessage(id).getPerson1().getSocialValue()) +
@ \old(getMessage(id)).getSocialValue() &&
@ \old(getMessage(id)).getPerson2().getSocialValue() ==
@ \old(getMessage(id).getPerson2().getSocialValue()) + \old(getMessage(id)).getSocialValue();
@ ensures (\forall int i; 0 <= i && i < \old(getMessage(id).getPerson2().getMessages().size());
@ \old(getMessage(id)).getPerson2().getMessages().get(i+1) ==
@ \old(getMessage(id).getPerson2().getMessages().get(i)));
@ ensures \old(getMessage(id)).getPerson2().getMessages().get(0).equals(\old(getMessage(id)));
@ ensures \old(getMessage(id)).getPerson2().getMessages().size() ==
@ \old(getMessage(id).getPerson2().getMessages().size()) + 1;
@ ensures \old(getMessage(id)).getPerson2().containsAd(id);
@ ensures !\old(getMessage(id).getPerson2().containsAd()) ==>
@ (\old(getMessage(id)).getPerson2().ads.size() ==
@ \old(getMessage(id).getPerson2().ads.size()) + 1);
@ also
@ public normal_behavior
@ requires containsMessage(id) && (getMessage(id) instance of AdvertiseMessage) &&
@ getMessage(id).getType() == 1 &&
@ getMessage(id).getGroup().hasPerson(getMessage(id).getPerson1()) &&
@ getMessage(id).getPerson1() != getMessage(id).getPerson2();
@ assignable people[*].socialValue, people[*].ads;
@ ensures !containsMessage(id) && messages.length == \old(messages.length) - 1 &&
@ (\forall int i; 0 <= i && i < \old(messages.length) && \old(messages[i].getId()) != id;
@ (\exists int j; 0 <= j && j < messages.length; messages[j].equals(\old(messages[i]))));
@ ensures (\forall Person p; \old(getMessage(id)).getGroup().hasPerson(p); p.getSocialValue() ==
@ \old(p.getSocialValue()) + \old(getMessage(id)).getSocialValue());
@ ensures (\forall int i; 0 <= i && i < people.length &&
@ !\old(getMessage(id)).getGroup().hasPerson(people[i]);
@ \old(people[i].getSocialValue()) == people[i].getSocialValue());
@ ensures (\forall int i; 0 <= i && i < people.length &&
@ \old(getMessage(id)).getGroup().hasPerson(people[i]); people[i].containsAd(id));
@ ensures (\forall int i; 0 <= i && i < people.length &&
@ \old(getMessage(id)).getGroup().hasPerson(people[i]); !\old(people[i].containsAd(id)) ==>
@ people[i].ads.size() == \old(people[i].ads.size()) + 1);
@ also
@ public exceptional_behavior
@ signals (MessageIdNotFoundException e) !containsMessage(id);
@ signals (RelationNotFoundException e) containsMessage(id) && getMessage(id).getType() == 0 &&
@ !(getMessage(id).getPerson1().isLinked(getMessage(id).getPerson2()));
@ signals (PersonIdNotFoundException e) containsMessage(id) && getMessage(id).getType() == 1 &&
@ !(getMessage(id).getGroup().hasPerson(getMessage(id).getPerson1()));
@ signals (InvalidMessageTypeException e) !(getMessage(id) instance of AdvertiseMessage);
@*/
public void sendAdvertisement(int id) throws RelationNotFoundException, MessageIdNotFoundException, PersonIdNotFoundException, InvalidMessageTypeException;
//Network.java
/*@ public normal_behavior
@ requires getPerson(customerId).containsAd(getPerson(producerId).getProductId());
@ assignable getPerson(producerId).money, getPerson(customerId).money;
@ ensures getPerson(producerId).money == \old(getPerson(producerId).money) +
@ getPerson(producerId).getProductPrice();
@ ensures getPerson(customerId).money == \old(getPerson(customerId).money) -
@ getPerson(producerId).getProductPrice();
@ ensures (\forall int i; 0 <= i && i < productIdList.length;
@ productIdList[i] == getPerson(producerId).getProductId() &&
@ productHeatList[i] == \old(productHeatList[i]) + 1);
@ also
@ public exceptional_behavior
@ signals (InvalidPersonTypeException e) !(getPerson(producerId) instanceof Producer) ||
@ !(getPerson(customerId) instanceof Customer);
@ signals (AdvertisementNotFoundException e)
@ !getPerson(customerId).containsAd(getPerson(producerId).getProductId());
@*/
public void purchase(int producerId, int customerId) throws AdvertisementNotFoundException, InvalidPersonTypeException;
//Network.java
/*@ public normal_behavior
@ requires containsProduct(id);
@ ensures (\exists int i; 0 <= i && i < productIdList.length; productIdList[i] == id &&
@ \result == productHeatList[i]);
@ also
@ public exceptional_behavior
@ signals (ProductIdNotFoundEXception e) !containsProduct(id);
@*/
public /*@ pure @*/ int queryProductHeat(int id) throws ProductIdNotFoundEXception;
五、心得体会
本单元虽然编程实现的难度不大,但非常考察我们编程时的的细致和debug时的测试能力。稍有不慎很有可能理解偏导致出现正确性问题,进而强测大量失分。
JML给我的观感就是虽然一眼看去冗长繁琐不易读,但耐下心仔细看可以发现JML的描述非常严谨非常清楚,无二义性。