OO第三单元总结
OO第三单元总结
设计策略
本单元作业的内容是基于JML规格实现一个社交网络。本单元作业看似只需要一板一眼地根据课程组提供的JML规格完成自己的代码,便可以确保程序的正确性,轻松通过弱测和中测,但是实际上并没有那么简单,本单元的强测对于程序的性能具有较高的要求,我第一次作业就单纯地按照JML规格,它告诉我用什么我就用什么,规格中使用循环对于函数功能进行描述,我也不假思索地直接使用循环完成,所得到的结果就是强测很多点的超时。
后来才了解到例如JML类规格中给我们属性是一个静态数组,我们并不是一定要使用静态数组,我们可以根据我们的需要选择使用多种不同的存储容器,例如容器ArrayList,集合HashSet等等,而所谓规格不过是贵定了一个函数的输入和相对应的输出,而函数的内部是如何实现的,则需要我们自己思考,采用正确性和性能兼具的方式来实现。
对于类中一些规格比较简单的函数,比如说MyPerson类,MyGroup类,MyMessage类中就有很多类似于equals,add,get相关的函数,对于这些我们只需要按照规格一板一眼地实现就好了,不需要进行性能上的优化。而且这些函数的规格中,很多都分为exception_behavior和normal_behavior两部分,对于这样规格的函数,我们只需要先写异常部分,在满足相应条件时抛出对应异常,接着再写normal_behavior部分,按照规格实现相应的功能。
而对于MyNetwork类中的一些规格比较复杂的函数,我们就不能直接开始写,我们首先要读懂理规格所声明的函数需要完成什么任务,接着基于整个程序框架进行总体上的设计使函数满足规格并且具有优秀的性能,切忌为了性能忽略正确性或者与规格不相符合,同时我们也不能够死板地完全按照规格的描述方法去实现函数,这样虽然能够确保正确性但是性能并不能够让人满意。
测试方法及策略
根据课上老师的介绍,本单元我们可以使用JUnit和OpenJML对代码进行测试,但是很可惜,本单元作业中我并没有使用这些方法对代码进行测试,而是采用了自己手动编写或者自动生成一些指令,找同学进行对拍。由于本单元作业中指令条数并不是很多,所以自动生成指令能够很轻松地覆盖所有的指令,使用这样的数据与别人对拍大概率可以验证程序的正确性。而对于一些我们自己认为可能会出错的函数,我们可以自己手动编写一系列的指令,例如图结构中的相关函数,很容易出错,我们可以用ap指令和ar指令生成一些简单的图,然后专门执行这些图相关函数验证这些函数的正确性,这样测试的好处在于比较直观地能够定位到Bug位置。
容器选择分析
在本单元的作业中,容器的选择十分关键,如果我们就根据规格所给的提示全部使用静态数组,不但后面有一些函数难以实现,在程序的性能方面也将不足以差强人意,所以我们在对容器进行选择时要慎重又慎重,并且要提前规划好在后面函数中该如何使用。
我们直接拿第三次作业进行分析:
MyPerson类
-
对于规格所要求的存储有关系的人的容器,存储与有关系的人的关系值的容器,存储信息的容器,我们均采用ArrayList容器,选择的原因还是在于对于ArrayList的使用比较熟悉,同时这两个容器实际上是一个同步变化的关系,我们仅需要注意在一个容器变化时另一个同步修改即可。
-
该类中还需要一个存放messages的容器,考虑到下面函数中会有删除消息,将消息插入容器头部的操作,我们直接采用ArrayList来实现这个容器因为ArrayList支持在指定位置插入元素,而且删除也仅仅只需要调用remove方法即可。
-
除此以外,为了实现在遇到指令qci和指令qbs有更好的性能,在MyPerson类中加入了一个存储与该人在同一连通分量中的人的HashSet集合,这样我们只需要在外部执行往社交网络中加入人,和在两个人之间添加关系时,对于该容器进行操作只需要使用addAll方法,而且不需要考虑两个集合中是否有相同人的问题,比较方便。
MyGroup类
- 本类中仅需要我们有一个存储该group中的人的容器,对于该容器中的元素并没有什么复杂的操作,我们只需要使用能够满足加入元素,删除元素,遍历元素这些操作的容器就可以了,所以这里我们简单地采用我们最为熟悉的ArrayList来完成。
MyNetwork类
- MyNetwork类与MyGroup类同样,我们只需要使用能够满足加入元素,删除元素,遍历元素这些操作的容器就可以了,我们这里同样采用五个ArrayList来实现,值得注意的是,EmojiIdList和EmojiHeatList同样是同步变化,即两个ArrayList在同一个地方存放的分别是一个Emoji的EmojiId,另一存放的就是该Emoji的热度,对于表情进行操作时,需要注意对于两个ArrayList进行同步变化。
异常类
- 本单元作业需要对于异常进行处理,每一个person,每一条message都有属于自己独一无二的id,我们需要在异常发生时,输出该id总共已经触发了多少次异常,即每一个id都有自己触发异常的数量需要进行存储,我们这里就采用HashMap进行存储,HashMap的key即为id,HashMap的value即为触发异常的次数,增加次数时只要根据key值将对应value加1即可。
性能分析
本单元有很多涉及到图结构的内容,关于图的搜索和操作如果设计得不好,很容易造成超时的问题。
第一次作业
关于图结构的指令有query_circle和query_block_sum两条,其中第一条指令是查询社交网络中两个人是否联通,即查询图中的两个节点是否联通,而第二条指令是查询整个关系网络中连通分量个。如果不考虑性能,就像我第一次作业中做的那样,对于query_circle指令,直接采用暴力的深度优先算法,即一旦出现qci指令,就调用这个深度优先搜索算法,递归遍历这张图中所有的节点,判断给出的两个节点是否联通,而对于query_block_sum指令,则需要多次调用前面判断两个节点是否联通的函数进行计算,该函数结构并不是很复杂,但是时间复杂度很高,而且一旦出现连续的qbs指令,虽然整张图的独立连通分量个数并没有变化,但是我们却要进行重复的复杂的计算,这显然是十分不合理的。所以我们可以在MyNetwork类中设置一个circles属性,该属性作为社交图的基本属性对于整张图的连通分量数量进行记录,这样只需要在我们add_person和add_relation时对于该属性进行更改,而执行qbs指令时我们只需要返回MyNetwork中的circles属性的值即可,这样虽然在add_person和add_relation时所花费的时间会稍微多一点,但是出现连续的qbs指令时,程序的时间复杂度会大大下降。除此以外,我们还可以对于qci指令的完成进行一定的简化,前面说到,我一开始采用的是深度优先搜索算法,由于该算法是循环加上递归,时间复杂度很高,对其进行修改同样可以从类属性的设计来完成,在MyPerson类中,我们还可以增加一个HashSet集合容器来存储和这个人在同一连通分量中人,同样地,我们只需要在add_relation时将两个人的该集合合并更新,这样我们在执行qci指令时,只需要查找该人的该集合属性中是否存储有另一个人,时间复杂度相较而言降低了不少。这就是第一次作业中主要容易超时的两个点,qci和qbs指令。
第二次作业
第二次作业中我们加入了Group类,对于在同一个group中的人我们可以对计算他们的平均年龄,即qgam指令,或者是算它们年龄方差,qgav指令,即对gruop中人的一些统计量进行获取。与第一次作业相同,倘若当我们在指令中连续地添加重复地获取这些统计量的指令,而我们又仅仅只是每次按照规格一遍一遍地进行循环计算,那么我们浪费的时间还是比较多的,对于这种情况解决的方法和上一次作业类似,我们可以将这些统计量作为Group类的基本属性,每当我们向group中add_person时,我们对于这些统计量进行更新,而之行类似qgam、qgav这些获取Group类中统计量的指令,只需要直接返回类中的属性值就可以了。
第三次作业
第三次作业中新增加的指令中,其实除了那个send_indirect_message指令外,并没有别的容易造成超时的,而该指令的实质是在图中寻找最短路径。由于寻找最短路径需要遍历图中的节点,而且一旦图中加入节点,加入关系,最短路径的变化情况难以预测,所以一旦指令中大量地出现sim指令,程序很容易超时。在这次作业中,我直接采用了Dijstra算法进行最短路径的计算,虽然确保了正确性但是性能并不让人满意,在强测中出现了几个点超时的问题。后面在修复Bug的过程中,和同学交流后得知可以使用堆优化的Dijstra算法来避免超时,大概思路就是使用优先队列实现寻找下一个要访问的节点时,直接从优先队列中寻找到距离最短的节点。
架构分析
说明
- 本单元三次作业是增量式开发,每一次作业都是在前一次作业的基础上增加类、方法或者属性,来增加这个社交网络的复杂程度,所以这里只分享本单元第三次作业的设计策略,该次作业基本上是在前两次作业架构的基础上完成的。
总体架构
-
MyMessage类
代表消息,为每一个人所拥有,可以进行发送等操作,Message类中均有两种type,一种是私发的消息,即一个人发给另一个人的消息,另一种是群发的消息,即一个人在某一个group中发的消息。该类作为父类,拥有MyEmojiMessage、MyRedPocketMessage、MyNoticeMessage三个子类,三种不同类型的消息拥有不同的操作,需要注意区分。
-
MyEmojiMessage类
表情消息,消息中包含表情。
-
MyRedPocketMessage类
红包消息,消息中包括红包金额。
-
MyNoticeMessage类
普通的消息。
-
-
MyPerson类
模拟社交关系网络中的人,每个人都有认识的人,与这些认识的人之间都有关系,每个人还都有一系列接收到的messages。
-
MyGroup类
该类对于社交网络中的圈子或者说群聊进行了模拟,在group中可以发送消息,其中如果发送的消息类型是RedPocket类型,还是一个群发红包的操作。
-
MyNetwork类
该社交网络的模拟,是一个带权的无向图,是本单元作业中最为复杂的结构,对于图结构的各种查找,查询连通分量数,寻找最短路径等操作都是本单元最容易超时的,我们需要对于其性能进行优化。
-
Exception异常类
对于不同的异常进行次数记录,在print函数中进行输出。
图模型的架构
- 本单元作业的图是带权的无向图,在MyNetwork类中,有一个People类型的ArrayList容器,用来存放社交网络中的所有人,图中的每一个节点均为MyPerson类的一个实例,图中的每一条边即为人与人之间的关系,边的权值即为两个有关系的人之间的value值。每当输入指令ap(add_person)时,我们将该person加入到存放people的容器中,即将这个人加入了该社交网络中,而在我们输入指令ar(add_relation)时,我们直接在找到这两个Person,对其acquaintance容器和value容器进行修改,即在关系图中的两个人之间加入了一条边。此即为图模型的生成与更新维护。
- 正如上面在性能分析板块所分析的那样,如果我们仅仅在MyNetwork中模拟一个图,每次需要查询图中的一些信息时都需要调用相关的算法进行计算,那么该程序的性能必然不会很好,所以我们在MyNetwork中加入了一个circles属性,用于记录图中的联通块个数,该属性初始值设为0,当add_person时,联通块数增加1,当add_relation时,如果两人本来不是一个联通块中的,则将联通块数减少1,并将两个人的联通块合并,如果两人本来就在同一联通块中,则就简单地普通增加图中的边即可。
- 在MyPerson中增加一个集合容器,记录与该人在同一联通块中的人,主要目的是为了配合图模型中联通块个数的计算。