北航面向对象设计与构造第三单元总结

        面向对象设计与构造课程的第三单元终于结束了。这一单元专注于培养学生的规格化设计能力。接下来我将基于我的个人代码进行一些介绍分析,并对个人第三单元的学习状况进行简要总结与分享。

架构分析

代码构造

         为了方便接下来对架构的分析,要先对本人的代码结构进行简要介绍,并对类的设计考虑进行简要介绍。如下方的UML类图所示:

        说是介绍一下自己代码的结构,其实并没有什么好讲的。每一次作业的代码都实现官方包接口中给出的抽象方法,也就意味着代码的总体架构已经固定,并没有很多变化的余地。

        这里简单介绍一下各个类的使用情景。本单元需要维护的是一个社交网络,用MyNetwork类管理;网络中的每个人用MyPerson类管理;此外,每个Person都拥有自己的Tag容器,每一个Tag类似“关注列表”,用MyTag类管理,只有与之建立社交关系的人才能被放入他的某个关注列表中;网络中还可以发送消息,消息的发送方式可以是一对一发送或者是“群发”,并通过发送消息改变双方的社交值等等属性。消息有四类:普通消息(MyMessage类),提示消息(MyNoticeMessage类),表情包消息(MyEmojiMessage类),红包消息(MyRedEnvelopeMessage类)。对于执行指令错误的情况有各类自定义异常类,这里不再赘述,并且为了记录每个异常的具体情况,为每个异常类内置了一个单例的ExceptionCounter。Bfs类用于实现寻找两点之间最短路径长度,UnionSet类用于判断两点之间是否连通。FriendLog类用于辅助提高queryBestAcquaintance方法的效率。

图模型构建

        本单元的重点除了规格外,就在于图相关的算法优化。我们维护一个社交网络其实就是在维护一个图的节点和边。网络中的每一个人对应着图的一个节点,addPerson操作就是向图中添加一个节点;两个人建立社交联系对应着图中该两点之间添加一条边,该边的权重就是社交联系的value,addRelation对应着加边,modifyRelation在新的value仍大于0时对应着修改对应边的权重,否则对应着删边。

        Tag对应着图的一个子图,这个子图包含该tag中所有人,也包含他们之间所有的边,即一个极大子图。addToPerson,deleteFromPerson分别向该子图中加节点/删节点。

维护策略

        我采用的维护策略大体上分为三种:动态维护,优化算法与利用数据结构。动态维护的意思是,与其每次重新计算,不如随着图的建立不断对该属性进行维护,这样,当需要查询该属性的时候只需要返回内部维护的该属性,时间复杂度由O(n^2) 优化到了 O(1),在图顶点数最多100的情况下,这是相当可观的性能提升。优化算法的意思是改变计算方法,采用性能更优的算法也可以降低时间复杂度,但在具体代码实现上一般更有难度。利用数据结构的意思是,利用更高效的数据结构存储数据可以提高运行效率,举个例子,getPerson方法需要根据id找到对应的Person,如果使用数组或ArrayList存储Person则需要遍历,时间复杂度O(n),而如果使用从id到Person的HashMap来存储,时间复杂度就只有O(1)。同理,管理无序集合我尽量都使用HashSet,因为HashSet的搜索复杂度是O(1)。

        Java内置的方法也会带来额外时间复杂度。比如LinkedList中的remove方法,如果传入的是对象,复杂度为O(n),如果传入的是索引,复杂度为O(1)。在使用这些方法时要合理考虑带来的时间代价。

        在本次作业中,几乎要将所有的方法时间复杂度优化到最差O(n)才可以不出现CTLE的情况,下面针对具体方法的维护进行说明。

        queryCircle与queryBlockSum的维护。采用并查集实现,每次添加新关系就在并查集内进行森林的合并,并在同时进行路径压缩。当出现删边情况,不立刻重建并查集,而是设置一个脏位,在下一次isCircle时重建并查集并将脏位回复,这样尽量避免了多余的重建。路径压缩的并查集求连通性的时间复杂度可以达到接近O(1)。queryBlockSum本质上是求图中连通分量的个数,同样利用该并查集,遍历所有Person在并查集中的根节点,数一数共多少个不同根节点即可得到结果,时间复杂度~O(n)。

        queryTripleSum的维护。采用动态维护。每次加边(删边)时,计算相关的两个节点有多少个公共邻居,并让triCount属性加上(减去)该数字。时间复杂度为O(1)。

        queryTagValueSum的维护。采用动态维护。每次向tag中加人,删人,加边,删改边都更新tag中valueSum的值。时间复杂度为O(1)。

        queryTagAgeVar的维护。采用动态维护。每次向tag中加人,删人更新tag中ageSum,ageSquareSum的值。时间复杂度为O(1)。

        queryBestAcquaintance与queryCoupleSum的维护。为每个Person动态维护一个TreeSet,每次加边/删改边向其中加入/删去新的伙伴信息(边的权,id),自定义比较器,使TreeSet的最顶端元素对应他的bestAcquaintance。这样queryBestAcquaintance只须找出id对应TreeMap的最顶端元素,时间复杂度为O(1);queryCoupleSum也使用了动态维护。每次删边/加边后如果出现bestAcquaintance的改变,就置脏位。当查询时如果脏位为真,就通过遍历所有Person完成计算,时间复杂度O(n),并更新coupleSum;否则直接返回属性coupleSum,时间复杂度O(1)。

        queryShortestPath的维护。采用广度优先遍历(BFS),时间复杂度O(n)。

