面向对象设计与构造第三单元总结博客

面向对象设计与构造第三单元总结博客

1 作业概述

第三单元作业的主要内容是根据给出的JML规格和接口定义实现一个社交系统,主要的功能包括社交关系的模拟与查询、群组功能、不同类型消息的接收与发送等。

相较于前两个单元,本单元的作业由于给出了JML规格,因此在设计上的难度相对较小,但如果只是将JML”翻译“成代码也是无法高质量完成作业的。个人认为本单元需要重点关注的几个点包括:确保实现的社交系统严格遵守给定的JML规格,合理的网络结构设计,使用时间复杂度较低的算法等。

2 架构设计与图模型

由于本单元作业需要严格遵守JML规格,因此在架构设计上并没有特别多的发挥余地,按要求在官方接口的基础上实现MyNetwork,MyGroup,MyPerson等几个类即可。下面重点讨论图模型的构建与维护。

不难发现本单元作业的核心是构建一个无向图模型,其中节点为人,边为人与人之间的关系,群聊则可以看作是子图,作业中绝大多数的操作都在在这个图上进行。考虑到本单元作业对性能有一定的要求,因此在实现图模型时大量使用了HashMap来进行存储,从而提高查询的性能,用空间换时间。

每次作业中,部分旧的设计可能难以满足新的需求,因此在每次迭代开发中图模型的构建与维护都有所变化。下面对每次作业中的图模型设计进行解释。需要注意的是,这一部分只讨论基本的图模型的构建,对于一些用于部分指令的设计将在下一部分中进行详细的讨论。

第一次作业

人:在MyNetwork类中用HashMap<Integer, Person>来存储,其中key是Person的id;

关系:在MyPerson类中用HashMap<Person, Integer>来存储,其中key是与该Person邻接的其他Person,value是这两个Person之间边的权值。为了确保在对一个Person的边进行查询时不会漏掉某些边,对于每一条边,在其连接的两个Person中都进行了相应的存储;

群聊:在MyNetwork类中用HashMap<Integer, Group>来存储,其中key是Group的id,每个群聊内,用HashSet<Person>存放该群聊内的全部Person。

第二次作业

人、群聊:与第一次作业相同;

关系:之前的设计难以满足本次作业的需求,因此本次作业中新增了一个Edge类用来表示边,在MyNetwork类中使用HashMap<Integer, ArrayList<Edge>>来存储每个Person对应的边,其中key是该Person的id,value是全部连接该Person的边组成的ArrayList。对于同一条边,依然在两个Person中均进行存储;

消息:在MyNetwork类中用HashMap<Integer, Message>来存储,其中key是Message的id。在MyPerson类中,用ArrayList<Message>来存储该Person收到的消息。

第三次作业

人、关系、群聊:与前两次作业相同;

EmojiMessage:在NetWork类中使用HashMap<Integer, Integer>来存储,其中key是该Emoji的id,value是其使用次数。

在图模型的维护上,只需按照JML规定,对相应的容器增删操作即可。

3 性能优化

本单元的作业对性能有一定要求,因此在部分功能的实现上需要选用合理的算法与数据结构。由于用到的算法都是图论中的经典基础算法,个人认为没必要给出算法的具体细节,因此这里重点解释如何将算法运用到作业当中。

并查集

对于第一次作业中的 query_circle 和 query_block_sum 两条指令,如果直接用bfs或dfs暴力遍历的话时间复杂度会达到 O(|V|+|E|),性能较差,因此选择使用并查集进行优化。qci指令用于判断两个 Person 是否联通,qbs 指令求解连通块的数量,对于这两种情况我们其实并不需要知道连通块中边的细节,只需确定 Person 之间的连通性即可,因此我使用了路径压缩来进一步提升性能。

构建与维护

MyNetwork类中,使用HashMap<Integer, Integer> root来保存并查集的结果,其中 key 是 Person的 id,value 是该 Person 在并查集中的根节点。

当加入一个新的 Person 时,向 root 中加入其对应的新的元素,其中由于此时没有连接该 Person 的边,因此 key 和 value 均为该 Person 的 id。当加入新的关系时,按照并查集算法进行路径压缩与合并。时间复杂度为 O(nα(n))。

