面向对象 Unit3 总结:规格、测试与协作

王竣翔-24373372 2026-05-28 10:24:09

面向对象 Unit3 总结:规格、测试与协作

一、对 JML 和规格驱动开发的理解

第三单元的核心并不是“多写几个容器类”,而是训练我们从规格出发进行开发。JML 给出的 requiresensuressignalsassignablepure 等约束,把一个方法应该做什么、不应该做什么、异常何时发生、哪些状态可以被修改都写成了相对明确的契约。

在前两个单元中,我更多是从输入输出和自然语言描述出发写程序,而第三单元要求我先阅读接口规格,再决定数据结构和实现策略。这个过程让我意识到:规格驱动开发中,代码不是第一位的,需求边界才是第一位的。比如一个查询方法如果标注了 pure,那么它不仅要返回正确值,还不能悄悄修改缓存、列表顺序或对象状态。再比如一个方法的异常顺序也不是随意的,必须严格符合 JML 中不同 signals 条件之间的优先级。

我对 JML 的理解主要有三点:

  1. JML 是方法行为的精确定义
    它把自然语言中容易产生歧义的部分形式化。例如“推荐第 N 个 up 主”并不是简单排序,还要明确候选集合、分数相同时的 id 比较、异常何时抛出。

  2. JML 约束的不只是结果,还有过程中的副作用
    很多方法看似只要返回值正确即可,但 assignable \nothingpure 实际要求方法调用前后系统状态保持一致。这提醒我在写查询方法时不能为了方便修改容器。

  3. JML 能帮助发现自然语言需求遗漏
    当我同时阅读指导书和接口 JML 时,经常能发现仅凭指导书不够精确的地方,比如 tie-break、异常优先级、容器长度变化等。真正实现时,JML 比文字描述更接近评测依据。

规格驱动开发的难点在于:读懂规格本身也需要成本。有时一个方法的 JML 很长,里面嵌套了 forallexistssumnum_of 等表达式,直接阅读会很吃力。我一般会先把它翻译成自然语言,再拆成“正常行为、异常行为、副作用、排序规则、边界条件”几个部分,最后再写代码。

二、JUnit 测试经验总结

第三单元要求为指定方法编写 JUnit 测试,这让我意识到测试不应该只测“正常输入输出”,还应该测规格中的所有关键约束。

我的 JUnit 测试经验主要包括以下几点:

  1. 测试正常功能,也测试异常路径
    对于 recommendNthUp 这类方法,除了测试第 1 名、第 N 名是否正确,还要测试 UserIdNotFoundExceptionInvalidRankExceptionNoVideoUploadedExceptionColdStartUserException 等异常是否按正确条件触发。

  2. 测试 tie-break 规则
    很多错误实现不是完全算错,而是在分数相等时没有按 id 最小或指定规则选择。互测中这种数据很容易构造,因此 JUnit 中需要专门设计平分场景。

  3. 测试 pure 和 assignable
    查询方法调用前后,用户、视频、网络状态都不应该变化。可以利用课程组提供的 strictEqualsgetUsers 等辅助方法,也可以手动记录关键字段。这个测试方向比只看返回值更能抓出隐蔽 bug。

  4. 测试迭代后的旧功能
    第三次作业新增推荐逻辑后,旧方法如 watchVideolikeVideocoinVideo 会影响新增字段,例如分区观看次数、视频热度、up 主影响力。因此旧方法也要重新测试,不能只测新增接口。

  5. 测试要小而明确
    一个测试用例最好只验证一类行为。比如“排序规则测试”“异常优先级测试”“pure 测试”分开写,失败时更容易定位原因。

这三次作业让我体会到,JUnit 不只是为了凑测试文件,而是帮助自己理解规格的工具。写测试的过程其实也是重新审查 JML 的过程。

三、三次作业迭代过程总结

第一次作业:基础社交网络

第一次作业主要实现用户、视频、关注、观看、接收未观看视频、最短路、互相关注数等基础功能。这个阶段最重要的是建立基础容器结构。

我使用了如下数据结构:

  • LinkedHashMap<Integer, User> 存储用户,便于按 id 查询。
  • LinkedHashMap<Integer, Video> 存储视频。
  • 用户内部维护关注列表、粉丝列表、接收视频列表、已观看视频集合。
  • 对于互相关注数,使用增量维护,避免每次查询都遍历所有用户对。

