307
社区成员
发帖
与我相关
我的任务
分享在接触规格驱动开发之前,我写代码往往是“边想边写”,想到哪里补哪里。但这次作业强制要求先阅读JML规格,这彻底改变了我的开发习惯。我对规格驱动开发的理解可以概括为两点:
JML就是一份严格的“契约”,分离“做什么”与“怎么做”。比如在 NetworkInterface 中,规格通过 requires、assignable 和 ensures 明确规定了方法的前置条件、副作用和后置条件。作为开发者,我只需要保证代码的执行结果满足 ensures 的约定即可,至于内部是使用哪种具体的数据结构,JML并不干涉。这给了我极大的底层实现自由度。
自然语言的需求往往会遗漏边界情况,但JML的逻辑表达式却极其严谨。在实现 addUser 或 uploadVideo 时,规格明确列出了抛出 EqualUserIdException 或 UserIdNotFoundException 等异常的精确条件。这逼迫我在编写核心逻辑前,必须先把防御性代码写好,大大减少了后期的Bug调试时间。
在这次作业中,我花了不少精力编写 NetWorkTest.java。通过编写测试,我总结出了几条行之有效的经验:
测试社交网络最繁琐的就是造数据。为此,我专门写了 build1() 和 build2() 这样的工厂方法,用来快速构建包含多个用户和复杂关注关系的初始网络。这让我的 @Test 方法变得非常干净,能够专注于测试具体的业务动作。
我发现仅仅用 assertEquals 是不够的,对象的状态非常复杂。因此我自定义了 compareUsr 和 compareVid 方法。在验证用户是否相等时,我不仅仅对比引用,而是通过调用 strictEquals 方法严格校验用户的 ID、年龄和名字是否完全一致。
我的测试用例没有死板地按方法分类,而是按照业务场景来覆盖,例如空图 (testEmpty)、单向/双向关注 (testSingleDir, testOnePair) 以及状态变更后的对比 (testAfterUnfollow)。
第一次作业网络还只是个简单的有向图。主要的挑战是图的广度优先搜索(BFS)求最短路径,以及维护互相关注的人数。在这个阶段,我学会了放弃 List 换成 HashMap,并初次尝试了动态维护(全局 sum)。
第二次作业业务场景引入了视频分类、硬币、热度。JML 规格中开始出现大量的嵌套遍历(比如求年龄递减最长序列、求单个 UP 主的最佳贡献者)。纯粹的模拟已经行不通了。我在这时引入了分类索引(videosByType)、脏数据标记(Dirty Bit)和记忆化搜索(memo),让查询从 O(N) 降维打击到 O(1)。
第三次作业全局最佳贡献者、个性化视频推荐(recommendVideo)、UP主推荐(recommendNthUp)等极度消耗性能的需求接踵而至。JML 规格写得轻描淡写,但如果照抄规格,复杂度可以到 $O(N^3)$。在这一代代码中,我再次利用缓存缓存机制,做到了循环内的常数级优化。
ensures 子句中出现了对特定属性的筛选条件时,就是重构容器的信号。比如HW2 中新增了按 type 查询最热视频的规格。如果只用全局的 HashMap<Integer, Video>,每次都要遍历全网视频判断类型。因此我立刻写出了 HashMap<String, List<Video>> videosByType 这个二维容器,这就是“基于查询维度的索引化”。虽然JML只描述逻辑,但我们可以通过“数理化”它的规格来预判性能瓶颈。总结下来,揪出瓶颈的方法有以下三招:
如果在 JML 中看到一个 \max 里面套着一个 \sum,这在代码里大概率就是一个 O(n^2) 的双重循环,绝对会 TLE。比如说在 HW3 中求全局最佳贡献者(queryGlobalBestContributor),如果现场算,需要遍历所有 UP 主,再遍历他们的粉丝。为了解决这个瓶颈,我在 Network 中引入了 bestCounts 容器。在每一次发生 coinVideo(投币)动作时,顺手计算并更新旧的 bestContributor 和新的 bestContributor 的次数。将原本 O(n^2) 的查询,平摊到了每一次投币的 O(1) 操作中。
识别“读多写少”的图查询,引入“脏标记缓存”,当一个涉及全图遍历的查询(如 HW2 的求最长年龄递减序列 queryLongestDecSeq)可能被连续调用多次,而中间没有任何节点发生变化时,重复搜索是极大的浪费。我设计了 dec 缓存和 memo。只有当 addUser 或 followUser 等真正改变图拓扑的写操作发生时,才调用 graphChange() 清空缓存。这种机制让连续查询瞬间返回。
本单元作业只出现了两个bug,一个是第一次作业的时候没有注意控制时间复杂度,导致双层循环超时,后期一直注意减少时间复杂度,没有再TLE过。
第二个bug是因为删除评论包含的字符串参数可能为空,并没有注意到这个细节,所以出错了。
我发现有人在传递过程中漏写了相关变量的类型,比如整型,而我自己也不小心看错了一次数据的边界条件,导致数据范围变化。
我认为还是JML和自然语言双重验证比较合适,并且善用git,让所有人对彼此的改变可见,也可以维护一个共享文档,撰写要求的人可以加粗标出自己认为别人可能遗漏的地方,并且支持大家随时询问和回答。