OO第三单元总结:规格化设计

OO第三单元总结:规格化设计

测试过程

黑箱测试与白箱测试:

黑箱测试:测试者将软件视作一个内部不可见的黑箱,不需要了解软件内部的工作原理或源代码,只通过软件的输入输出来判断正确性与评估性能。

白箱测试:要求测试者通过查看源代码,了解程序内部运行逻辑和结构。通常涉及到代码覆盖率、逻辑路径测试、错误处理等方面,要求所有的内部路径都经过测试,需要测试者对软件的内部实现有足够的了解。

在实践中,黑箱测试和白箱测试通常结合使用,以确保软件的质量和可靠性。黑箱测试关注于软件的功能和用户交互,而白箱测试关注于软件的内部逻辑和结构。通过这两种方法的互补,可以更全面地测试软件,提高软件的质量和性能。

本单元的学习过程中使用了黑白箱结合的测试方式:使用评测机进行黑箱测试,判断基本的输入输出是否正确;如果发现错误,则使用白箱测试,构造针对性的测试用例,对具体代码进行阅读调试,最终找到bug所在之处。

多种测试的理解
  • 单元测试:对软件中最小的可测试部分(某个方法)进行的测试,通常使用断言来检查代码的行为是否符合预期。单元测试有助于及早发现错误,简化调试过程,避免后续迭代开发过程中错误的积累。
  • 功能测试:也称为黑盒测试,它关注软件的功能需求是否得到满足。从用户的角度触发,不关系内部实现,只关心软件提供的服务是否能够按照既定需求工作。可以手动进行,也可以通过自动化测试工具来执行。
  • 集成测试:检查不同模块或服务之间接口和交互的测试,当各个单元或模块通过单元测试后,它们需要被组合在一起进行集成测试,确保它们之间的协同工作,用于发现接口不匹配、数据传递错误等问题。
  • 压力测试:用来验证软件程序在高负载或极端工作条件下的性能和稳定性,用于测试软件的极限处理能力。压力测试有助于识别性能瓶颈、内存泄漏等问题,保证软件在实际使用过程中的可靠性。
  • 回归测试:是在软件已有功能上进行的测试,确保引入的代码更改或者bug修复没有破坏现有功能,当软件发生更改,如添加新功能、修改原有功能或修复bug时都需要进行回归测试。
数据构造策略
  • 综合考虑稠密图与复杂图
  • 构造极端数据进行压力测试(如qtvs)
  • 扩大数据规模
  • 手动构造特殊数据,对边界条件进行检测

作业架构设计

HW9

第九次作业要求我们实现一个简单的社交网络,根据NetworkPerson两个接口的JML表述进行代码设计。在阅读并理解各个方法的JML后,主要的实现便是类的数据结构的选择与算法的实现,整体难度不大。

本次作业实现的重要函数为isCirclequeryBlockSumqueryTripleSum

并查集

isCircle函数用于判断两个节点之间是否连通,为了简化这个操作,我选择了使用并查集来维护整张图的连通性。

基本操作

并查集是一种用于管理元素所属集合的数据结构,它为每个节点赋予一个“代表”,用于表征不同节点所属集合的关系。顾名思义,并查集需要支持合并查询两种基本操作,我们使用容器HashMap<Integer,Integer> father来记录节点与其父节点之间的关系:

public int find(int id) {
    int result = id;
    while (father.get(result) != result) {
        result = father.get(result);
    }
    return result;
}
public void union(int id1, int id2) {
    int rep1 = find(id1);
    int rep2 = find(id2);
    if (rep1 == rep2) {
        return;
    }
    father.put(rep2, rep1);
}

优化

基本的优化方式有两种:按秩合并与压缩路径。

  • 路径压缩

​ 在查询过程中,对于同一个集合(连通分支)中的节点,他们都可以选择本分支中的父节点作为代表,这样在判断节点的连通性的时候便可省去重新查询的过程,直接比较父节点即可。
在这里插入图片描述

  • 按秩合并

​ 在对两个集合(连通分支)进行合并时,为防止树的退化,我们使用容器HashMap<Integer,Integer> rank来记录当前树的高度。对于初始化的节点,其父节点为自身且秩为1,在执行union操作时将秩小的分支合并到秩大的分支中。
在这里插入图片描述

删边引起的并查集的维护

虽然并查集在判断两个点是否连通上具有不错的性能表现,但是它无法记录整张图的状态,即当modifyRelation导致某条边被删除时,整张并查集的状态就要进行更新,此时便需要对并查集进行维护。