测试分析

对测试的理解

黑盒测试是以用户的角度,从输入数据与输出数据的对应关系出发进行测试的,通过测试来检测每个功能是否都能正常使用。

        进行黑盒测试主要是为了发现以下错误。

(1)是否有功能错误,是否有功能遗漏。

(2)是否能够正确地接收输入数据并产生正确的输出结果。

(3)是否有数据结构错误或外部信息访问错误。

(4)是否有程序初始化和终止方面的错误。

        我广泛地应用了黑盒测试。我根据JML规格自行构造样例,尽量做到覆盖每一个数据可能性,这样只要输出结果正确,我就初步认为我的程序正确。事实上我们所使用的评测机也是一种黑盒测试,只不过数据强度要更大。

白盒测试全面了解程序内部逻辑结构、对所有逻辑路径进行测试。白盒测试是穷举路径测试。在使用这一方案时,测试者必须检查程序的内部结构,从检查程序的逻辑着手,得出测试数据。贯穿程序的独立路径数是天文数字。

        白盒测试的原则是:

(1)一个模块中的所有独立路径至少被测试一次。

(2)所有逻辑值均需测试true和false两种情况。

(3)检查程序的内部数据结构,保证其结构的有效性。

(4)在取值的上、下边界及可操作范围内运行所有循环。

        容易看到,白盒测试更加全面完备,但是需要很长的时间进行全面测试,我并没有进行白盒测试。我不由得想到上学期oopre的覆盖率测试要求。覆盖率测试包括类覆盖率,分支覆盖率,方法覆盖率,行覆盖率等,这也是一种白盒测试。

单元测试是在软件开发过程中要进行的最低级别的测试活动,软件的独立单元将在与程序的其他部分相隔离的情况下进行测试。

        在本单元的作业中要求对个别方法进行JUnit单元测试,全面地对该方法的各种情况进行充分测试;使用@Parameters参数化测试后,测试可以变得更简单,一次测试多组数据。

功能测试就是对产品的各功能进行验证,根据功能测试用例,逐项测试,检查产品是否达到用户要求的功能。

        功能测试是从用户的角度进行测试的。如此来看,功能测试也是黑盒测试的一种。

集成测试是在单元测试的基础上,将所有模块按照设计要求(如根据结构图)组装成为子系统或系统,进行集成测试。

        一些模块虽然能够单独地工作,但并不能保证连接起来也能正常的工作。一些局部反映不出来的问题,在全局上很可能暴露出来。不过本次作业的各模块之间耦合度很低,没有进行集成测试的必要。

压力测试是一种软件测试方法,通常用于测试应用程序或系统在高负载、高压力情况下的性能和稳定性。

        压力测试在工程上有广泛应用。在U2中可以表现为在一瞬间同时投放大量请求,从而检测代码的健全性。在本次U3作业中则表现为在大量Person(100)与大量社交关系(完全图)的情况下投放大量数据查询请求时是否会超时。同样要测试id为int最大值,tagid,personid,messageid相同的特殊情况等。

回归测试是指修改了旧代码后,重新进行测试以确认修改没有引入新的错误或导致其他代码产生错误。

        回归测试是一种常用的测试方法。当进行代码的性能优化后,或者进行重构后,都要重新运行已经通过测试的测试样例并与正确答案对比,重新在评测机上运行,防止代码逻辑原先正确的部分出现新的错误。

数据构造策略

        数据构造策略在上一部分已有体现,总结起来:

  • 根据JML规格自行构造样例,尽量做到覆盖每一个数据可能性
  • 测试大量Person(100)与大量社交关系(完全图)的情况下投放大量数据查询请求时是否会超时
  • 使用JUnit单元测试
  • 测试id为int最大值,为0,tagid,personid,messageid相同等特殊情况
  • 充分利用自动化工具(评测机)测试

