面向对象第三单元总结
面向对象第三单元总结
第九次作业
作业需求
第九次作业要求我们根据官方包给出的接口中的JML规格来实现接口中的各种方法,最终需要实现了社交网络的模拟系统。
这次作业中的任务主要有
- 人的信息的维护
- 人与人之间关系的维护
- 群组信息的维护
- 网络中对社交关系的查询
其中前三项均是较为简单的功能,重点和难点在第四个任务上。
基本设计
本次作业是根据官方包接口的规格进行实现,所以整体的架构已经给出了,没有太大的改动空间。其中可以需要进行设计的地方只有queryBlockSum
和isCircle
两个函数,queryBlockSum
函数并没有像JML中写出的那样依赖于isCircle
函数,而是使用HashMap
维护了一个并查集来实现两个函数
queryBlockSum
和isCircle
实现细节
我们将本次作业中的社交网络看作一个图,而每一个小团体(小团体中的人直接或间接地认识彼此,而小团体之间的人完全不认识)则是一个分支。queryBlockSum
函数就是查询这个图中分支的数目,isCircle
函数则是查询两个人是否处于一个小团体中。
对于一个小团体,我使用一个HashSet
来表示,同时使用HashMap
使每个人映射到其所属于到小团体。我们可以发现对于等价类之间只有合并这一个操作。而会导致等价类发生合并的情况,只有对分属于两个等价类之中的人执行addRelation
操作。所以我们可以根据以下的原则在全局维护一个等价类信息。
- 当新增一个人进入
Network
时,其属于一个新的等价类,该等价类之中只有他一个人- 当进行
addRelation
操作时,判断参数中的两个人是否满足isCircle
- 如果
isCircle
的结果为true
则不做其它处理- 如果
isCircle
的结果为false
则需要合并两个等价类,即将两个HashSet
进行合并,并修改人员的的映射
在完成这一等价类信息的维护后,isCircle
函数的行为则是判断两个Person
映射到的HashSet
是否相同,而queryBlockSum
则是获取整个Network
中的HashSet
的数目。
第十次作业
新增需求
第十次作业在第九次作业的基础上,增加了Message
相关的操作和对于第九次作业中分支最小生成树的查询。
其中出现了一些需要解决的问题:
- 最小生成树的实现
- Message中以0号元素为栈顶的栈结构实现
- Message成员的实现
基本设计
本次作业中新增的类同样有架构基础,但是我在本次作业中将分支抽象出来成为了一个单独的Parity
类,同时考虑到qgvs
的实现效率,对其实现细节进行了修改。新增的Parity
类结构如下。
qgvs实现
在JML规格中,qgvs
函数需要对于Group
中的Person
两两调用queryValue
方法进行查询,如果在Person
中使用ArrayList
来储存其认识的人的话,粗略计算复杂度为\(O(mn^2)\),其中\(m\)为平均每个人认识的人数,\(n\)为Group
中的人数。而如果采用HashMap
来储存的话,则是\(O(n^2)\),但是HashMap计算的时间常数无法忽视,这两种情况下,时间复杂度均不算理想。
所以我在实现时采用了另一种方法:取出Person
中储存认识的人的容器,遍历容器中的成员,判断其是否属于当前组,如果在Group
中使用HashMap来储存组内的Person
的话,这一方法的时间复杂度则是\(O(mn)\)。考虑关系图为稠密图的可能性不大,性能要优于上述的两种方法,尤其由于第一种方法。
最小生成树-Kruskal
本次作业中最小生成树的实现我选择的是Kruskal算法,Kruskal算法的基本思路即为,不断选择不会导致成环的权值最小的边加入生成树中,直到边的数目等于点的数目减一
此时我们需要解决的两个问题就是
- 如何选择满足条件的最小权值的变
- 如何保证加入的边不会成环
对于这两个问题,我采用的方法遍历排好序的边,如果加入当前边不会成环,则加入,否则则跳过。
边的排序使用链表排序法,即维护一个升序的链表,每次插入边时插入到合适的位置来满足升序条件。
成环的判断采用了第九次作业中等价类划分的思路,由于对一个满足无环条件的分支而言,向其内部加入一条边,一定会形成环。所以我们只需要保证:
- 初始时不存在有环的分支
- 在构造过程中,不会存在向分支内部加边的行为
则可以保证最后构造的分支是无环的。
所以其实现思路如下
- 初始化分支表,每一个节点对应一个分支
- 遍历升序的边链表
- 如果遍历到的边两端位于不同的分支,将边加入最小生成树,合并两个分支
- 如果遍历到的边位于同一个分支,跳过
Message容器选择
本次作业中,每个Person
收到的Message
需要存入每个Person
内部,且需要满足Person
内部的容器的第0个元素为最新收到的信息,其中getReceiveMessage
方法也需要获取到最新的若干条信息,普通的ArrayList
对于头部增删并不友好,所以采用了LinkedList
来实现。
第十一次作业
新增需求
第十一次作业在原有作业的基础上,对于Message
的类别进行了细分,区分出了Message
、RedEnvelope Message
、Emoji Message
三个细分信息,同时增加了sendIndirectMessage
方法用于在非直接认识的人之间发送消息,同时需要计算出发送信息所经过路径的消耗。
基本设计
在本次作业中,新增的结构仅有Emoji
相关的结构,其需要一个容器来进行储存,我将Emoji
的容器封装为EmojiContainer
类,使Emoji
相关的操作都委托给该类来完成,EmojiContainer
类的结构如下。
除此之外,还需要实现deleteColdEmoji
和sendIndirectMessage
两个函数
deleteColdEmoji实现
deleteColdeEmoji
需要删除热度低于limit
的Emoji
,同时删除未发送的引用了该Emoji
的EmojiMessage
。所以对于这一函数的实现我们需要解决两个问题。
- 如何快速删除热度低于
limit
的Emoji
。 - 如何删除对应的
EmojiMessage
为解决第一个问题,我采用链表来维护了一个升序的Emoji
链表,同时为了实现快速查找,还配合了HashMap对于链表项进行索引。维护该链表需要注意新增Emoji和增加Emoji热度两个操作时的热度。在进行两个操作时,都需要将被操作的Emoji
移动到合适的位置。
而第二个问题我采用的解决方法是在Emoji
内部储存引用其本身的Message
的ID
,我们需要在addEmojiMessage
、SendMessage
时进行额外的操作来保证Emoji
内部的ID
的正确性。而在deleteColdEmoji
时会调用EmojiContainer
内部的方法来删除Emoji
同时返回所有需要删除的Message
的ID,再由Network
来对Message
进行删除。
sendIndirectMessage实现
由于对于sendMessage
中发送各种不同类型的Message
的实现逻辑进行了封装,所以在sendIndirectMessage
中可以进行复用,在实现这一方法时重点在于最短路径权值和的计算方法。
我采用了Dijkstra算法来实现,但是在具体实现时,考虑到堆优化的时间复杂度为\(O(nklogn)\)(\(n\)为分支中的人数,\(k\)为平均度数),当图稠密甚至是完全图时,复杂度就会趋向\(O(n^2logn)\),还是选择了\(O(n(n+k))\)的普通算法。没有考虑到构建稠密图实际上需要消耗很多的指令条数,会使图不能具有很大的规模。在强侧中的第六个点中CPU时间直冲9秒,险些超时。
JUnit使用与测试感想
在本次作业中,我对于JUnit测试进行了一定的尝试。
本来以为JUnit是会自己生成测试样例点那种,但是实际上还是需要自己写(汗),但实际上应该也还不大可能有那种完全测试样例都完全生成的测试器。
所以使用的过程中需要对着JML规格来进行测试。我面对JML构造样例的逻辑主要分为以下几个步骤
- 现根据JML中的normal_behavior和exception_behavior来区分几种前置条件的行为模式
- 在每种行为情况中,构建若干符合前置条件的样例并调用带测试函数来执行操作
- 使用assert语句来对结果的正确性进行判断,并调用JUnit内置的部分assert方法来识别异常的抛出。
但是在实际的测试过程中发现在作业中使用JUnit来进行测试存在一个问题,那就是JUnit测试样例的编写取决于自己对于JML的理解程度,如果从一开始就理解错了那在测试时写出的测试样例也不会满足条件,而且在编写测试样例的过程中,受限于思维定势也更倾向于于编写出符合自己开发逻辑的测试样例。所以我认为在这类专门进行测试的工具中,开发人员和测试人员应该是分离的。开发人员进行开发,测试人员编写测试样例对于产品进行测试。
但是JUnit这样的单元测试相较于以往的OJ普通那样的整体测试来说,更容易发现问题,能更好的避免错错得对的那种情况。经过这样的测试能比整体测试具有更好的鲁棒性。
Network拓展设计
为实现新增需求,需要新建以下几个类,新增的部分UML规格如下
而在Network中可以新增方法来调用这些类中的方法实现指定的功能。
从拓展的部分中,选择sendAdvertiseMessage
、addProduct
、submitAdvertiser
来撰写JML规格
sendAdvertiseMessage
/*
@ public normal_behavior
@ assignable messages;
@ assignable targetPeople[*].messages
@ 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 int u; 0 <= u && u <= targetPeople.length;
@ (\forall int i; 0 <= i && i < \old(targetPeople[u].getMessages().size());
@ \old(targetPeople[u].getMessages().get(i+1) == \old(targetPeople[u].getMessages().get(i))));
@ ensures (\forall int u; 0 <= u && u <= targetPeople.length;
@ \old(targetPeople[u].getMessages().get(0) == \old(getMessage(id));
@ (\forall int u; 0 <= u && u <= targetPeople.length;
@ ensures \old(targetPeople[u].getMessages().size() == \old(targetPeople[u].getMessages().size()) + 1);
@ public exceptional_behavior
@ signals (MessageIdNotFoundException e) !containsMessage(id);
*/
public void sendAdvertiseMessage(int id) throws MessageIdNotFoundException;
addProduct
/*
@ public normal_behavior
@ requires !(\exists int i; 0 <= i && i < products.length; products[i].equals(product));
@ assignable products;
@ ensures products.length == \old(products.length) + 1;
@ ensures (\forall int i; 0 <= i && i < \old(products.length);
@ (\exists int j; 0 <= j && j < products.length; products[j].equals(\old(products[i]))));
@ ensures (\exists int i; 0 <= i && i < products.length; products[i].equals(product));
@ also
@ public exceptional_behavior
@ signals (EqualProductIdException e) (\exists int i; 0 <= i && i < products.length;
@ products[i].equals(product));
*/
public void addProduct(ProductInfo product) throws EqualProductIdException
submitAdvertiser
/*
@ public normal_behavior
@ requires !(getAdvertisers.contain(advertisers));
@ assignable advertisers;
@ assignable advertiser.targetPeoples;
@ ensures (\forall int i; 0 <= i <= \old(advertisers.length)
@ getAdvertisers.contain(advertisers[i]));
@ ensures \old(advertiser.length) == advertiser.length - 1;
@ ensures (getAdvertisers.contain(advertisers));
@ ensures (\forall int i; 0 <= i <= \old(advertiser.getTargetPeoples().size)
@ advertiser.getTargetPeoples().contain(advertiser.getTargetPeoples().get(i))));
@ ensures \old(advertiser.getTargetPeoples().size) == advertiser.getTargetPeoples().size - 1;
@ ensures (advertiser.getTargetPeoples().contain(this));
@ also
@ public exceptional_behavior
@ signals (EqualPersonIdException e) getAdvertisers.contain(advertisers);
*/
public void submitAdvertiser(Advertiser advertiser);
学习体会
第三单元的作业是我第一次接触契约式编程的思想,本单元的作业任务都是基于JML这一形式化的语言来给出的。其中JML语言的behavior的区分让我感受到了对于形式化语言进行撰写时需要保障的严谨与完备。同时,实现第九次作业的queryBlockSum
、isCircle
方法和第十次作业的queryGroupValueSum
方法时,依照着后置条件逻辑来编写的代码并不具有很好的性能。需要自己另行进行设计。在写完这些方法后,我才意识到,JML只是一个规格保证,编写的程序并不需要安装保证的逻辑来实现,只需要保证结果符合逻辑即可。把JML规格当作伪代码来看待并不是合适的行为。