我选择染色法进行并查集的更新:

  • 对于将要删除关系的两个节点A和B,首先将其父节点各自指向自身;
  • 选取其中一个节点(A)开始进行BFS染色(使用Acquaintance依次访问相邻节点),将经过的节点的父节点设置为A节点;
  • BFS完成后,判断另一个节点(B)是否已被染色:
    • 如果已被染色,说明两个节点仍处于一个连通分支中,重建结束(此时blockSum保持不变)
    • 如果节点B未被染色,说明A与B间的边为桥,开始对B节点进行BFS染色(此时blockSum++
动态维护

对于查询类指令,如果单纯根据JML的表述,在每次查询时才进行遍历查询(三元环的查询甚至需要三层循环),显然不符合性能要求,因此我们需要在网络建造的过程中实时维护这些待查询的值。

这样的作法会不可避免地增加增删关系时的成本,在处理增删关系指令大量出现时表现不佳,但是三次作业下来也并没有出现超时的情况。一种可能的解决方法是设置脏位,在关系修改时设置脏位,查询时进行延迟重建并且取消脏位,但我没有采用这种方式。

HW10

第十次作业新增了标签Tag,用于为联系人设置分组,主要需要实现queryShortestPathqueryCouplequeryTagValueSum三个函数。

queryShortestPath用于查询两个人之间通信需要经过的最少的“中介”,本质上是一个最短路径问题。考虑到此处的路径为无权的,因此使用BFS即可解决。

queryCouple用于查询网络中“最佳好友组合”的组数,我仍然采用了动态维护的方法,在增加关系与调整关系时维护coupleCnt这一属性。这一部分需要讨论的情况比较多,讨论起来容易晕圈,不过只要理清思路实现起来难度也并不大(节点AB间的边p被“修改”):

  • 只有在两个节点中存在“最佳搭档”改变的情况时,我们才需要进行coupleCnt的维护
  • 将两个节点各自的bestAcquaintance保存下来
    • 如果节点A的newBestAcquaintance变为节点B,则判断原有的couple是否被破坏;否则判断是否新增了couple。节点B进行同样的操作。
  • 最后对公共边p进行讨论,如果此前A与B为couple,则coupleCnt–;如果之后A与B成为couple,则coupleCnt++。

queryTagValueSum用于统计标签下所有人的value总和。按照JML里的双重循环表述来实现肯定是不可以的,实际上遍历所有的Tag即可,复杂度仍控制在O(n)数量级,此处的n为Tag的总数目。

其他函数如queryTagAgeVar仍采用此前动态维护的处理方法,整体思路没有发生很大的变化,处理方法也比较固定。

HW11

第十一次作业新增了消息类型Message,本次作业是三次作业中难度最低的,不涉及到具体图论算法的实现,给出的JML描述也基本符合复杂度要求,只要读懂JML并考虑到些许细节即可~~(不过这次的JML是真的长啊)~~。

性能与bug

在本单元的三次作业的强测和互测中,我并没有出现bug,不过在强测中出现了几处险些超时的情况~~(?可能是评测机的问题)~~。在考虑性能的过程中,我们需要明确一点:需要保证所有方法的时间复杂度在O(n)级别,即性能的“木桶原理”。只要某处出现O(n^2)及其以上的复杂度情况,便会有极大可能由构造的特殊数据导致超时。

总的看下来,本单元中我对性能的保证主要体现在:容器的选择、动态维护、优秀算法的选择这三个方面以及性能的“木桶原理”这一基本指导方略。某些地方的吹毛求疵是不必要的,实际上对性能的提升也并不明显。

此处具体讨论以下规格与实现分离的理解:

本单元出现的部分JML篇幅相当长,但它表达的意义却十分简单,比如queryShortestPath这一方法,数十行的JML描述归结下来可以用一句话简单地描述:两点间的最短路径。这样看来,JML语言是否是臃肿甚至是多余的呢?在我看来并非如此。

起初我在看到这一精简的方法名称与这一段相当长的JML语言描述时,便先入为主地认为这就是一个原始的求最短路径的问题,并带着这一观念迅速浏览了一遍JML表述,并没有仔细分析其中的细节,这时候便导致了理解的脱节。此方法表述的实际含义其实是:两个节点之间想要进行联系至少需要经过的中间人的数目,这和我的理解绝对是不对等的,因此我在处理时也就错误处理了相邻节点的情况,但这一点却明确蕴含在JML表述中。

究其原因,问题出现在自然语言的二义性,或者说是模糊性。尽管自然语言具有易于理解的先天优势,但是与我们精准编程的要求似乎是相悖的,处理细节难免会在表述过程中被忽略。这不禁让我想起此前学习离散数学的过程,许多证明题在我们看来是显然的,但仍需要经过严谨复杂的过程一步步证明,其中蕴含的正是逻辑语言独有的严谨性,我们也不应该因其看起来过于冗余而心生厌烦。

JML语言也正是如此,它给出了接口的约束,这种约束需要一个严谨的载体进行传递,即逻辑语言。这种传递方式能够尽可能地避免交接偏差的出现,也正是规格与实现分离的必要之处。这种约束不仅需要需求方实现完整、严谨的规范化表述,还需要编程者选取合适实现方式,尽可能地提高性能,更像一种双向约束,这种双向约束使得工程项目更好地进行。

由于本单元课程出现了一些bug,且实现的方法的规模并不是很高,可能使我们忽略了JML描述所起的作用,产生了厌烦情绪。设想我们此后面对规模极其庞大,功能极其复杂的工程时,想必JML发挥的作用应该足够引起我们的重视,也会在其中体会到规格化设计的必要之处。

Junit测试

本人在编写Junit的过程中基本遵循了以下步骤:

  • 构造基本网络
  • 随机生成数据,保证数据的覆盖足够充分
  • 根据JML编写标程获得正确结果
  • 运行个人实现并对比结果(对于pure方法需要保证没有对象被修改)

从个人体验与同学们在群内的反馈来看,Junit的编写为我们的带来了一定困难,经常会出现改过了一个点另一个点又出错的情况。此处的Junit的编写使同学们的眼光局限于中测的通过上,在编写时大多是缝缝补补,写出来的测试逻辑也并不清晰,如果单纯从测试效果方面来讲,评测机似乎更佳(但Junit也帮本人测出过本地的bug)。或许是项目规模不够,也没有系统地学习具体的编写要求,私以为今后可以通过引入真实工程中的Junit分析来加强这一部分的学习。

学习体会

本单元相较于前两单元来说,强度显著下降,也算是为我带来了一段缓冲期。由于整体架构已由课程组提供,减少了许多架构上的顾虑,更多的精力集中在具体实现层面,有一种在一个优秀架构下做增量开发的舒适感。但在完成此单元的学习后,我感觉个人的收获并不是很多,对JML只能称得上是有了初步的理解,也没有对图的一些算法有更深的认识。

个人认为本单元在设计重点上有些顾此失彼,不过仍衷心希望OO课程越来越好。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值