BUG分析

代码漏洞分析(性能问题)

第九次作业

简化queryTriCount的实现,采用动态更新;

为了提高countCommonNeighbour的效率,为MyPerson引入AcSet属性;

为了提高getPerson的效率,将persons由ArrayList改为HashMap;

为了提高isCircle的效率,引入压缩路径的并查集算法,省去不必要的路径压缩操作;

为了提高mr,ar速度,将values,acquaintance改为HashMap;

理解qbs的意义后,利用并查集对其进行大规模优化。

第十次作业

queryCoupleSum方法中我原先用了双重for循环进行遍历,从而导致CTLE;修改后将其更改为单层for循环,降低了时间复杂度,从而修复了strong 3,strong 4两个点的bug。

第十一次作业

并未出现bug。

规格与实现分离

//@ public model non_null int [] elements;

/*这两个方法的规格必须建立在IntHeap所管理的数据规格上,因此为了准确说明这两个方面的规格,首先给出了
IntHeap所管理的数据规格,如第5行所示。其中的model表示后面的 int[] elements 仅仅是规格层次的描述,并不是这个类的声明组成部分,此外也不意味该类的实现人员必须提供这样的属性定义。*/

        以上面的这一段JML为例。在HW9的最开始,我并没有很深刻地体会到JML手册中关于数据规格的这一段描述的含义,以为persons只能通过数组类型进行管理。可是这样做为我的方法实现带来了很大麻烦,数据结构的限制也使我的时间复杂度很高。事实上,数据规格描述只是为了方便后续方法的JML描述所必须“预设”的前提,与实际代码中的实现并没有很大关系,即规格与实现分离,persons可以用ArrayList,或HashMap管理。

        方法的规格也同理。按照JML来实现会导致效率低下,时间复杂度高。在代码中你只需要保证这样实现的结果与JML给出的结果一致。JML与实际代码能够一一对应,运行后的结果与按照JML实现的结果一致,这就足够了。说到底,JML是为了准确的描述一个方法都做了什么,实际代码如何实现跟它并无太大关系。

JUnit测试分析

设计

        JML严格规定了一个方法的运行结果,这方便了JUnit测试的进行,我们只需要对照着JML规格一条条验证,只要有一条断言错误,就证明方法不满足JML规格。

        JUnit内部需要实现一个“标程”代码,通过对比一个网络在标程代码与待测试代码运行后的结果是否相等来判定运行结果是否正确。

        当方法名带有/*@ pure @*/ 标签,就说明这个方法是安全的,不会改变任何属性,仅做查询与后续处理。此时我们还需要测试,待测试方法是否改变了不该改变的对象信息,需要对比运行前后同一对象是否发生改变。

效果

        JUnit有效地测试了方法的实现是否符合JML规格,事实上也开拓了我的思路。我以前认为单元测试只是判定方法的返回值是否正确,并没有想到可能会对无关对象产生改变,等等,这足以体现JML的严谨性。

学习体会

        OO的第一单元第二单元结束后,U3的确让人松了一口气。因为代码的架构基本已经固定,难点都在于时间复杂度的优化上,而优化方法也都大同小异,这样本单元的难度就较低,但绝不是失去了其思维含量。

        本单元中我经常专注于一些优化的细枝末节,比如想到了使用TreeSet并自定义比较器这种花哨的方法,但是仍然CTLE,最后发现是采用了不必要的双重for循环,捡了芝麻丢了西瓜,这种情况在之后万不可再出现。

        必须要感谢本届OO课程组与助教,世上还是好人多啊。听说上一届的JML规格代码不能利用已经造好的轮子,都是从最开始慢慢写起,我完全不敢想象会是什么场景,还好本届助教引入了“轮子”和pure标记,大大减轻了我们眼睛的压力。本届OO也没有要求手搓JML规格。还有与上一届相比题目也更平易近人,没有卡时间特别死,最后变为卷奇怪算法的大赛。毕竟这一单元是讲JML规格,而不是讲算法。助教非常耐心,能力也强,十分感谢。

        但是说起来总觉得有些奇怪。U3是讲规格的单元,但是我们作业的主体却不是在规格上,而是在优化性能上,也说不清楚自己在学什么了。不过完全可以理解,因为纯粹考察规格确实能做的不多,通过图相关的东西让我们体会规格与实现分离的理念。JUnit与本单元的主题契合的很好,从JUnit与JML严格对应就可以看出。

        总之U3的学习还算平稳。希望自己有始有终,在最后的U4能够再接再厉。

  • 18
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值