第一次作业让我熟悉了 JML 中“对象是否相等通常按 id 判断”的特点,也让我开始意识到容器顺序、重复元素、浅拷贝等细节的重要性。

第二次作业:互动行为和贡献关系

第二次作业加入了点赞、投币、转发、评论、清理垃圾评论、勋章、最佳贡献者、最长年龄下降链等功能。

这一阶段的主要变化是:视频不再只是静态对象,而是拥有播放数、点赞数、投币数、转发数、评论列表等状态;用户也增加了金币、勋章、贡献者列表等状态。

我在迭代时重点检查了两个方面:

  • 已有方法是否需要同步维护新增字段
    例如 coinVideo 不只是修改视频投币数,还要修改用户金币、up 主金币、贡献者列表和贡献值。

  • 新增查询是否可以增量优化
    例如互相关注数、最长下降链如果每次都完全重算,数据量变大时可能成为瓶颈。因此我对部分结果使用了缓存或增量维护。

第二次作业中,我进一步体会到 assignable 的意义。一个方法可能涉及多个对象的字段修改,必须严格控制修改范围,不能因为方便而顺手改了无关状态。

第三次作业:推荐系统

第三次作业加入了智能推荐视频、推荐 up 主、用户画像、最有影响力 up 主、全局最佳贡献者等功能。相比前两次,这次的难点在于新增功能依赖旧功能产生的数据。

例如:

  • watchVideo 会影响用户在对应分区的观看次数。
  • likeVideocoinVideoforwardVideowatchVideo 会影响视频热度。
  • 视频热度变化会进一步影响 up 主在对应分区的影响力。
  • 用户兴趣度又依赖分区观看次数、总视频数、已观看视频数。

因此第三次作业不能只“新增几个方法”,而要重新审查所有旧方法的副作用。我采用的方式是:

  1. 阅读新增 JML,列出新增 model 字段。
  2. 回到旧方法中,检查哪些旧操作会影响新增字段。
  3. 对容易超时的查询设计增量维护。
  4. 为推荐方法构造排序、过滤、异常、pure 性质测试。

性能方面,我重点关注了推荐 up 主和影响力查询。如果每次推荐都对每个 up 主重新遍历其所有视频计算影响力,复杂度会较高。我的做法是给用户维护各分区影响力缓存,视频热度变化时增量更新 up 主对应分区的影响力。这样推荐时只需要按 7 个分区计算分数,不需要反复扫描视频列表。

四、如何发现已有方法和容器在迭代中的变化

我总结出一个比较有效的方法:每次迭代不要先写新增方法,而是先对比新旧接口。

具体做法如下:

  1. 对比 UserInterface、VideoInterface、NetworkInterface 的新增 model 字段和方法。
  2. 找到新增字段的来源。
    例如 typeCounts 的来源是观看视频,videos 的来源是上传视频,influence 的来源是视频热度。
  3. 回查旧方法。
    如果某个旧方法会改变新增字段的来源,就必须修改旧方法。
  4. 检查查询方法是否仍然 pure。
    新增缓存时尤其容易出问题。如果在 pure 方法里懒更新缓存,就可能违反 assignable \nothing
  5. 重新设计测试数据。
    测试不只覆盖新增方法,还要覆盖“旧方法 + 新字段”的组合。

这种方法能避免“新增方法写对了,但旧方法没有维护新状态”的问题。

五、如何发现性能瓶颈

第三单元的数据规模不算特别大,但如果完全按 JML 暴力实现,仍然可能超时。

我发现性能瓶颈主要靠三种方式:

  1. 从复杂度上预判。
    如果一个查询方法每次都要遍历所有用户、所有视频,甚至嵌套遍历,就要警惕。比如推荐 up 主如果对每个候选 up 都重新扫描其全部视频,就可能在多次查询时变慢。

  2. 关注高频查询。
    有些方法虽然单次不慢,但指令中可能大量出现。例如 queryMutualFollowingSumrecommendNthUpqueryMostInfluentialUp 等都可能被重复调用。

  3. 构造极端数据测试。
    可以构造大量用户、大量关注关系、大量视频,再连续调用查询方法。虽然本地测试不一定完全模拟评测机,但能帮助发现明显的 O(n^2) 或 O(nm) 问题。

