2022-面向对象设计与构造-第三单元总结

2022-面向对象设计与构造-第三单元总结

架构设计

第九次作业

本次作业中图模型相关的方法主要是isCirclequeryBlockSum,二者都是依赖并查集优化。在并查集的实现上,我采用了路径压缩和按秩合并的策略,有效得提高了其查找的性能,尤其是对环形图的查找,可以从O(n)变为O(1)。同时为了避免MyNetwork复杂度过高,我单独建立了一个UnionFindSet类和Unit类来封装并查集,其中Unit类很简单,就是将节点的父亲和秩封装在了一起;UnionFindSet通过一个HashMap<Integer, Unit>将id映射到对应的Unit节点来维护必要的信息,同时其还有一个blockSize字段,用来给出queryBlock的结果,这个字段在新加入元素时加一,在执行union时若两个元素原来不是一组则减一。

在维护策略上,主要是有以下几个方法做了优化

  • isCirclequeryBlockSum:采用路径压缩的并查集维护,时间复杂度O(1)(路径压缩会导致少量的非O(1)情形)
  • hasPerson, contains, getPerson, getGroup, queryValue等:采用hashMap存储相应的数据,使得能在O(1)时间内完成
  • getValueSum:在addPersondelPerson是遍历当前的所有组员,维护一个valueSum,使得getValueSum时间复杂度从O(n2)变为O(1),但代价addPersondelPerson时间复杂度从O(1)变为O(n),这是值得的
  • getAgeMean:维护一个ageSum,调用该方法时根据 ageMean=ageSum/sizeO(1)内给出结果
  • getAgeVar:维护一个年龄平方和ageSquareSum,调用该方法时根据 ageVar=(ageSquareSum+sizeageMean22ageMeanageSum)/sizeO(1)内给出结果

第十次作业

本次作业中主要增加了一个最小生成树的方法queryLeastConnection,我是使用的最小生成树算法是Kruskal算法,主要是因为已经实现了路径压缩的并查集,而且java标准库中有PriorityQueue,实现一个堆优化的Kruskal算法很简单,时间复杂度为Elog2E,效率很高。通过一个打竞赛的室友得知可以动态维护最小生成树,这样可以在查询很多时进一步提高小效率,但是考虑到作业中对查询次数限制较严格,但是对修改次数限制宽松了很多,这样不仅会大大增加作业难度,而且还可能导致效率不增反降,因此没有采用这个方法。同时还注意到可以通过设计标志位记录一些已经算出过的数据,这样能在连续询问同一联通分支时只计算一次,大大提高效率,但问题是我们可以在每次查询中插入修改来使该方法完全失效,同时不使用该方法手动构造的用时最长的数据CPU时间也不超过2s,感觉没必要,也没有采用这个方法。在没有遇到性能问题时最好不要去考虑性能问题,优先去保证其功能的正确性。

此外,我认为官方包中给的Person可以用来当做图中的点和边,但是由于没有方便的接口进行操作,如果新增方法的话又需要向上转型,因此我新建了Edge类用于存放边,主要用于最小生成树方法。同时,本次作业中我对并查集做了优化,通过阅读别人的代码,我发现我第九次作业的并查集结构结构比较复杂,

同时还有一个小细节,在Person中有对Message的相关操作,仔细观察可以发现所有的操作都是在头尾进行的,不存在向中间的操作,因此一个好的做法是使用LinkedList,这样可以在常数时间内完成对头尾的插入删除。

第十一次作业

本次作业中主要增加了一个计算最短路径的方法sendIndirectMessage,我是使用的是PriorityQueue优化的Dijkstra算法,时间复杂度为Elog2E。和上次作业类似,我没有用官方包中的Person对图进行建模,而是使用了自己定义的NodeDot对图进行建模,其中Node是一个点,拥有一个HashMap<Integer, Integer>存放与其相邻的点和对应边的权值;Dot是也是一个点,主要用于在Dijkstra算法中记录当前点的distance,之所以用两个类是因为Node是用来永久存放图信息并会更新的;Dot只是在计算最短路径时使用,为不可变对象。同上次作业一样,我想到了记录已经算出来的最短路径来提高连续询问时的性能,但是没有遇到性能问题,因此没有实现该功能。

