关于架构
本单元的核心任务是根据接口提供的需求,写出其具体实现。
三次迭代的递进关系并不很强,更像是不同模块的各自实现。
第一次迭代明确了Network作为中枢,并引入了核心对象person与tag
第二次迭代引入account模块与article系统
第三次迭代则引入了综合性很强的message系统,其中涉及到了tag与article的相关内容
由于接口的需求很明确,整个图模型的架构相对确定,我们的核心内容主要只是完成方法与辅助方法。
关于测试
本单元对测试的要求较高,中测的核心问题主要出现在Junit测试中。
但为了通过中测所进行的测试,往往是最简单的单元测试。
因此哪怕已经经历了一整个单元,我对许多其他测试的具体内容仍然是完全陌生的。
单元测试
针对软件中的最小可测试单元进行的测试,确保每个单元能正常工作。
特点:
- 粒度最小:聚焦单个单元,测试范围独立。
- 白盒测试为主:需了解代码内部结构,设计测试用例覆盖不同分支、边界条件等。
功能测试
基于软件需求规格说明书,验证功能是否符合用户预期,不关注内部实现细节。
特点:
- 黑盒测试为主:从用户视角出发,仅关注输入与输出。
- 粒度较大:对整体进行测试,系统性强,只能鉴定问题的存在性,而很难做好定位
集成测试
将多个单元或模块组合起来,测试它们之间的接口和交互是否正常。
特点:
- 关注接口与协作:测试重点往往是模块间的依赖关系和数据流动。
- 渐增式测试:一般采用自顶向下、自底向上或混合方式逐步集成模块,便于定位模块间交互出现异常的位置。
压力测试
通过模拟高负载、高并发场景,测试系统在极限条件下的性能、稳定性和容错能力。
特点:
- 破坏性测试:常常故意施加超过正常范围的压力。
- 关注性能指标:如CPU time,Running time,内存等指标。
回归测试
在发生如代码修改、功能新增等变化后,重新测试已验证过的功能,确保变更未引入新缺陷或破坏原有功能。
特点:
- 重复性测试:可重复执行历史测试用例来检验是否破坏原有功能。
关于大模型
本次作业我使用大模型的地方并不是很多,但第六次上机实验给了我很大的启发。
上机实验中的三个阶段分别是:
- 直接生成
- 给出相关知识与解释
- 给出具体的示例
在根据这三个步骤实现之后,大模型的表现确实有很大进步。首先给出任务目标,然后再明确期望格式和核心目的,采用的是采用目标层
→约束层
→示例层
的三层结构,减少模型理解偏差。
在实际使用过程中,我还有一些其他的使用体验,例如动态反馈。其实这个是我使用大模型时的常态,究其根本原因,可能便是对自己要解决什么问题都不清楚。于是在大模型的输出中进一步寻找问题,寻求解释,从而一层一层地解决问题。虽然最后一般也有不错的结果,但是还是相对耗时。
可能由自己直面问题,将核心目的拆解出来再借助大模型,会有更好的效果。
关于性能
规格与实现分离。规格是题干,实现是题解。题干限定了你的结果,但是做题的过程由你的实现所决定。一道选择题可能大家都能写出正确答案,但是不同人的思考模式与做题技巧是不同的。如果你的实现能够满足规格的所有要求,那么这个实现便是正确的,但未必是优秀的。
在本次作业中,优秀的评断主要体现在TIME。因此我们一般要考虑构建一些数据结构,试图以空间换时间。
并查集
在query_circle中,我采用了并查集的方法,结合了路径压缩与按秩合并。
并查集的核心思想就是为一个连通分支选出一个代表结点。
路径压缩:
|
|
按秩合并:
|
|
但是并查集便于增添,不便于删改。当某段关系被解除,路径压缩后的并查集不能再判断出这个人是否会脱离该连通分支。因此此时须考虑重新构建并查集。
增量化设计
这一优化对我的影响相对较深,至少让我首次意识到size的维护居然能避免一次大遍历。
增量化设计的核心思想就是在变化时对数值做维护,在查询时直接得到当前结果。或者说这是一种在线算法,求值不从头开始,而是由上一状态+变化过程而得来。
在query_tag_value_sum
,query_triple_sum
和query_tag_age_var
中,我主要使用了这一思想。具体实现上的易错点就不必在这里赘述了。
缓存与脏位
缓存类似于增量化设计,但缓存的内容不一定是正确的。
某些值虽然我知道在某个过程中发生了改变,但是维护起来相对复杂,甚至效率堪比重新计算。倘若在每次改变的时候都进行维护,虽然精准,但会造成效率的损失。
例如均值是方便增量化维护的,而方差就不方便。
因此可以选择设置dirty
位,仅在需要查询时再进行缓存数据的更新。
我在query_best_acquaintance
,query_best_contributor
,query_tag_age_var
以及并查集的重建
中主要使用了这一思想。
MyContainer
主要解决在有序列表中按值删除对象中的遍历问题。
Linkedlist固然好,但是它只是能很快的删除某个结点,内部不存在值与结点的映射。因此只需仿照Linkedlist构建一个类,但增加一个属性HashMap用来由值索引到结点。(因为一个值可能对应多个节点,所以可以考虑HashMap<Integer,HashSet<...>>
)
在hw10强测中因未采用这一数据结构而发生了CTLE。
关于Junit
我在本单元的研讨课上较多地讲述了如何由规格构建测试的方法论。在此将主要内容进行重述。
自动化测试基本方法
-
@RunWith(Parameterized.class)
切换到 Parameterized 运行器,让测试类依据多组参数生成多个Test对象,多次执行同一个测试方法,避免了手动编写大量重复的测试代码。
@Parameterized.Parameters
定义一个返回测试数据集合的静态方法,返回的集合包含testNum个小集合,每个小集合包含生成Test对象的参数。 -
多次@Test
手动构建测试样例,覆盖能够触发 JML 规格中所有可能行为的输入数据和对象状态。
基于规格的数据生成
- 数据限制
- requires明确了方法的某种输入约束(如参数范围、对象状态)。
- invariant是类级别的约束,必须确保测试数据满足这些基本约束。
- 多场景构造
- 正常场景:构造满足前置条件的典型输入,触发 JML ensures 条款中的正常行为。
- 边界场景:构造边缘输入,如空集合、边界值、单一元素等。
- 异常场景:构造违反前置条件的输入,触发 JML signals 条款中的异常。
- 复杂场景:构造具有复杂关系或大规模数据的场景,测试方法在高负载下的正确性。
基于规格的断言生成
invariant -> 基本数据要求,可断言执行方法后是否满足
constraint -> 数据变化要求,可保存old信息进行断言
assignable -> 对没被assign的变量做pure断言
signal -> 异常捕捉方法或assertThrow
ensures -> 方法核心效果的断言
pure -> 保存old信息(或备份信息)进行断言
关于心得
本单元整体给我的感觉是比较舒适的。除了hw10的实验预习题中那颗树的JML让我感到深刻畏惧,其他的JML读起来都让人很明白自己需要做什么。哪怕是看起来最复杂的sendMessage,很多行JML也只是对应一个小方法的调用。而有些言简意赅的JML,背后却需要十分复杂的实现。
有了颜色标注之后,阅读JML的枯燥与混乱已经基本消失殆尽。
但是JML的书写对我来说仍然是极为困难的课题,哪怕强如助教也会在官方包出现小的失误。但JML作为根本要旨,要保证绝对的严格与严谨,书写JML需要有对方法极高的概括力,还有对细节的全面把握。不过学完了这个单元,我们终于可以说自己走出了了解JML的第一步。