面向对象第三单元作业个人总结与反思

面向对象第三单元作业个人总结与反思

看到作业要求之后感到各个点之间重叠的部分比较多,有一些无从入手,因此仍然打算首先按照三次作业的顺序来依次梳理,之后再对一些共性进行总结。

这篇博文没有图片,因为确实觉得没有什么需要用图片来说明的地方。

第一次作业

此次作业正值五一假期,时间相对来说比较充足,在吸收了之前的教训之后,我决心要建立一个能够禁得起之后请求考研的架构。

需求分析与架构设计

首先对所有的JML规格进行阅读;

Person类的建立较为简单;其结构类似于JavaBean,描述了网络中的一个点,因此只需要在其中对该点的邻居进行储存(因为存在查询该点邻居的要求)即可。

各个异常类的结构比较单一,唯一的特殊需求便是需要记录每个id触发异常的次数;此时自然而然想到利用static类型变量来记录数据。由于id值分布广泛,自然而然想到使用散列,即HashMap来对各个id的异常触发次数进行记录

唯一较为复杂的类是Network。该类描述的是一个网络类,并且要提供一些修改信息、查询其内部信息的接口,如加点加边、查询id对应的人、查询网络中两个点是否属于一个联通块、查询网络中联通块数目等。

对于加点加边的操作,与一般的图结构完全相同,因此考虑使用效率很高的链式前向星来进行储存;其在java中可以用一个HashMap<int/*person_id*/, List<Edge>>来进行实现;这样我们就可以在进行查询等操作时快速找到与一个点关联的所有边。

对于查询操作,与异常类的计数相似,自然而然想到使用HashMap并以id为键值来进行储存;

对于联通块的操作,我对其进行了时间复杂度的分析;实际上,无论是使用JML规格里直接给出的暴力方法,还是使用较为一般的dfs暴力方法,在极端数据情况下都可能导致超时的结果。因此,我考虑了以下几种情况下的最优情况:

  • 如果图算法中新增操作只有加点加边,删除操作只有删点,则可以使用并查集来得到较高的效率;
  • 如果图算法中包括了删边操作,此时普通的并查集不再适用;一种较优的方法是记忆化并查集(出现删除操作时即重新构造并查集),或者在删边操作较少时对被删除的边的两个端点的所有邻接边进行遍历。
  • 如果原本的无向图更改为有向图,则并查集算法彻底不适用,需要使用Tarjan算法或者kosaraju(java不是很好实现?)算法进行计算。由于在大量搜索之后发现百度前几篇讲Tarjan的博客没有一篇定义严谨并且严格证明的,我花了一个下午去啃了Tarjan的论文……

综合以上考虑,我使用了记忆化并查集的方式来实现对联通块的维护。为了保持这之后的可扩展性,我还将统计联通块的代码独立封装出来,并提出了对应的接口。事实证明浪费了大量时间。。。

实际上,这三次作业中我们提供的所有接口的时间复杂度都不应该高于或等于$O(n^2)$.

第二次作业

需求与架构设计

对JML进行阅读,可以发现除了新增的几个同质的Exception之后,作业要求中还增加了Group和Message两个类。Group是网络中一部分点的集合;而Message则是一种沿着网络中传递的信息,其会对发出者与接受者的属性进行改变。

Person类与Network类中新增了消息数组的要求。Network类中的消息数组有根据id随机访问的请求,因此使用HashMap进行实现;对于Person类,其消息数组的访问与加入操作类似于对栈的操作,可以直接使用List进行模拟。由于之后有关Message的请求需要在接受者的消息数组的头部插入消息,因此使用LinkedList来实现消息数组相比ArrayList来说更为简单。

Group类中的大部分请求比较plain,只要遍历一边并进行输出即可;唯一对性能要求较高的请求为getValueSum请求,需要计算该组中所有节点之间的边权总和;考虑到数据限制,在节点较多的情况下图一定是稀疏图,因此我选择对组内所有点的邻接边进行遍历来完成对所有边的计算。

Message类相关的请求也比较简单,唯一比较复杂的请求是sendMessage请求;该请求需要根据Message类型的不同向单个人或者组内的每个人发送该消息,因此需要处理的情况相对来说较多,方法的复杂度相对较高;因此我在之后也对该方法进行了着重测试。

第三次作业