性能问题和修复情况

三次作业中我都没有遇到性能问题,但是这里我想说一下我第一次作业中遇到的一个和性能相关的小bug。

对于方法getValueSum,最简单的做法是两遍循环,时间复杂度为O(n2),可能TLE。为了减低时间复杂度,我最开始的维护策略是给每个人维护一个valueSum,然后再GroupgetValueSum中,对所有人的valueSum求和,因此gteValueSum的复杂度是O(n),同时Group中的addPersondePerson方法的复杂度是O(1),这样的复杂度肯定不会TLE。但是后来发现JML规格中要求对组内所有的边求和,我这种方式求的还有组外的,这就是我测试过程中发现的一个小bug。思考组内的VlaueSum何时会改变,发现只有在addRelationGroup.delPerson, Group.addPerson中会改变,我就改成了组内维护一个ValuseSum,在addRelation时遍历一遍groups,看这两个人是否都在这一组,如果都在,就增加改组的ValueSum;在Group.addPerson, Group.delPerson时遍历一遍组内当前的人数,看是否有和新增/删的人相连的人,如果有的话就对应地维护ValueSum。这样getValueSum的复杂度就是O(1),而Group.addPerson, Group.delPerson的复杂度是O(n)addRelation复杂度也有所增加,但是考虑到实际上group可能不多,可以忽略。

测试设计

初次尝试Junit

本单元我首次尝试了Junit测试,但是感觉用在本单元挺鸡肋的,主要是我个人觉得(限于我对Junit的了解)它和测评机比唯二的优点是不调试就知道是哪个方法出了问题和能统计分支覆盖情况,但是在这个单元中每条指令几乎和每个方法时一一对应的本身就可以快速定位到出问题的方法,而分支覆盖问题我用测评机也能较好地解决,这就导致使用测评机相对使用Junit是几乎是完全占优策,如果放到第一、四单元Junit应该能突出其优势。因此本单元我主要的测试方案还是测评机自动测试,正确性判定是通过多人对拍。

测评机数据生成策略

本次作业自动生成数据较为简单,用随机数按照格式生成就好了。但是如果完全随机地生成会产生很多无效的id,导致正常分支覆盖率低,但是正常分支往往又是bug出现较为集中的地方。未解决这个问题,提高正常分支的覆盖率,我的室友陈正昊提出了一个比较好的方案:就是在数据生成器中记录一些必要的信息,在生成id时,根据id可能的关系进行分类,然后依概率进入每种类型,这样就能很大程度上控制数据进入正常分支还是异常分支,具体的做法如下

在生成id时,我们可能遇到需要单个id的情况,这时我们就看是需要personId还是groupId,我们以一定的概率从当前的personIdgroupId集合中取一个id,这样取出的id就是有效的,能够进入正常分支。我们还可能遇到需要取两个id的情形,这又分两种情况,一种情况是两个personId和一个personId一个groupId,这两种情况中又有如下的子情况

在取出两个personId时(记为这两个id对应的对象为person1person2),有四种子情况

  • person1person2都不存在
  • person1person2只有一个存在
  • person1person2都存在,且不邻接
  • person1person2都存在,且邻接

在取出一个personId和一个groupId时(记两个id对应的对象为persongroup),有五种子情况

  • person不存在,group不存在
  • person不存在,group存在
  • person存在,group不存在
  • person存在,group存在,persongroup里面
  • person存在,group存在,person不在在group里面

在这些子情况基本能覆盖所有的分支(正常分支和异常分支),两种大情况的后两种子情况对应的是正常分支,我们可以通过随机数控制进入每种分支的概率,然后根据维护的信息生成对应子情况的数据。

完成了这样的数据生成器后,我通过在方法的不同分支中增加特定输出,简单地测试了一下各个分支的覆盖率,当每次数据量为10万条时,每次都能覆盖到所有的分支,说明该策略有效。

针对新的需求给出JML规格

新需求

出现了几种不同的Person

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

