前言
第三单元的主题主要是以基于JML实现一个社交网络。三次作业总体上难度不大,按照JML并在部分数据结构上做好优化即可。JML虽然很繁琐,但是在某些场景下确实可能有他的用武之地,这个单元让我们初步体验了契约式编程,相较前两个单元轻松不少。
测试分析
黑箱测试与白箱测试
-
黑盒测试是数据驱动的测试方式,将被测软件视为一个无法打开的黑盒。测试人员不了解软件的内部结构,只关心输入和输出以及系统的功能。
-
黑盒测试往往需要大量的数据来检验软件实现是否正确,但是效率比较低,可能比较难以覆盖各种情况
-
白盒测试是逻辑驱动的测试方式,目标是对软件的内部结构和逻辑进行分析。测试人员需要了解源代码和程序的全面结构。
-
由于测试人员了解全面结构,所以可以针对性的构造测试数据,比如根据分支覆盖率测试。针对性较强,但是可能比较费时。同时由于可能受到已有思维的限制,对一些情况没有考虑全面。
单元测试
- 定义:针对最小功能模块进行的测试,通过编写测试用例来验证每个函数或方法在各种输入条件下的正确性。
- 目的:确保每个模块被正确编码,及早发现并解决潜在错误。
功能测试
- 定义:验证软件的功能是否符合用户需求和设计规格的测试。
- 目的:确保所有功能需求得到满足,包括安全性、容错性、兼容性等。
集成测试
- 定义:将通过单元测试的模块组合在一起,测试它们作为一个整体时的行为。
- 目的:发现与模块接口有关的问题,验证模块间数据流和控制流的正确性。
压力测试
- 定义:在超出正常运行条件的环境下,测试软件的稳定性和可靠性。
- 目的:确定软件的负载极限,评估在高负荷下的性能表现。
回归测试
- 定义:在软件修改后,重新运行之前的测试用例以验证修改是否引入新的错误。
- 目的:确保原有功能和性能没有受到影响,维护软件的稳定性和质量。
数据构造
- 通过随机生成的大量数据来保证大部分情况下不会出错
- 手动构造极端数据来保证边界条件或者运行时间满足要求
第一次作业分析
总体分析
第一次作业难度不大,主要是添加人,添加关系,修改关系,查询图中存在的一些结构。
UML类图
架构分析与性能优化
-
存储结构:对于MyPerson和MyNetwork中要存储的数据基本使用了HashMap,以保证查询的效率。
-
并查集:对于查询连通性,使用路径压缩的并查集可以查询任意两个人是否是联通的。
-
动态维护BlockSum和TripleSum:在对人和关系进行修改的时候看是否需要对这两个维护的值进行修改。
-
并查集的删除:当需要对某条边删除时,这个连通块就可能会受到影响。此时需要对这个连通块进行重建并查集,如下图。删除3,4之间的边,需要利用Person存储的相邻的人的数据,对3或4进行bfs或者dfs得到删除边之后此人的连通块,如果仍然包含另一人,则说明对连通性没有影响;不然,对两人的连通块分别进行并查集的重建,不妨设两人为父节点。
······ bfs(id1);// 利用bfs得到id1连通块中的人 boolean iscontain = set.contains(id2); int fa = findFather(id1); if (!iscontain) { cntBlock++; HashSet<Integer> set2 = new HashSet<>(); for (int i : father.keySet()) { if (findFather(father.get(i)) == findFather(fa)) { set2.add(i);//得到原来连通块中的人 } } for (int p : set) { father.put(p, id1); } set2.removeAll(set);//剩余为另一连通块中的人 for (int p : set2) { father.put(p, id2); }
bug分析
本次作业未出现bug,不过可能存在dfs爆栈的问题,比如上面的查询连通块若使用dfs,所以尽可能使用bfs。
本次作业要注意时间效率,尽可能控制在单个指令时间效率<=O(n)。
第二次作业分析
总体分析
第二次作业添加了Tag类,并增加了query_tag_value_sum,query_tag_age_var,query_best_acquaintance,query_couple_sum,query_shortest_path等方法。
这些查询量大多采取动态维护,在查询时效率就比较高。
UML类图
架构分析与性能优化
-
query_tag_value_sum,query_tag_age_var 两个函数查询tag的相关量,需要在对tag操作时进行。
值得注意的是要注意除数为0时单独处理。
方差不好直接维护,可以由下公式拆成几部分进行维护。
( ∑ i = 1 n a g e i 2 ) − 2 × a g e M e a n × ( ∑ i = 1 n a g e i ) + n × a g e M e a n 2 n \frac{(\sum_{i=1}^nage_i^2)-2\times ageMean\times(\sum_{i=1}^nage_i)+n\times ageMean^2}{n} n(∑i=1nagei2)−2×ageMean×(∑i=1nagei)+n×ageMean2 -
query_best_acquaintance,query_couple_sum 这两个函数在任何对人或者关系进行修改时都需要维护每个人的best_acquaintance,细节比较多。
-
query_shortest_path 查询图中两点的最短路,使用bfs即可。
bug分析
本次作业互测中出过一个bug
if (person.getId() == bestid) {
bestid = -1;
for (MyPerson p : acquaintance.values()) {
if (bestid==-1 || value.get(p.getId()) > value.get(bestid)) {
bestid = p.getId();
} else if (Objects.equals(value.get(p.getId()), value.get(bestid))
&& p.getId() < bestid) {
bestid = p.getId();
}
}
}
当某个人的id就是-1的时候,尽管他可能是best_acquaintance,也会直接把他替换。所以使用任何可能的ID作为默认ID是不妥的。可以用一个变量标记是否开始查找。
if (person.getId() == bestid) {
bestid = -1;
boolean flag = false;
for (MyPerson p : acquaintance.values()) {
if (!flag || value.get(p.getId()) > value.get(bestid)) {
bestid = p.getId();
flag = true;
} else if (Objects.equals(value.get(p.getId()), value.get(bestid))
&& p.getId() < bestid) {
bestid = p.getId();
}
}
}
同时,发现同房的人qtvs由于没有维护,使用两重循环导致超时。
第三次作业分析
总体分析
第三次作业添加了Message类,有红包,表情消息等几个子类,需要符合接口要求,发送消息可能会对人的某些属性产生影响。
UML类图
架构分析与性能优化
第三次作业基本不涉及比较复杂的算法,按照规格实现即可。
bug分析
对于sendMessage的群发模式,这个Tag应该是从message中得来,Person的Tag可能会被删除或再添加。在强测之前有同学提出了这个问题,发现我的实现有问题,进行了初步修改。在强测之后再次发现了问题,是由于没有size = message.getTag().getSize()
,而是由Person的Tag得到Size,第一次修改遗漏导致出了问题,所幸强测和互测没有出问题(感谢同房的同学)。
if (getMessage(id).getType() == 1) {
Message message = messages.get(id);
messages.remove(id);
person1.addsocialvalue(message.getSocialValue());
((MyTag) message.getTag()).addSocialValueInTags(message.getSocialValue());
if (message instanceof RedEnvelopeMessage) {
int size = message.getTag().getSize();
if (size > 0) {
int money = ((RedEnvelopeMessage) message).getMoney() / size;
person1.addMoney(-money * size);
((MyTag) message.getTag()).addMoneyInTags(money);
}
}
规格与实现分离
规格定义了软件的功能和行为,它是对软件外部可观察行为的描述。而实现是如何做到规格所描述的要求,要考虑效率,可维护性,可拓展性等。
规格与实现分离可以提高抽象层次,可以专注于“做什么”而不是“怎么做”,这有助于清晰地理解问题。同时可以更容易地在不同模块间共享接口。测试可以针对规格进行,而不必关心实现细节,这有助于提高测试的有效性和效率。
在本单元的作业中,规格与实现分离通过JML表达,JML描述了某个方法的基本功能规格,但是实现方式由我们决定,比如选择合适的容器,采取合适的算法和设计模式,来保证效率。
Junit测试
本单元进行了三次Junit单元测试。第一次对于qts测试,由规格中没有涉及到其他部分,只要采取随机数据生成多个图计算即可。第二次对于qcs测试,数据生成基本同于第一次测试,不过这次测试保证了生成数据包含稠密图和稀疏图才通过,在第一次情况下更进一步。第三次有所变化,针对dce,确保生成消息和发送消息的多样性,才通过测试。
本单元重拾Junit,根据JML规格逐条编写测试代码,在不了解源码的情况下也可以进行测试。
心得体会
-
本单元学习了JML规格化编程,与前两个单元很不一样,初步体验了契约式编程。
-
本单元需要仔细阅读JML代码,无疑是一件比较繁琐的事。借助AI工具初步解读JML规格后,再去仔细阅读,防止解读错误或者遗漏,帮助我提高了编程效率。
-
本单元对于图论只是浅尝辄止,但是仍然帮助我们重温了部分图论知识。
-
本单元整体上比较简单,但是仍然存在易错的地方,不能太大意。