面向对象程序设计第三单元总结
Posted Armorr
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了面向对象程序设计第三单元总结相关的知识,希望对你有一定的参考价值。
面向对象第三单元总结
一、实现规格采取的设计策略
-
Step 1:
阅读第一次作业所有相关的规格,首先要梳理整个系统的功能和架构。刚读到Network中一堆灰色的代码时,我一度以为课程组忘了上传每个方法的描述,直到阅读完JML规格手册,才逐渐理解了通过规格来描述类与方法的行为。本次作业以一个社交网络系统为大框架,实现了对网络的的部分功能。
-
Step 2:
阅读每个类中相应的规格,整理出一些对容器、对架构的特殊要求,例如:较多的查找、方法实现复杂度的权衡。依照这些需求来选择合适的结构以便进一步组织代码;同时,根据类之间的继承和结构关系,先选择较为基础的类实现,如
MyMessage
、MyPerson
等。 -
Step 3:
选择好对应的容器后,可以实现一些较为简单、复杂度较低的函数,如
getPerson
、contains
、getSize
等,这类函数逻辑简单,一般仅基于容器就可编写。 -
Step 4:
落实到复杂度较高的方法时,由于注释中的代码阅读较为困难,可以将其整理出来——括号匹配友好度+++,明确的分开方法中每一种行为的前置条件、后置条件、赋值行为、异常行为等,并结合实际的物理意义,将整个方法的逻辑理解透彻。为了提高代码的可读性和逻辑性,我采取了如下顺序:
- 首先通过if_else条件判断所有
exceptional_behavior
的情况,并对应throw异常 - 接下来剩下
normal_behavior
:满足了所有前置条件时,再对相应需要实现的代码进行处理
- 首先通过if_else条件判断所有
-
Tips:
为了进一步满足方法的优化设计,可以在实现的类中添加一些规格外的辅助方法(外部调用时需要向下转型),例如:
-
在
sendMessage
与sendIndirectMessage
时,均会针对该message的type有后续操作,因此我将该后续操作提取出新的方法,增加了代码的可读性,降低了耦合度。private void dealWhenSendMessage(int type, Message mes) { messages.remove(mes.getId()); if (type == 0) { mes.getPerson1().addSocialValue(mes.getSocialValue()); mes.getPerson2().addSocialValue(mes.getSocialValue()); if (mes instanceof RedEnvelopeMessage) { mes.getPerson1().addMoney(((RedEnvelopeMessage) mes).getMoney() * -1); mes.getPerson2().addMoney(((RedEnvelopeMessage) mes).getMoney()); } else if (mes instanceof EmojiMessage) { emojiId.merge(((EmojiMessage) mes).getEmojiId(), 1, Integer::sum); } mes.getPerson2().getMessages().add(0, mes); } else if (type == 1) { MyGroup group = (MyGroup) mes.getGroup(); group.addSocialValue(mes.getSocialValue()); if (mes instanceof RedEnvelopeMessage) { int money = ((RedEnvelopeMessage) mes).getMoney() / group.getSize(); group.giveOthersMoney(mes.getPerson1().getId(), money); } else if (mes instanceof EmojiMessage) { emojiId.merge(((EmojiMessage) mes).getEmojiId(), 1, Integer::sum); } } }
-
在维护
Group
自身的ValueSum
、AgeVar
等变量、组内发红包修改金额时,也可以添加方法实现相应的功能。
-
二、基于JML规格设计测试的方法和策略
课程组介绍了JUnit作为本次作业辅助设计测试的工具:
JUnit是一个开放源代码的Java测试框架,用于编写和运行可重复的测试。他是用于单元测试框架体系xUnit
的一个实例(用于java语言)。它包括以下特性:
1、用于测试期望结果的断言(Assertion)
2、用于共享共同测试数据的测试工具
3、用于方便的组织和运行测试的测试套件
4、图形和文本的测试运行器
本单元作业中,我采取了JUnit手动构造数据样例和形式化验证结合的策略,在针对性能分析上,通过Python编写复杂度较高的测试样例并计算CPU运行时间。
三、容器选择和使用的经验
容器之间的区别通常归结于什么在背后"支持"它们,也就是说,所使用的接口是由什么样的数据结构实现的。Java实际为我们提供了四类接口:Map
、List
、Set
、Queue
。
为了充分发挥所选择的容器在我们给定的数据上的表现,首先我们要确定的一点是:我会对这些数据更多的进行什么样的操作?以及什么样的数据结构更容易满足我的需求?
以实例来看,在本次作业中:
-
MyPerson
需要一个容器来存放所有的人员信息。纵观整个代码体系,Person
类id的不可重复性、以及对Person容器的增删改查中,尤其需要很多查找操作。因此,结合数据结构的知识,Hash表在此方面具有比链表、树、堆更加优越的性能——使用private final HashMap<Integer, Person> people = new HashMap<>();
来存储,既方便了id
与Person
的一一对应,又将查找复杂度降低到了O(1)。 -
在第二次作业新增的
Message
时,需要为一个MyPerson
单独维护一个已收到信息的容器。但对于该容器的操作仅限于获取至多前四个值,因此只需要用ArrayList
即可满足。 -
对于其他各有所长的容器,可以选择在求图中最短路径的过程中使用堆优化的
PriorityQueue
以获得更高的排序性能;还有选择基于双向链表,增删更方便的LinkedList
来存放遍历算法中取出、删除节点的队列。 因此,不同的容器满足了不同的需求,在选择容器之前,一定需要根据相应的需求进行权衡,以充分利用不同数据结构的特性,实现更简洁高效的程序。
四、性能分析
令人感动的是本单元作业并没有采取多线程单元根据程序运行时间排名给分的情况。但也有一个CPU运行时间的最大限制,鞭策我们在翻译完规格时进一步思考和优化。对于本单元作业中一些值得优化的地方我总结了以下几点:
-
容器选择的优化:
这一点上文已经提到过,无需多言,可自行体会一下
addPerson
数千之后慢慢删查在HashMap
和ArrayList
结构下的性能差距。 -
可以将复杂度分散的优化:
这类优化在本次作业中多次体现,尤其在
queryGroupValueSum
、queryGroupAgeMean
等方法中,如果单纯按照规格给出的逻辑编写,可能复杂度至少也得O(n^2)了,但是可以将这类问题转化为数学问题进行简化。对一个Group
操作时,这些valueSum
、AgeMean
等值仅在Group
中人员信息发生变化(新增成员、删除成员、已有成员新增关系)的时候才会发生修改,而只要我们捕捉到每次修改,就可以以至多O(n)的复杂度拿下这些统计值。 -
对于单个方法的算法优化:
isCircle
:单独按照常见的算法来写复杂度较高,翻译规格更是铁定TLE,可选择并查集+路径压缩的方法,大大降低了单个方法的冗杂。queryBlockSum
:求关系网中强连通分量的个数,偷懒,设置了一个BlockSum
变量,在每次新增Person时加1,新增Relation时添加判断,若两人之前非isCircle
则需要将其减1。此时相当于将求强连通分量的复杂度分散到了添加关系中(调用isCircle
函数)。- 求最短路径:可采用堆优化的Dijkstra算法。
五、架构设计与维护策略
整体代码依照规格实现,除了在实现的类中增添了部分特定的算法,并没有其他的新增,故略去类图结构。
对于本单元作业的大型网络图结构。抛开Person、Message等特殊的细节,以整体的角度审视,可看作是一个带权值的无向图(或者有向图中,点连通==>双连通)。我采取的方式并没有在图上有更高级的结构,而是以HashMap
为基础,建立了一个类似邻接表的结构:每个点都维护自己的邻边。在此基础上,提供并查集算法来完成连通性判断、Dijkstra完成最短路径求解。整体来说算是比较基础和经典的图结构。
以上是关于面向对象程序设计第三单元总结的主要内容,如果未能解决你的问题,请参考以下文章