我的优化思路主要是:

  • 对互相关注数进行增量维护。
  • 对最长下降链设置脏标记,关注关系变化后再重算。
  • 对 up 主各分区影响力增量维护。
  • 推荐第 N 个 up 主时用大小为 rank 的堆,而不是每次全量排序。

六、自己程序出现过的 bug 及原因分析

1. 代码风格缩进问题

第三次作业中,我的程序曾经出现代码风格扣分,平台显示 Indentation 有 7 处问题。后来助教提醒检查 lambda 表达式,我定位到 PriorityQueue 比较器中使用了 lambda:

new PriorityQueue<>(rank,
        (first, second) -> {
            ...
        });

这段代码在我本地使用某些 Checkstyle 配置可以通过,但评测端对 lambda 换行缩进更敏感,导致缩进被扣分。最后我将 lambda 改成普通的内部比较器类,避免不同工具版本对 lambda 缩进解释不一致。

这个 bug 的原因是我过于相信本地检查结果,没有意识到 Checkstyle 的版本、配置路径、扫描范围都可能影响最终结果。之后我会尽量避免在课程作业中使用格式容易产生争议的写法,例如复杂 lambda、多层链式调用等。

2. 新旧字段同步维护容易遗漏

第三次作业新增推荐系统后,旧方法的副作用变多。例如 watchVideo 不仅要增加播放数,还要更新用户分区观看次数;likeVideocoinVideoforwardVideo 会改变视频热度,进而影响 up 主影响力。

这类 bug 的根本原因是迭代时只关注新增方法,而没有从新增 model 字段反向检查旧方法。解决方式是每次迭代都建立“字段来源表”,明确每个字段由哪些方法维护。

七、Unit3 第二次研讨课“JML 击鼓传花”感悟

研讨课中的“JML 击鼓传花”让我印象很深。每个人先写一段需求或规格,再交给别人理解和实现。在这个过程中,我明显感受到:需求传递一次,信息就可能损失一次;边界条件如果没有写清楚,后面的人就会按照自己的理解补全。

是否发现了自己或别人的 JML bug

我发现 JML 中最容易出 bug 的地方主要有:

  • 忘记写异常条件。
  • 正常行为和异常行为条件不互斥。
  • 没有说明分数相等时如何比较。
  • 忘记写 assignable,导致实现者不知道哪些状态可以修改。
  • 用自然语言觉得“显然”的条件,在 JML 中没有体现。

有些 JML 看起来描述了返回值,但没有描述容器长度变化、元素顺序、是否允许重复等细节。实现者如果根据自己的直觉写,就可能和出题者预期不一致。

需求和边界是否在传递中发生变化

我认为确实会发生变化。需求在传递过程中,经常从“明确规则”变成“个人理解”。比如“推荐最合适的对象”如果没有明确评分公式、候选范围和 tie-break,那么不同人会实现出完全不同的版本。再比如“删除元素”如果没有说明删除一个还是所有、顺序是否保持,也会产生歧义。

需求边界不能靠口头补充,也不能靠“大家应该懂”。只要没有写进规格,后续实现者就可能产生不同理解。

这次研讨课让我认识到,规格的价值不只是约束个人代码,更重要的是帮助团队建立共同语言。多人协作中,代码可以重构,算法可以优化,但需求理解一旦不一致,后面所有工作都会变得低效。

八、总结

Unit3 让我从“面向输入输出编程”逐渐转向“面向规格编程”。JML 让方法行为更加精确,也让我意识到正确性不仅包括返回值,还包括异常、状态变化、副作用和性能。

三次作业的迭代过程也让我体会到,软件开发不是简单地不断加功能。每次新增需求都会影响已有结构,旧方法也可能需要更新。如果没有系统地分析新增字段、容器关系和性能瓶颈,就很容易在迭代中埋下 bug。

JUnit 测试、复杂度分析、代码风格检查、需求文档和团队沟通,这些看起来都不是“写功能代码”,但它们共同决定了程序能否稳定、可维护地演进。第三单元的训练虽然细节很多,但也让我更接近真实的软件开发流程。

...全文
4 回复 打赏 收藏 转发到动态 举报
写回复
用AI写文章
回复
切换为时间正序
请发表友善的回复…
发表回复

305

社区成员

发帖
与我相关
我的任务
社区描述
2026年北航面向对象设计与构造
java 高校
社区管理员
  • 孙琦航
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告
暂无公告

试试用AI创作助手写篇文章吧