需求与架构设计

对JML进行阅读,可以发现除了新增的Exception之外,作业中还新增了好几种不同的Message,并且新增了这些Message的发送效果;此外,对于EmojiMessage类,JNL还要求我们维护一个Emoji使用次数的数组,并提供去除使用次数低于一定数值的所有表情以及含有这些表情的信息。对于臃肿的Network类, 除了一些较为普通的接口之外,本次规格还新增了sendIndirectMessage请求,需要我们在图中找到两点之间的最短路。

对于各类Message,我新建了一个实现了Message接口的AbstractMessage类用来进行代码的复用;所有不同的Message都继承自该类,并实现了相对应的接口;对于RedPocket类的两种不同的“红包”效果,虽然有一些破坏封装性,但考虑到不用再考虑可扩展性,我选择将分配红包的函数提出来作为一个静态函数,以此来降低代码的复杂度。

对于Emoji类,由于其编号固有的离散性,虽然由于限制了数据范围因而数组可能是一个更好的选择,但是我仍然沿用习惯使用了HashMap进行储存;HashMap中的key为EmojiId,value为表情的热度。

在删除过程中,由于容器的特殊性,我使用了removeIf函数对HashMap进行了遍历删除;对Message序列也同样如此。

对于sendIndirectMessage请求,由于数据约束使得图中不存在负环,并且所求为单源最短路,因而我认为记忆化dijkstra应该是最快的方法。出于数据规模的考虑,我只使用普通的堆优化dijkstra算法,但对于本次作业来说已经足够。

总结

  • 基于规格的设计策略?
    • 一般来说,我会先细读所有的JML,并且将其转化成自己的理解(即理解其方法意图);此后,我会根据我自己的理解来编写代码,并在编写后与JML相互校验。
  • 测试的方法与策略?
    • 我在每一次实验中都自己撰写了JUnit Test,并且保证了代码覆盖率达到近乎100%;但如此测试存在着一个缺点,即在JML规格理解错的情况下JUnit Test也会写错,因此我在吃了一次教训之后选择将我的JUnit测试文件每次至少对两个同学的代码进行测试,以此来降低我的错误率。
    • 在第一次作业中我使用了JUnit4;但是随即发现自动生成的代码会导致Checkstyle爆零,并且JUnit4对异常的测试的支持相对较差,因此我在第二次作业中换成了JUnit5来进行测试。
  • 容器的使用经验?
    • 我不会去首先阅读容器的规格; 我选择首先阅读该规格的所有的相关方法,之后根据相关操作来选择适合这些操作的相应的数据结构(即对应的容器)。
  • 性能问题?
    • 结论:所有的Network类对外提供的接口,其时间复杂度都应严格小于$O(n^2)$。
    • 在我互测看着八份堆优迪杰代码抓耳挠腮之后想出了爆破HashMap让所有查找操作的复杂度从O(1)升到O(n)的损招,但是由于课程组巧妙的数据范围设置以及房子里另外七个人卷到极致的优化而没能得逞。可惜。
  • 图模型与维护策略?
    • 这次作业中我主要使用了三个算法/数据结构:链式前向星(用来保存图)、并查集(用来维护联通分量),以及dijkstra(用于计算单源最短路)。
  • 一些其他的心得体会?
    • 第二单元就因为接口定义不清晰而吃过亏,具体情况可以见上上篇论文。
    • JML确实是这一点上的一个很好的补充,但是写出良好的JML不论是时间上还是精力上(没啥区别?)对于我们的作业来说都不太允许。
    • 怎么说,感觉前三次作业下来,我已经有一套完整的开发流程了:需求分析 -> 接口定义 -> 代码填充 -> 测试正确性; 需求分析由我和语文老师保证;接口定义由设计模式辅助决定;代码填充由我的代码功底协助;测试正确性由Python评测机或者JUnit来完成。
      • 从需求分析到接口定义需要我的脑子对设计模式有所掌握,并且懂得如何去把握全局;
      • 从接口定义到代码填充由Javadoc、JML等工具辅助进行完成;
      • 从代码填充到测试正确性则有多种方式(白箱、黑箱、交给互测)等,需要我们灵活地进行运用。
    • 建议改为: How to write your code right.
posted @ 2021-05-30 17:19  tadshi  阅读(98)  评论(0编辑  收藏  举报