2026面向对象第三次博客作业

发布时间:2026/6/6 9:04:37

2026面向对象第三次博客作业 # BUAA OO Unit3 总结JML、规格驱动开发与测试反思## 一、对 JML 和规格驱动开发的理解第三单元的核心主题是 JML 与规格驱动开发。和前两个单元相比这一单元的最大变化在于我们不再主要依赖自然语言描述去理解需求而是通过形式化规格来约束方法的行为。JML 给出了方法的前置条件、后置条件、副作用范围以及异常分支使得“应该实现什么”变得更加明确。在实践中我对 JML 的理解经历了一个变化过程。最初阅读 JML 时我更关注每个方法表面上的功能例如 addUser 是添加用户followUser 是建立关注关系queryShortestPath 是查询最短路径。但随着作业推进我逐渐意识到 JML 更重要的价值在于描述对象状态的变化哪些容器会发生改变哪些对象字段必须保持不变异常发生时状态是否应该回滚以及返回值和内部数据结构之间应满足什么关系。规格驱动开发的关键不是“先写代码再调试到能过样例”而是先从规格中抽取出状态模型再设计数据结构和方法实现。例如在本单元中用户、视频、关注关系、粉丝关系、观看记录、点赞投币记录等都可以从 JML 中抽象为若干集合、映射和计数器。只要这些内部结构能够维护 JML 要求的不变量方法实现就会更稳定。我认为 JML 的优势主要体现在以下几个方面1. **减少需求歧义**自然语言容易遗漏边界条件而 JML 对正常行为和异常行为都有明确描述。2. **帮助拆分实现步骤**可以根据 requires、ensures、signals 等语句先处理异常再处理正常状态转移。3. **便于构造测试样例**每条规格都可以转化为测试点包括正常用例、异常用例和边界用例。4. **促进接口与实现分离**只要实现满足接口规格内部容器可以自由选择如 ArrayList、HashMap、HashSet 等。同时JML 也不是直接等价于高效代码。规格描述的是“逻辑正确性”并不总是告诉我们应该选择怎样的数据结构。如果直接照着量词和集合表达式逐层遍历很容易写出复杂度很高的实现。因此规格驱动开发中还需要在正确性之外主动考虑复杂度和可维护性。## 二、JUnit 测试经验总结本单元要求我们基于 JML 编写 JUnit 测试。相比随机数据测试JUnit 更强调对某个方法规格的精确验证。我在测试中的主要经验如下。### 1. 将 JML 子句转化为测试点一个方法通常包含多个分支正常执行、异常执行、边界输入、状态变化等。编写测试时可以按 JML 子句逐项拆分。例如对于关注关系相关方法需要测试- 用户不存在时是否抛出正确异常- 自己关注自己时是否抛出异常- 重复关注时是否抛出异常- 正常关注后关注者列表和粉丝列表是否同时更新- 取消关注后两侧关系是否同时删除。这种方式比只根据直觉写样例更系统也更容易发现实现中遗漏的状态更新。### 2. 关注异常分支下的状态不变性在规格驱动开发中异常分支不仅要求抛出正确异常还经常要求对象状态不发生改变。如果只检查异常类型而不检查异常前后的容器大小、计数器和关系集合就可能漏掉“先修改状态再抛异常”的 bug。因此在测试异常分支时我会记录执行前的关键状态例如用户数量、视频数量、关注关系、金币数等然后在异常触发后再次断言这些状态没有改变。### 3. 构造边界数据本单元中有很多边界条件例如年龄范围、金币数量、评论为空、推荐排名非法、冷启动场景、没有视频或没有用户等。这些边界往往是 bug 高频点。JUnit 测试需要覆盖- 最小合法值和最大合法值- 刚好非法的值- 空集合场景- 只有一个元素的场景- 并列情况下的 tie-break 规则。例如推荐类方法经常要求在分数相同的情况下选择 id 更小的对象如果测试中没有构造相同分数的情况就很难发现排序规则错误。### 4. 测试内部一致性而不是只看输出有些方法表面输出很简单但会影响多个内部状态。例如 watchVideo 不只是表示用户观看了视频还可能涉及移除未观看列表、增加播放量、更新用户兴趣等。如果测试只检查输出信息而不检查后续查询方法结果就可能无法发现内部状态维护错误。因此我在测试时更倾向于通过一组方法组合验证状态。例如先上传视频、关注用户、观看视频再调用推荐、热度、用户画像等查询方法从最终结果反推内部状态是否一致。## 三、三次作业迭代过程分析### 1. 第一次作业建立基本社交网络模型第一次作业主要围绕用户、视频和关注关系展开核心功能包括添加用户、上传视频、关注/取关、观看视频、查询未观看视频、查询粉丝年龄比例、查询互相关注数以及最短路径。我在第一次作业中使用了 MapInteger, User 和 MapInteger, Video 来维护用户和视频便于通过 id 快速查询。关注关系则保存在用户对象内部。对于最短路径我采用 BFS因为关注关系可以看作有向图查询从一个用户到另一个用户的最短关注路径。这次作业让我初步体会到JML 中的集合和量词可以映射为程序中的容器关系。比如“某用户是否关注某用户”可以由 following 集合判断“粉丝年龄比例”可以由 followers 集合统计。### 2. 第二次作业加入视频互动与更多统计功能第二次作业在第一次的基础上加入了视频类型、金币、点赞、投币、评论、清理垃圾评论、最热视频、勋章、最长降龄序列等功能。相比第一次状态复杂度明显提升。这一次最大的变化是单个操作往往会影响多个对象。例如投币操作不仅要扣除投币用户的金币还要增加视频金币数、增加上传者金币数并维护贡献者列表。点赞操作既要修改用户的点赞集合也要修改视频的点赞数。评论相关方法则需要维护评论 id 与评论内容之间的对应关系。在迭代中我发现原有容器是否合适主要看以下几个方面- 是否频繁通过 id 查询对象- 是否需要保持插入顺序或排序关系- 是否需要快速判断某个关系是否存在- 查询方法是否会被大量调用- 状态更新时是否容易保持双向一致。例如如果只使用 ArrayList 保存用户和视频每次 containsUser 或 getUser 都需要线性扫描当数据规模变大时这会成为明显性能瓶颈。因此在第二次作业中我更倾向于用 HashMap 建立 id 到对象的映射同时保留列表用于遍历。### 3. 第三次作业加入推荐、画像和全局统计第三次作业继续扩展功能加入了全局最佳贡献者、视频推荐、UP 主推荐、最有影响力 UP 主、用户画像等方法。这些方法更加偏向统计和推荐很多实现需要综合用户历史行为、视频热度、类型兴趣、关注关系等信息。这一阶段我对“迭代”有了更深的体会如果前两次作业中没有良好维护观看记录、类型统计、贡献关系和视频上传关系第三次作业就会变得很难写。也就是说前一次作业中的状态设计会直接影响下一次作业的扩展难度。第三次作业也暴露了一个问题有些方法如果按照 JML 朴素翻译会进行大量重复遍历。例如推荐视频时需要对所有视频计算得分推荐 UP 主时需要对候选用户排序。如果每次计算得分又继续遍历全部历史记录整体复杂度就会迅速上升。解决这类问题的关键是缓存或增量维护例如在观看视频时同步更新用户对各类型视频的观看次数在投币时同步更新贡献关系而不是每次查询时重新从头统计。## 四、如何发现已有方法和容器在迭代中的变化我总结出几种比较有效的方法。### 1. 对比新旧接口规格每次新作业发布后首先应该对比接口新增了哪些方法、旧方法签名是否变化、异常类型是否变化、后置条件是否增加。例如第一次的上传视频只需要上传者和视频 id后续加入视频类型后就需要增加类型合法性判断并将视频类型纳入热度、推荐和画像计算。## 2. 建立“状态影响表”对于每个方法记录它会读写哪些状态。例如- watchVideo读用户、视频写观看记录、未观看列表、播放量、用户类型兴趣- coinVideo读金币、观看记录写用户金币、上传者金币、视频金币、贡献者记录- followUser写关注集合和粉丝集合- recommendVideo读用户观看记录、视频热度、类型兴趣。当新增方法依赖某个状态时就能快速判断原有实现是否已经维护了该状态。如果没有就需要补充容器或在已有方法中增加同步更新。### 3. 通过复杂度估算寻找性能瓶颈发现性能瓶颈的方法主要有两种一是阅读代码时估算循环嵌套层数二是构造极限数据测试运行时间。例如 queryMutualFollowingSum 如果每次都两层遍历所有用户再判断互相关注复杂度接近 \(O(n^2)\)。在用户数量较大、查询次数较多时这个方法就可能成为瓶颈。类似地最短路径查询需要 BFS如果频繁查询也可能较慢推荐和排序类方法如果每次都重新计算所有候选对象的复杂得分也容易超时。因此性能优化的思路通常包括- 用 HashMap 替代线性查找- 用 HashSet 快速判断关系是否存在- 对频繁查询的统计量进行增量维护- 对排序类查询注意 tie-break并避免重复计算- 对图算法明确复杂度不在 BFS 内部做额外线性查找。## 五、自己程序中可能出现的 bug 及原因分析结合本单元作业我认为我的程序中容易出现以下几类 bug。### 1. 规格理解不完整导致状态更新遗漏本单元很多方法的状态变化不是单点的而是联动的。例如关注操作既要修改关注列表也要修改粉丝列表投币操作既要影响用户金币也要影响上传者金币、视频金币和贡献者记录。如果只关注方法名称对应的主要功能就容易遗漏辅助状态。这类 bug 的根本原因是没有把 JML 的后置条件完整转化为状态影响表而是凭直觉实现。### 2. 异常优先级错误多个异常同时可能发生时规格通常规定了检查顺序。如果实现时调整了判断顺序就可能在强测或互测中因为抛出了不同异常而失败。例如用户不存在、视频不存在、参数非法等条件同时出现时必须严格按照 JML 指定顺序处理。这类 bug 的原因是把异常当作“只要报错即可”而没有意识到异常类型和触发优先级也是规格的一部分。### 3. 边界条件遗漏例如空评论、非法类型、非法排名、无用户、无视频、冷启动用户、没有贡献者等场景都容易遗漏。尤其是推荐相关方法在没有视频、用户没有观看记录、候选对象不足时需要抛出不同异常不能混淆。这类 bug 通常是测试数据太常规没有覆盖空集合、单元素集合和并列情况。### 4. 容器选择带来的性能问题如果所有用户和视频都只存在 ArrayList 中那么每次根据 id 查询都要遍历。虽然在小数据下看不出问题但在强测中可能造成超时。第三次作业中的推荐、画像和全局统计进一步放大了这种问题。这类问题的根本原因是只关注功能正确性没有从迭代角度考虑未来查询需求。### 5. 并列规则处理错误很多查询要求在指标相同的情况下选择 id 更小的对象。例如最热视频、最佳贡献者、推荐对象等。如果实现时只比较主要指标而忘记 id 的二级排序就会在特殊数据下出错。这类 bug 需要通过专门构造“分数相同但 id 不同”的测试样例来发现。## 六、关于大模型和 Code Agent 的使用体会在 Unit3 中大模型对于规格驱动开发有一定帮助尤其是在以下方面1. **辅助解释 JML**可以帮助把复杂的逻辑表达式转化为自然语言说明。2. **生成测试思路**可以根据方法规格列出正常用例、异常用例和边界用例。3. **检查实现遗漏**可以让模型对照方法规格和代码提示可能漏掉的状态更新。4. **辅助重构容器设计**可以询问某个查询频繁时适合使用怎样的数据结构。但大模型也有明显局限。它往往更擅长给出“看起来正确”的直接实现却不一定主动优化复杂度。如果只让它根据 JML 翻译代码它可能会按照规格中的量词写出多层循环而忽视强测数据规模下的性能风险。它也可能忽视已有架构和容器设计生成与原项目风格不一致的代码。因此我认为使用大模型时应该明确提出约束例如- 要求分析时间复杂度- 要求说明需要维护哪些容器- 要求列出异常检查顺序- 要求根据 JML 生成 JUnit 测试点- 要求只修改必要代码不破坏已有接口。Code Agent 的优势在于可以直接阅读项目文件、定位实现位置、批量修改和检查测试。但使用时仍然需要开发者自己把握需求边界。尤其在 OO 作业中JML 是最高优先级依据不能让模型随意“优化”语义。比较好的使用方式是先让 Agent 总结规格和现有结构再要求它针对某个方法或某类测试进行局部修改最后由自己复查异常顺序和边界条件。## 七、JML“击鼓传花”游戏的感悟Unit3 第二次研讨课中的 JML“击鼓传花”游戏给我留下了很深印象。这个活动模拟了多人接力理解和传递需求的过程也暴露了形式化规格和自然语言理解之间的差异。### 1. 是否发现了自己或别人 JML 中的 bug在交流过程中我发现 JML 很容易出现两类问题。第一类是边界条件不完整例如只描述了正常输入下的结果却没有说明空集合、非法参数或重复元素时应该怎样处理。第二类是副作用描述不完整例如只说明返回值正确却没有说明哪些对象状态应该改变哪些状态必须保持不变。别人指出我的问题时我也意识到自己写规格时容易默认某些“显然”的条件但对于阅读者来说如果规格中没有明确写出就可能产生不同理解。这说明规格不是写给自己看的而是写给实现者、测试者和维护者共同使用的契约。### 2. 传递过程中需求和边界是否发生变化在传递过程中需求和边界确实容易发生变化。最初的设计者可能有一个隐含假设但经过几轮传递后后续同学只能根据文字理解需求。如果某个边界没有写清楚后面的人可能会补充自己的理解导致需求发生偏移。例如“选择最佳对象”这种描述如果没有说明指标相同时如何处理不同人可能分别选择最先出现的、id 最小的或任意一个。再比如“删除元素”如果没有说明不存在时是忽略还是报错也会导致实现差异。这些问题在单人编程时可能不明显但在多人协作时会迅速放大。### 3. 多人组队编程时如何统一需求和实现理解我认为多人协作时首先要建立统一的需求文档和规格文档。自然语言描述适合说明背景和整体目标但关键接口必须有明确的输入、输出、异常、状态变化和边界条件。对于复杂方法最好用表格或伪代码列出所有分支。其次团队需要约定统一的数据模型。例如哪些对象是全局唯一的哪些关系需要双向维护哪些统计量需要增量更新哪些查询可以临时计算。如果每个人都在自己的模块里维护一套不同理解的数据结构就很容易出现接口对不上或状态不一致。再次要通过测试统一理解。团队可以先根据规格共同编写一批公共测试尤其覆盖异常优先级、边界条件和并列规则。测试本身也是一种需求表达。当某个需求存在争议时可以先讨论并固化成测试再进行实现。最后要减少信息差需要建立明确的协作规则1. 所有需求变更必须记录在统一文档中2. 接口修改必须通知所有相关成员3. 每个方法都要说明读写哪些状态4. 对边界条件和异常顺序进行集中评审5. 合并代码前必须通过公共测试6. 对复杂逻辑进行 code review重点检查规格一致性而不是只看能否运行。这次研讨课让我认识到JML 的意义不仅是帮助机器或测试工具验证程序更重要的是帮助人和人之间减少误解。形式化规格越清晰团队协作中的信息损耗就越少。## 八、总结第三单元让我从“实现功能”进一步转向“实现规格”。JML 使需求更加精确也暴露了我在边界条件、异常顺序、状态一致性和性能设计方面的不足。JUnit 测试让我学会从规格出发构造测试而不是只依赖样例和随机输入。三次作业的迭代也说明面向对象设计不能只满足当前功能还要考虑后续扩展。合理的数据结构、清晰的状态维护、严格的异常顺序和系统化测试是保证程序在迭代中保持正确性的关键。总体来看Unit3 的收获不仅是学会阅读和实现 JML更重要的是理解了规格驱动开发的思想先明确契约再设计结构最后通过测试和评审保证实现与契约一致。这种方法对于今后的多人协作和大型项目开发都有很强的现实意义。

相关新闻