如此Network可以支持市场营销,并能查询某种商品的销售额和销售路径等

对新需求的讨论

首先,我们对新增的三类Person建模,他们都继承自Person类,新增内容如下:

  • Advertiser:新增producer字段,记录和他合作的Producer,以便发送相应的广告并给Producer发送购买消息;新增salesVolume字段,记录销售额
  • Producer:新增produceSize字段,记录当前产品个数;新增productType字段,用来表示生产的产品的种类,并记录产品的价格、成本等信息
  • Custom:新增preference字段,用来表示偏好的产品;新增product字段,用来表示购买的产品

同时,我们还应新增Advertisement类并继承自Message,用来表示一个广告消息,要有包含相应的advertise, producer, custon, money, product, selelCount等(其中saleCount字段表示有多少人通过该广告购买了该产品);需要新增Product类,用来表示产品。

我认为这是最基础的情况,我们还可以进一步考虑复杂情况,如:Advertise可以根据不同的Custom(比如老主顾可以给点儿优惠啥的)和不同的产品给出不同的价格,Custom可以选择从更便宜的Advertise处购买,Producer可以给出对Advertiser激励方案(博弈与社会乱入),来点儿618促销等等。

方法先关的,首先AdvertiseraddAdvertisement方法用于投放广告,有cooperate方法用于和特定的Producer绑定合作关系,有querySaleVolume获取销售额;ProducersaleProduct方法用于销售产品,有cooperate方法用于和特定的Advertiser绑定合作关系;CustomdelUselessAdvertisement方法删除收到的广告中无关自己偏好商品的广告,有buy方法选择特定的广告购买商品等等。

部分方法的JML规格

Advertiser会根据产品和生产商投放相应的广告,对应的方法addAdvertisement方法规格如下

/*
 @ public normal_behavior
 @ requires contains(advertiserId) && contains(producerId)
 @		&& containsProduct(productId) && !containsMessage(advertisementId) &&
 @		isCooperated(advertiserId, producerId) && 
 @		!(\exists int i; 0 <= i && i < producers.length; producers[i].getId() == producerId &&
 @		!producers[i].haveProduct(productId));
 @ assignable messages;
 @ ensures messages.length == \old(messages.length) + 1;
 @ ensures (\forall int i; 0 <= i && i < \old(messages.length);
 @		(\exists int j; 0 <= j && j < messages.length; messages[j].equals(\old(messages[i]))));
 @ ensures (\exists int i; 0 <= i && i < messages.length; messages[i].getAdvertise.getId() == advertiserId
 @ 		&& messages[i].getProducer.getId() == producerId && messages[i].getProduct.getId() == productId
 @		&& messages[i].geMoney() == money && messages[i].getSaleCount() == 0));
 @ ensures
 @ public exceptional_behavior
 @ signals (PersonNotFoundException e) !contains(advertiserId) || !contains(producerId);
 @ also
 @ public exceptional_behavior
 @ signals (ProductNotFoundExcrption e) contains(advertiserId) && contains(producerId) && 
 @		!containsProduct(productId);
 @ also
 @ public exceptional_behavior
 @ signals (EqualMessageIdException e) contains(advertiserId) && contains(producerId) && 
 @		containsProduct(productId) && containsMessage(advertisementId);
 @ also
 @ public exceptional_behavior
 @ signals (NotCooperatedException e) contains(advertiserId) && contains(producerId) && 
 @		containsProduct(productId) && !containsMessage(advertisementId) && 
 @		!isCooperated(advertiserId, producerId);
 @ also
 @ public exceptional_behavior
 @ signals (ProducerNotHaveProductException e) contains(advertiserId) && contains(producerId)
 @		&& containsProduct(productId) && !containsMessage(advertisementId) &&
 @		isCooperated(advertiserId, producerId) && 
 @		(\exists int i; 0 <= i && i < producers.length; producers[i].getId() == producerId &&
 @		!producers[i].haveProduct(productId));
 */
public void addAdvertisement(int advertiserId, int producerId, int productId, int advertisementId, int money) throw PersonNotFoundException, ProductNotFoundExcrption, NotCooperatedException, ProducerNotHaveProductException, EqualMessageIdException;

