BUAA-OO-Unit3-CommunicationNetwork-Summary
第三单元作业总结--JML与社交网络实现
关于基于JML测试
本单元指导书中推荐的测试方法是进行JUnit测试。在简单尝试后,我感觉JUnit测试能够正常运作的前提是需要构造出合适的测试样例,并且根据官方给出的JML,自己用assert等方式设定正确结果,也就是说使用JUnit测试的前提依然是正确理解JML。既然势必要自行构造测试样例,并且无法通过JUnit判断自己对于官方给出的JML的理解,我最终还是没采用JUnit的测试方式,而是通过传统的编写随机生成数据脚本+搭建评测机和小伙伴对拍的方式实现的测试。
数据生成和测试方面,我和小伙伴合作编写了数据生成脚本和评测机,并进行了对拍。
第一次作业中,我主要完成了第一次作业中所有指令的有策略的随机生成部分,可以实现调整生成异常指令的概率,并且在每次更改信息后,通过生成查询指令对状态进行查询。
例如
def addPersonPackaged(canErr): #canErr参数控制此次生成指令是否可以异常
ifErr = random.randint(0,100)%errHappenRate == 0 #errHappenRate参数控制异常发生的概率
if(ifErr and canErr and len(personIdList) > 0):
id = equalPersonIdErr()
add_person(id, random.choice(nameList), random.randint(0, 201))
else:
id = personIdNoFoundErr()
add_person(id, random.choice(nameList), random.randint(0, 201))
personIdList.append(id)
第二次作业中,我主要完成了与Message和Group相关的指令的随机生成
第三次作业中,由于新增指令较多,新增的message类型较多,由于时间原因,我列出了测试要点,手动构造测试数据进行测试。
关于架构
本次的架构比较简单,就是按照官方给出的接口实现了要求我们实现的类和异常类。除此之外,为了便于实现图的各种算法,我额外实现了一个并查集类和边节点类。具体的查找最短路,最小生成树的算法部分,由于比较简单,并且没有重复利用,于是我直接在MyNetWork
里实现了。
关于图模型的维护和构建,我在储存数据时均采用id->Object
的HashMap
来储存,这样可以比较快的获取目标元素。至于具体到每个类储存什么信息,则和官方给出的JML保持一致即可。
关于算法性能以及bug修复
第一次作业
性能分析
第一次作业中,时间复杂度可能较高的方法是查询两人是否处于同一联通分支的isCircle
方法。我采用了并查集来保证时间复杂度不超出限制。并且增加了路径压缩优化和按秩合并优化,使得复杂度降低到\(O(1+log*n)\)
如果没有实现并查集,那么另一个比较危险的指令则是query block sum
。在实现了并查集的前提下,可以维护一个记录当前联通分支数量的变量,在每次对并查集进行merge操作时减少1,在进行add操作时增加1,即可实现\(O(1)\)复杂度的查询。
最后一个问题是,如果使用了递归方法来维护并查集,那么在面对形如
1 -> 2 -> 3 ... -> n
这样的超长链时,在链长大约5000时会爆栈,因此需要用循环代替递归来维护并查集
bug分析
这次作业没有发现bug,互测房内也没有人成功刀中
第二次作业
性能分析
第二次作业中,时间复杂度可能较高的方法为查询一个group中的总value值、查询group年龄方差和求最小生成树的方法。如果不增加缓存,那么query group value sum
指令带来的时间复杂度可以达到\(O(n^2)\)(n代表group中的人数),这是非常危险的,因此我在group中增加了一个value sum
的属性,在每次增加人、减少人和增加关系的时候更新,这样在查询时就能实现\(O(1)\)复杂度,在其他时候均摊\(O(n)\)复杂度。
对于query group age var
指令,我在group中维护一个ageSum
属性,在每次增加和减少人员时更新,保存上一次算出的ageVar
,并且设置一个类似脏位的属性,在每次有人员变动时置1,若脏位为0,则直接返回上次的计算结果,脏位为1时再重新\(O(n)\)计算年龄的平均值。
对于求最小生成树,我使用了Prim算法,并且在判断某条边链接的两点是否已经处于同一个联通分支时,沿用了第一次作业中实现的并查集算法。我将第一次作业中的并查集算法包装成了一个新的工具类,便于复用。
bug分析
本次作业没有在中测、强测、互测中被发现bug,但是在自测的时候发现了bug。在维护group value sum
的时候,在add relation
时没有更新group value sum
。惊险的是,和我对拍的小伙伴也出现了这个问题,于是这个bug并没有在自己搭建的评测机中测出TAT,而是我在快到作业截止时间时,心血来潮浏览学长的总结博客时发现的。这也体现出对拍的一个缺点。多找些人对拍即可解决(bushi)
第三次作业
性能分析
第三次作业中可能比较耗时的指令是send indirect message
指令,采用了堆优化的Dijkstra算法实现,复杂度为\(O(nlogn)\),在对于其他指令的处理上,时间复杂度不是大问题,反而在正确性上因为细节较多,比较容易出错。
bug分析
本次作业在自测、中测、强测、互测中均没有被发现bug。由于这次作业没有实现随机数据生成,因此手头的数据不多,没有hack出本房间的两个正确性bug。或许是因为偷懒看到成功率不高就没测
关于PPT中的Network拓展
假设出现了几种不同的Person
- Advertiser:持续向外发送产品广告
- Producer:产品生产商,通过Advertiser来销售产品
- Customer:消费者,会关注广告并选择和自己偏好匹配的产品来购买 -- 所谓购买,就是直接通过Advertiser给相应Producer发一个购买消息
- Person:吃瓜群众,不发广告,不买东西,不卖东西
如此Network可以支持市场营销,并能查询某种商品的销售额和销售路径等 请讨论如何对Network扩展,给出相关接口方法,并选择3个核心业务功能的接口方法撰写JML规格(借鉴所总结的JML规格模式)
-
新增Person的子类:
Advertiser
:包含属性:商品列表Producer
:包含属性:可生产的产品列表Customer
:包含属性:偏好的产品列表
-
新增Message的子类:
AdvertiseMessage
:包含信息:商品信息,销售者ProductMessage
:包含信息:商品信息,生产者,广告者PurchaseMessage
:包含信息:商品信息,购买者,广告者
-
新增Product类,属性包括:商品的id、名称,价格,销售额,销售路径、生产者
-
新增核心功能的接口方法:
-
生产产品
produce
:- 生产者生产一个产品,加入自身的产品列表中
- 生产者
Producer
向 广告者Advertiser
发送一个ProductMessage
,包含的信息有产品信息,消息发送者,消息接收者
-
发送广告
advertise
- Advertiser向所有与之有关的Person发送
AdvertiseMessage
,并且向所有包含此Advertiser的群发送AdvertiseMessage
。
- Advertiser向所有与之有关的Person发送
-
购买产品Purchase
- 可包含在
sendMessage
中 Costomer
向关联的Advertiser
发送购买信息,并将money
转给Producer
发送广告的JML
- 可包含在
-
// Network.java
/*@ public normal_behavior
@ requires contains(advertiserId) && (getPerson(advertiserId) instanceof Advertiser);
@ requires containsMessage(MessageId)
@ assignable people[*].messages;
@ ensures (\forall int i; 0 <= i && i <= people.length &&
@ people[i].isLinked(getPerson(advertiserId)) && people[i].getId() != advertiserId;
@ \old(people[i].messsages.length) == people[i].messsages.length - 1);
@ ensures (\forall int i; 0 <= i && i <= people.length &&
@ people[i].isLinked(getPerson(advertiserId)) && people[i].getId() != advertiserId;
@ (\forall int j; 0 <= j && j <= \old(people[i].messages.length);
@ (\exists int k; 0 <= k && k <= people[i].messages.length;
@ people[i].messages[j] == people[i].messages[k])));
@ ensures (\forall int i; 0 <= i && i <= people.length &&
@ people[i].isLinked(getPerson(advertiserId)) && people[i].getId() != advertiserId;
@ (\forall int j; 0 <= j && j <= getPerson(advertiserId).messages.length;
@ (\exists int k; 0 <= k && k <= people[i].messages.length;
@ getPerson(advertiserId).messages[j] == people[i].messages[k])));
@ ensures (\forall int i; 0 <= i && i <= people.length &&
@ people[i].isLinked(getPerson(advertiserId)) && people[i].getId() != advertiserId;
@ (\forall int j; 0 <= j && j <= getPerson(advertiserId).messages.length;
@ (\exists int k; 0 <= k && k <= people[i].messages.length;
@ getPerson(advertiserId).messages[j] == people[i].messages[k])));
@ ensures ( (\forall int i; 0 <= i && i <= people.length &&
@ people[i].isLinked(getPerson(advertiserId)) && people[i].getId() != advertiserId;
@ (\exists int j; 0 <= j && j <= people[i].messages.length;
@ getPerson(advertiserId).messages[j] == \old(getMessage(messageId))));)
@ also
@ public exceptional_behavior
@ signals (PersonIdNotFoundException e) !contains(advertiserId);
@ signals (AdvertiserIdNotFoundException e) contains(advertiserId) &&
@ !(advertiserId) instanceof Advertiser);
@ signals (MessageNotFoundException e) !containsMessage(messageId);
@*/
public void advertise(int advertiserId, int MessageId) throw PersonIdNotFoundException, AdvertiserIdNotFoundException, MessageIdNotFoundExceprion;
生产产品的JML:
/*@ public normal_behavior
@ requires contains(producerId) && (getPerson(producerId) instanceof Producer);
@ assignable getProducer(producerId).product;
@ ensures getProducer(producerId).product.length ==
@ \old(getProducer(producerId).product.length) + 1;
@ ensures for(int i; 0 <= i && i <= getProducer(producerId).product.length; \exist(getProducer(producerId).product[i] == product))
@ also
@ public exceptional_behavior
@ signals (PersonIdNotFoundException e) !contains(producerId);
@ signals (ClassTypeException e) !(getPerson(producerId) instanceof Producer);
@*/
public void produce(int producerId, Product product);
购买产品的JML
//仅包含sendMessage的新增功能
/* @ ensures (\old(getMessage(id)) instanceof PurchaseMessage) ==>
@ (\old(getMessage(id)).getPerson1().getMoney() ==
@ \old(getMessage(id).getPerson1().getMoney()) - ((PurchaseMessage)\old(getMessage(id))).getMoney() &&
@ \old(getMessage(id)).getPerson2().getMoney() ==
@ \old(getMessage(id).getPerson2().getMoney()) + ((PurchaseMessage)\old(getMessage(id))).getMoney());
@ ensures (!(\old(getMessage(id)) instanceof PurchaseMessage) && !(\old(getMessage(id)) instanceof RedEnvelopeMessage) ) ==> (\not_assigned(people[*].money));
@*/
public void void sendMessage(int id) throws
RelationNotFoundException, MessageIdNotFoundException, PersonIdNotFoundException;
心得体会
本次作业极大程度的磨练了我的耐心。这次作业的实现细节繁多,需要足够的细心才能准确的完成,因此我不得不耐下心来,一行一行的阅读满屏的JML,虽然有时可以通过方法名猜测这个方法的功能,但是最终在检查时,还是需要不跳过的读完每一行,才能检查出有没有问题。
这也是我体会到的JML的优点,也就是传递设计要求的准确性,如果课程组不使用JML来传递设计细节,而是用自然语言来描述每个方法的功能,那一定会有很多没有传达清楚的地方。JML虽然细节多,比较长,但是它精准无误的传达了所有的设计要求,助教们回答同学们的问题时,大多可以直接回复:请参照JML。
至此全部互测已经结束,OO之旅正在接近尾声,希望在最后一个单元中也能够顺利完成任务!
特别鸣谢一起完成评测机的小伙伴:王钧石同学