query_circle

对于 qci 指令,考虑到同一连通块内节点在并查集中的根节点一定相同,因此只需判断两个 Person 对应的根节点是否相同即可,可以用 O(1) 的复杂度完成查询。

query_block_sum

对于 qbs 指令,考虑到并查集内的根节点与连通块一一对应的关系,因此只需计算出并查集中的根节点数目即可。在我的实现中,由于第一次作业并没有单独存储联通块,因此需要遍历一遍所有节点,并记录根节点是其自身的节点的数目,即为qbs的结果,可以用 O(n) 的复杂度完成查询。

最小生成树

在第二次作业中,query_least_connection 这一 JML 较为复杂的指令,实际含义是查询某个 Person 所在的最小生成树的边权之和。在这里,我选择了在第一次作业实现的并查集的基础上,通过 Kruskal 算法来求解最小生成树的边权和。

构建与维护

考虑到对最小生成树进行动态维护在实现上较为复杂且代价较高,因此我选择了仅在查询时计算最小生成树。优点在于避免了复杂的动态维护,缺点是对同一颗最小生成树的查询每次都要重新计算。但考虑到查询前后整个图都有可能发生变化,因此个人认为综合考量下在查询时计算是更好的解决方案。

为了便于获取最小生成树的全部节点,第一次作业的基础上再MyNetwork中新建了HashMap<Integer, HashSet<Integer>> connection,其中 key 是根节点,value 是该根节点全部子节点的集合,在增加节点和边等操作时进行相应的维护。

query_least_connection

显然最小生成树内各个节点一定是联通的,因此我们可以通过并查集首先获得待查询节点所在最小生成树的全部节点:首先通过并查集查找到待查询节点的根节点,再通过新增的HashMap connection获得最小生成树的全部节点。

获得全部节点后,再通过HashMap<Integer, ArrayList<Edge>>获取这些节点的所有边,并对这些边按权值进行排序,时间复杂度为 O(|E|log|E|)。

接下来,对上述的节点和边按照 Kruskal 算法进行计算即可,其中判断两节点是否联通时依然采用并查集来优化性能,该步骤时间复杂度为 O(|E|α(|V|))。总的时间复杂度为 O(|E|log|E|)。

最短路径

在第三次作业中出现的 send_indirect_message 指令要求我们在两个 Person 之间以最短路径发送消息,并计算出最短路径的长度。在具体实现上我选择了经典的 Dijkstra 算法。

构建与维护

为了便于 Dijkstra 算法中起点到节点路径值的记录与比较,新建了一个 DijNode类,该类实现了 CompareTo 接口以便进行排序,同时提供路径值的更新方法。

由于算法过程中会不断根据距离对未计算出最短路径的节点进行排序,因此使用 TreeSet<DijNode> 来存放节点,保证了有序性。

send_indirect_message

与最小生成树中的处理类似,首先通过并查集获得起点所在的联通块内的所有节点,这些节点为最短路径可能经过的全部节点,然后在这些节点上进行 Dijkstra算法的计算即可。注意到我们只需求到固定终点的最短路径,因此当算出终点的结果时即可退出算法。

4 异常处理

本单元作业中我们需要实现一些具有计数功能的异常类,这些异常类在实现上非常类似,只是处理的具体异常不尽相同,因此我实现了一个 Counter 类来进行计数,每个异常类均将该类的一个实例作为一个 static 属性,从而实现计数。

Counter 类中用一个 int 类型的属性记录对应异常出现的总次数,用一个 HashMap<Integer, Integer> 记录某元素引起异常的次数,其中 key 是 id,value 是次数。

5 测试与Bug分析

测试

手动构造

对于连通块、最小生成树和最短路径等作业中的难点问题,由于这些也是图论中的经典问题,因此我采用的方法是到网上收集一些常见的易错样例进行测试。

对于异常类,构造一些异常数据来测试每个异常是否能正确触发、输出字符串是否正确等。

对于是否符合 JML 规格,主要通过 JUnit 进行单元测试。同时通过阅读指导书和 JML 来找出了一些细节问题,如群组人数最大为 1111,针对这些问题进行了重点测试。