Advertiser有对应的销售额,可以通过querySalesVolume方法询问其销售额,规格如下

/*
 @ public normal_behavior
 @ requires contains(advertiserId);
 @ assignable \nothing;
 @ ensures (\exists int i; 0 <= i && i < people.length; people[i].getId() == id && \results = people[i].getSalesVolume());
 @ public exceptional_behavior
 @ signals (PersonNotFoundException e) !contains(advertiserId);
 */
public /*@ pure @*/ int querySalesVolume(int advertiserId) throw PersonNotFoundException;

Customer可以根据自己的偏好过滤掉自己不感兴趣的广告,对应方法delUselessAdvertisement规格如下

/*
 @ public normal_behavior
 @ requires contains(customerId);
 @ assignable getPerson(customerId).messages;
 @ ensures (\exists int i; 0 <= i && i < getPerson(customerId).messages.length; 
 @		getPerson(customerId).messages[i] instanceof Advertisement &&
 @		getPerson(customerId).isInterestedIn(\old(getPerson(customerId).messages[i]).getProduct()));
 @ ensures (\exists int i; 0 <= i && i < getPerson(customerId).messages.length; 
 @		!(getPerson(customerId).messages[i] instanceof Advertisement));
 @ ensures !(\exists int i; 0 <= i && i < getPerson(customerId).messages.length; 
 @		getPerson(customerId).messages[i] instanceof Advertisement &&
 @		!getPerson(customerId).isInterestedIn(\old(getPerson(customerId).messages[i]).getProduct()));
 @ public exceptional_behavior
 @ signals (PersonNotFoundException e) !contains(customerId);
 */
public void delUselessAdvertisement(int customerId) throw PersonNotFoundException;

心得体会

  • 通过本单元,我学会了理解基础的JML语言,并能根据规范写出符合要求的代码。我认为用JML语言来描述方法的规格优点在于严谨,不会出现谜语人的问题(OS上机就经常谜语人),但是缺点在于相对于自然语言要更难阅读和理解,又时会遇到自然语言一句话能说清的问题,JML要写很多很多。同时JML的另一大优点在于便于进行单元测试和形式化验证、机器检验,现在也有一些能够根据JML规格自动测试方法的工具,但是很不成熟,用起来很鸡肋。现在随着程序越来越大,对正确性要求越来越高,我认为未来JML语言会不断发展完善,希望它能成为一个为程序正确性保驾护航的工具,而不是成为一个华而不实,给程序员增加负担的语言。
  • 本单元我回顾了一些在大一学习的算法和数据结构,如并查集、最小生成树算法、最短路径算法等,领略了算法和数据结构的魅力。同时,通过使用java自带的工具类,我很方便地完成了对部分算法的优化,熟悉了一些新的工具类,惊叹于面向对象、泛型等机制的精妙、方便。
  • 本单元对程序的性能做了限制,促使我去思考如何动态维护一些值来加速特定的查询过程,其实动态维护的方法并不难,但是如果没有这个限制我很可能直接按照JML描述暴力求解,由此我认识到不应该直接按照JML去写方法,而是建立在理解规格的含义之后再去写方法。写程序也是,不能想到什么些什么,要先有一个整体的把控,想好了在深入到细节去具体实现,这样往往能起到事半功倍的效果(这样的工程化方法从计算机组成课程就开始强调了)。
  • 本单元重点训练的是根据JML写出符合规范的代码,但是对编写JML训练很少,我认为JML的这种思维很重要:通过前置条件、后置条件、约束条件等编写描述方法,在测试时可以直接根据这些前置条件、后置条件、约束条件进行检查,十分有利于单元测试。我们在进行单元测试时,也可以考虑方法的这些写出条件,然后给出相应的检查函数用来检查。
posted @   xjh_buaa  阅读(55)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 地球OL攻略 —— 某应届生求职总结
· 提示词工程——AI应用必不可少的技术
· Open-Sora 2.0 重磅开源!
· 周边上新:园子的第一款马克杯温暖上架
点击右上角即可分享
微信分享提示