OO_Unit3
架构设计
第一次作业
本次作业的内容是创建基本的图。基础操作为加点,加边和修改边。对于关系网,决定采用并查树的结构进行表示。
对于queryValue,直接查询即可。
对于queryCircle,充分利用并查集的优势,判断两个人所在并查树的根节点是否相同。若相同,则证明两人直接联系或能通过与其他人之间的关系联系到一起。
对于blockSum的维护,在network中设置一个属性blockSum,利用离散数学中学习到的知识,在每次addPerson,addRelation和modifyRelation时对blockSum进行加减,实现动态维护,减少查询时的时间复杂度。
对于tripleSum的维护,在network中设置一个属性tripleSum,每次addPerson,addRelation和modifyRelation时遍历所有person,判断是否构成新的三角形或破坏了原有的三角形,对tripleSum进行加减,实现动态维护,减少查询的时间复杂度。
第二次作业
本次作业在原有基础上添加了Tag。
addTag,delTag,addToTag,delFromTag这几个指令的实现非常简单。
对于queryTagValueSum,在tag中设置一个属性valueSum,每次向tag中加人或删人时,动态维护valueSum。
对于queryTagAgeVar,动态维护年龄的和,即ageSum,每次查询时都计算一次ageVar。这里不进行动态维护的原因是一旦tag中加人或删人,ageAverage几乎都会发生变化,所以还是要遍历tag中的所有人,重新进行计算,并不能减少操作,对时间复杂度没有优化,甚至还可能提高时间复杂度。
对于BestAcquaintance,可以在person中设置一个属性,用于记录当前与该person间存在边的的所有人中与该person的value最大的人。当这一关系没有变化时,查询只需要调用这个值。当这一关系发生改变时,需要重新查找。这里有一种比较优秀的数据结构的选择,即优先队列,它能以O(1)的复杂度查询所需有限级(排名)的元素,这使得重新查找变得更快(O(n) --> O(1)),但美中不足的是,它的删除操作的时间复杂度是O(n),略高。
对于queryCoupleSum,没有想到很好的动态维护的方法,所以每次查询时就遍历所有person,时间复杂度为O(n),可以接受。
对于queryShortestPath,采用堆优化的迪杰斯特拉算法,对潜在的最短路径上的person进行广度优先遍历,遍历结束就得到了从给定person出发到各点的最短路径。
第三次作业
所有指令都比较简朴,没有涉及到什么高效的算法,也没有比较有意义的动态维护。
测试过程
对黑箱测试、白箱测试的理解
-
黑箱测试:程序实现未知。通过观察输入与输出之间的关系推测程序可能存在的问题。
-
白箱测试:程序实现透明。通过观察代码的逻辑、细节,判断是否错误。或者通过有针对性的数据构造,测试代码的每一个分支。
单元测试、功能测试、集成测试、压力测试、回归测试
单元测试
单元测试是用于检测某个特定方法的正确性的测试方法。
在这三次的作业中,使用Junit的测试就是单元测试。对于一个方法,使用构造的数据判断方法的效果是否符合预期。
功能测试
功能测试是黑箱测试的一部分。主要是对代码能否实现功能进行检验。不论是对某个方法实现还是整个文件的输出,都是进行功能测试。
集成测试
假设已经实现并测试了一些模块,检测模块之间的组合是否正确。
压力测试
用于验证软件应用程序的稳定性和可靠性。压力测试的目标是在极其沉重的负载条件下测量软件的健壮性和错误处理能力,并确保软件在危急情况下不会崩溃。它甚至可以测试超出正常工作点的测试,并评估软件在极端条件下的工作情况。
回归测试
用于验证最近对软件的更改或更新是否无意中引入了新错误或对以前的功能方面产生了负面影响。其主要目标是确保旨在改进的修改不会破坏软件的既定性能和可靠性。
数据构造策略
在几次中测的Junit测试中,我通过错误的测试点感受到,在测试时构造的数据需要涉及到一些极端数值,例如0,MAX_INT,对于图,需要涉及稀疏的图和稠密的图。总之像这种边界的特殊情况需要测试。由于本单元测试的对象都是单个方法,所以对于本地代码的分支覆盖比较简单。但是,对于课程组提供的src中的文件进行的测试都是黑箱测试,代码不透明,所以在数据构造时要十分全面,这也是要求边界数据的原因,单纯的随机数据很难覆盖这类型的数据。
性能问题
第一次作业中,并查集维护中的路径压缩方法写错了。
在第二次作业的强测中,我的程序在两处地方的性能十分低下,一处是第一次作业时所写的并查树重构,另一处是第二次作业所写的queryCoupleSum。两处都是因为进行了很多没有必要的循环而导致了超时。
规格与实现分离
规格用最基础,最简单的逻辑语言描述了方法应当满足的条件。
它的作用仅用于描述方法,让代码实现者掌握该方法的要求。至于该方法的实现,规格常常没有描述具体的实现方法,而是描述方法应得到的结果。即使一些目的简单的规格看似给出了正确的实现方法,其时间、空间复杂度往往很高,没有实用性,所以需要在理解规格的基础上,自己选择合适的、高效的方算法、容器来实现方法。
Junit
关于Junit测试,我的感受是,由于本单元的代码实现有JML语言的规范,对方法的测试代码的编写其实就是对JML语言的翻译。只要JML没有漏洞,并且翻译没有错误,那么测试代码便是完备的(不考虑数据生成的影响)。
但是通常我们自己在编写程序时没有JML的规范,因此没有一个可靠的参照物进行对齐。受本单元的影响,我觉得一个方法的规范分为两点,一是实现方法功能,二是不进行任何多余的操作。在自行编写测试代码时,可以依据这两点,参考@assingal、@invariant在test中的意义,检查方法调用前后是否实现功能,是否进行多余操作,来判断方法是否存在问题。
学习体会
- 根据JML规格写代码时一定要通读一遍规格后再着手实现方法
- 在实现方式时选用合适的容器和算法来提高性能
- 多测试,JML是测试的可靠帮手
- 尽量降低方法间的关联性,一个方法实现一个功能,每个方法内部不要太复杂,方便测试
- 编写JML规格十分考验逻辑的缜密性,在课上的练习中可以感受到