自动测试

自动测试方面我只进行了大量数据的随机生成,除了做到全部指令的覆盖以外并没有想到很好的构造思路。

Bug分析

三次公测中未出现Bug,第二次和第三次作业的互测中出现了 Bug,互测中没有 hack 到别人。其中第三次作业的Bug是因为自己手滑,在 if 语句中少输入了一个 ”!“导致的,没什么意义,就不在此记录了。第二次作业的Bug则是由于性能原因,具体如下:

query_group_value_sum 指令导致的性能问题

这个指令本身其实并不难,但我在考虑性能问题时,只关注了三次作业中的难点(连通块、最小生成树、最短路径),而在其他看起来比较“简单”的指令的实现上并没有进行性能的优化。对于 qgvs 这个指令,我直接在查询时暴力遍历,导致互测中超时。解决的方法也很简单,只需要在 MyGroup 中使用一个属性来记录边权和并在增删 Person 以及增加关系时实时更新,查询时直接返回其值即可。

6 心得体会

通过本单元,我初步掌握了 JML 规格的基础知识,并在实验课和讨论课中掌握了一定的 JML 编写能力。

这是我第一次在严格的规格限制下进行编程,相较于此前的完全自由发挥,在思维方式、编程习惯上等都是有所不同的。规格的存在让我们可以更好地对自己的代码进行设计,帮助我们更加规范地完成任务。

但是,我们依然不能完全依赖于规格。规格只是告诉我们应该做什么和不能做什么,但没有规定应该怎么做。虽然直接把 JML “翻译”到代码往往能解决大部分问题,但很多情况下这并不是最好的实现方式,我们应该多去思考如何在严格遵守 JML 的条件下更好地完成代码的编写。

一个教训是不能只关注难点而轻视其他内容,我本单元两个互测中的 Bug 都是由于在实现一些简单功能时掉以轻心且没有进行充分测试导致的。

7 NetWork 扩展

扩展思路

Advertiser 、Producer 和 Customer 实现为 Person 的子类,Advertisement (广告)和Purchase(购买消息)实现为 Message 的子类,增加类 Product 表示商品,提供查询 id 的方法 getId()。每个 Producer 内通过属性存放其能够生产的商品,并提供判断能否生产某种商品的方法 惨Produce()。

JML 规格

完成了生产商品(produceGoods)、发布广告(publishAd)和购买商品(purchaseGoods)三个方法的 JML 规格:

public interface Network {
	/*@ 
	  @ public instance model non_null Goods[] goods;
	  @*/
    
