BUAA OO Unit3 总结

BUAA OO Unit3 总结



0. 前言

本单元旨在学习契约式编程的相关知识。不同于以往的指导书形式的用自然语言描述功能要求,此单元作业的方法规格功能约束都在官方代码中以jml语言的形式呈现,最终实现了一个简易的社交网络系统。同时,在性能方面,也通过规格和实现的分离考察了图相关的一些算法。

1.测试分析

1.1 黑箱测试与白箱测试

1.1.1 黑箱测试

黑箱测试又名为功能测试,主要测试软件的功能,不关心程序内部的实现结构,只关注程序的输入输出,被测程序像是一个黑盒。以本次作业为例,相当于随机生成对程序功能范围内的操作指令,检测程序的输出是否符合预期。

1.1.2 白箱测试

白箱测试也称为结构测试,目标是对软件的内部结构及其背后的逻辑进行分析,可以看见程序内部的模块进行精细的覆盖测试。以本次作业为例,相当于针对规格里规定的各个分支情况,编写JUnit单元测试,检查所有分支的逻辑是否正确。

1.2 各类测试

1.2.1 单元测试

单元测试通常是白箱测试,是对软件中最小可控制单元进行检查和验证的过程,例如对代码的某个重要功能方法编写测试程序进行测试以确保模块被正常编码。

1.2.2 功能测试

功能测试是一种黑箱测试,关注于系统的功能性需求,检验系统是否依照功能需求正常运作,例如检测系统的边界情况、异常处理情况、业务流程等。

1.2.3 集成测试

集成测试是在软件系统集成过程中进行的测试,目的是检查软件单元之间的接口是否正确,例如将已通过单元测试的模块/方法组合成需要的程序机构时采用增量继承而非一次性的继承

1.2.4 压力测试

压力测试是评估系统在负载增加的情况下的性能和稳定性的过程,例如模拟高负载、大数据量、高并发的情况,确定系统的极限容量和相应实践,有助于发下系统在压力下的崩溃和性能问题。

1.2.5 回归测试

回归测试指进行代码迭代后,重复此前相同的测试,以确保修改没有引入新的错误或导致其他代码出现错误,确保了现有功能的稳定性。

1.3 数据架构

数据架构上,先是用大量随机生成的大量指令与朴素算法对拍测试程序的功能是否符合要求,然后针对指导书中所描述的数据限制,以压力测试的方法构造极端的需要大量计算的数据测试分别测试特定指令,检查极端情况下程序的性能。

2.架构设计

本次作业中课程组已经限制了各个方法规格和数据规格,整体架构大同小异。各个分析指标中,除了MyNetwork类的代码量稍大外并无明显不合格之处,故在此按下不表。

2.1 图模型构建

将社交网络MyNetwork抽象为图,每个参与其中的MyPerson对象就是一个点,人与人之间的熟人关系是带权无向边,在全局唯一的这张图上跑图上的算法以完成功能需求。图的存储用HashMapPerson封装的属性以及新建的Link类一起构成了点索引的邻接表数据结构。

2.2 维护策略

该图是动态的,所以需要维护,主要有加人、加标签、信息传递、增减改边这些修改操作。因此需要图的增加节点、增加/删除边,修改边权这些基本的动态维护操作,这些可以通过HashMap容器高效地完成。此外,对于连通块数量、部分边权和等数值,若在每次查询时都重新计算一次会带来巨量的时间开销,不适用于大量查询的情况,因此也可以采用动态维护和缓存的策略,具体如下:

  • 缓存策略:对查询数指设置脏位,当数据所相关的结构发生变化且没有进行动态维护时,使脏位有效。查询时若脏位有效则将数值重新计算、存下新数值并无效化脏位,否则直接返回之前存下的缓存数值,减少了查询开销。

  • TagValueSum:标签内的边权和,增/删/改边时遍历所有tag,并维护相关的tag的数值。

  • TagAgeVar:标签内年龄方差,tag内部加减人时维护ageSum年龄和,ageSquareSum年龄平方和,依据公式可以求出方差,不用每次查询B都遍历所有相关人。

  • BlockSuch:连通块个数,简单的并查集只支持加边的维护而不支持删边的维护,一开始采用了每次使用搜索查询删边后是否连通并部分调整并查集来实现动态维护,后面改用了删边时设置脏位以及查询时取缓存数据或重建并查集统计的方法。

  • TripleSum:三元环个数,采用了有限更新的策略,加减边动态更新时遍历端点所关联的边,查询是否构成三元环并更新数值,当更新次数达到一定限制后不再更新而是设置脏位,在查询时用依据点的度数重建有向无环图的方式统计三元环,这样在大量查询和大量修改的极端的数据中都能有相对良好的性能表现。

  • BestAcquaintance:最亲密朋友(边权最大的相连边的端点),在Person点内部重写PriorityQueue优先队列的比较器实现大根堆来维护该性质。