    /*@ public normal_behavior
      @ requires contains(producerId) && (getPerson(producerId) instanceof Producer) && 
      @ 		 (getPerson(personId) instanceof Producer).canProduce(GoodsId);
      @ assignable goods;
      @ ensures goods.length == \old(goods.length) + 1;
      @ ensures (\forall int i; 0 <= i && i < \old(goods.length);
      @          (\exists int j; 0 <= j && j < goods.length; goods[j] == (\old(goods[i]))));
      @ ensures (\exists int i; 0 <= i && i < goods.length; goods[i].getId() == GoodsId);       @ also
      @ public exceptional_behavior
      @ signals (PersonIdNotFoundException e) !contains(personId);
      @ signals (NotProductException e) !(getPerson(personId) instanceof Producer);
      @ signals (CantProduceGoodsException e) !((getPerson(personId) instanceof Producer).canProduce(GoodsId));
      @*/
    public void produceGoods(int producerId, int goodsId) throws PersonIdNotFoundException, NotProducerException, CantProduceGoodsException;
    
    
    /*@ public normal_behavior
      @ requires containsMessage(adId) && getMessage(adId) instanceof Advertisement;
      @ assignable messages;
      @ assignable getMessage(adId).getPerson2().messages;
      @ ensures !containsMessage(adId) && messages.length == \old(messages.length) - 1 &&
      @         (\forall int i; 0 <= i && i < \old(messages.length) && \old(messages[i].getId()) != adId;
      @         (\exists int j; 0 <= j && j < messages.length; messages[j].equals(\old(messages[i]))));
      @ ensures (\forall int i; 0 <= i && i < \old(getMessage(adId).getPerson2().getMessages().size());
      @          \old(getMessage(adId)).getPerson2().getMessages().get(i+1) == \old(getMessage(adId).getPerson2().getMessages().get(i)));
      @ ensures \old(getMessage(adId)).getPerson2().getMessages().get(0).equals(\old(getMessage(adId)));
      @ ensures \old(getMessage(adId)).getPerson2().getMessages().size() == \old(getMessage(adId).getPerson2().getMessages().size()) + 1;
	  @ also
      @ public exceptional_behavior
      @ signals (MessageIdNotFoundException e) !containsMessage(adId);
      @ signals (NotAdException e) !(getMessage(adId) instanceof Advertisement);
      @ signals (PersonIdNotFoundException e) !contains(getMessage(adId).getPerson1().getId());
      @ signals (NotAdvertiserException e) !(getPerson(getMessage(adId).getPerson1().getId()) instanceof Advertiser);
      @*/
    public void publishAd(int adId) throws MessageIdNotFoundException, NotAdException, PersonIdNotFoundException, NotProducerException;
}

    /*@ public normal_behavior
      @ requires containsMessage(purchaseId) && getMessage(purchaseId) instanceof Purchase && contains(getMessage(adId).getPerson1().getId()) && getPerson(getMessage(adId).getPerson1().getId()) instanceof Customer && (\exists int i; 0 <=i && i < goods.length; goods[i].getId() == getMessage(purchaseId).getGoods().getId());
      @ assignable messages;
      @ assignable getMessage(purchaseId).getPerson1().money;
      @ assignable getMessage(purchaseId).getPerson2().messages, getMessage(purchaseId).getPerson2().money;
      @ 
      @ ensures !containsMessage(purchaseId) && messages.length == \old(messages.length) - 1 &&
      @         (\forall int i; 0 <= i && i < \old(messages.length) && \old(messages[i].getId()) != purchaseId;
      @         (\exists int j; 0 <= j && j < messages.length; messages[j].equals(\old(messages[i]))));
      @ ensures (\forall int i; 0 <= i && i < \old(getMessage(purchaseId).getPerson2().getMessages().size());
      @          \old(getMessage(purchaseId)).getPerson2().getMessages().get(i+1) == \old(getMessage(purchaseId).getPerson2().getMessages().get(i)));
      @ ensures \old(getMessage(purchaseId)).getPerson2().getMessages().get(0).equals(\old(getMessage(purchaseId)));
      @ ensures \old(getMessage(purchaseId)).getPerson2().getMessages().size() == \old(getMessage(purchaseId).getPerson2().getMessages().size()) + 1;
	  @ensures \old(getMessage(purchaseId)).getPerson1().getMoney() ==
      @         \old(getMessage(purchaseId).getPerson1().getMoney()) - ((Purchase)\old(getMessage(purchased))).getMoney();
      @ensures \old(getMessage(purchaseId)).getPerson2().getMoney() ==
      @         \old(getMessage(purchaseId).getPerson2().getMoney()) + ((Ppurchase)\old(getMessage(purchased))).getMoney());   
      @ also
      @ public exceptional_behavior
      @ signals (MessageIdNotFoundException e) !containsMessage(purchasedId);
      @ signals (NotPurchaseException e) !(getMessage(purchaseId) instanceof Purchase);
      @ signals (PersonIdNotFoundException e) !contains(getMessage(adId).getPerson1().getId());
      @ signals (NotCustomerException e) !(getPerson(getMessage(adId).getPerson1().getId()) instanceof Customer);
      @ signals (GoodsIdNotFoundException e) (\exists int i; 0 <=i && i < goods.length; goods[i].getId() == getMessage(purchaseId).getGoods().getId());
      @*/
    public void purchaseGoods(int purchaseId) throws MessageIdNotFoundException, NotPurchaseException, PersonIdNotFoundException, NotCustomerException, GoodsIdNotFoundException;
}
posted @ 2022-06-05 17:55  h_bh  阅读(26)  评论(0编辑  收藏  举报