2.3 UML类图

在这里插入图片描述

3.问题与修复

3.1 性能问题及修复情况

3.1.1 连通块数量BlockSum

第一次作业中出现了BlockSum相关的超时bug,原先采用了动态搜索维护并查集的方法,理论上该方法相较于后续修复改用的缓存重建机制也能在限制时间内完成功能需求,但是在由于搜索查询中使用了ArrayList来标记遍历过的点,其contains()方法是O(n)的,每次扩展一个点都要顺寻查询,使得原先预计O(n + m)的维护算法恶化成O(n^2 + m),现在复盘发现采用将ArrayList替换成HashSet的方式来实现对特定节点编号的O(1)访问查询。

3.1.2 边权和TagValueSum

第二次作业中出现了TagValueSum相关的超时bug,由于采用了每次查询时使用双重循环计算的方式使得在大量查询的边界数据下程序性能不佳。由上述分析可知,可以利用上一次计算的数值设置缓存来进行修复。

3.2 其他问题及修复

在第三次作业中,出现了TagValueSum相关的功能bug,在第二次超时并采用缓存机制修复后,又想采用有限动态维护的方法,在加人和减人时维护该值,仅在改边时设置脏位。这种策略忽略了加边和减边时对应的数值也会变的情况,同时没能很好地复用之前的单元测试代码进行成分测试,黑盒测试的随机性较大十分“幸运”地在多次混检中没查出bug,最终导致了bug。修复方案只需在加边和减边时遍历tag维护相关值即可。

3.3 规格与实现分离

由上述分析可见,规格规定更像是朴素的、直接模拟需求的代码功能规定,这样直接严谨直观遍历所有状态空间的算法往往是性能不佳的,为此在实现具体的业务逻辑时,可以采用更为高级和抽象的算法提高性能,也就是规格和实现分离。需要注意的是,在这个过程中应该时刻确保实现要完全符合规格的约束,和规格的行为一致,在正确性的基础上追求更好的性能。

4.JUnit测试分析

4.1 测试设计

本单元中借助课程组提供的jml规格约束可以按部就班的编写测试函数,使用条件标记位的方式可以简便地实现测试需求,例如对于全称约束\forall:

boolean flag = true;
for (int i = 0; 0 <= i < test.length; i++) {
    if (!test[i]) {
        flag = false;
        break;
    }
}
assertTrue(flag);

对于存在约束\exist则可以:

boolean flag = false;
for (int i = 0; 0 <= i < test.length; i++) {
    if (test[i]) {
        flag = true;
        break;
    }
}
assertTrue(flag);

下一步的关键在于如何生成数据,JUnit本身提供了批量测试数据的生成方法,可以十分方便地将多组数据一起进行自动化测试,达到大量测试的目的。但是若采用随机生成的策略生成数据有可能不能覆盖所有业务场景的分支情况,同时,JUnit单元测试本身是一种静态测试,在本单元需要支持动态维护的场景下若动态操作的维护方法实现不正确将无法检测出此类错误,若在测试方法中调用方法外部的动态维护方法又有违单元测试的设计理念,也导致了本单元中的一些bug。可以采用生成特定情形的数据和将动态操作转变成前后两份静态数据来测试这种情况。

4.2 测试效果

依照jml本身编写的测试代码在静态测试场景下表现良好,能检测出课程组的bug代码的错误,但是由于没有对动态维护需求等特殊场景考虑周全,在最后一次作业中没能测出本地的作业代码的bug。

5.心得体会

本单元的学习初步介绍和实践了契约式编程,学习了如何依照规格编写代码以及如何设计规格本身。初见jml未免觉得冗长,相较于之前的作业,此次在编写具体方法时可以做到不需要理解全局的业务流程也可以依照规格要求编写代码,也更有螺丝钉的感受。通过本单元学习,深刻体会到契约式编程在团队开发中的作用,了解了规格设定是如何在大型项目中将业务拆分分发以实现高效开发和正确性维护。

  • 9
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值