OO Unit3 博客作业

沈钡-24373462 2026-05-29 23:34:40

面向对象规格化设计——JML与迭代开发实践总结

本文是面向对象设计与构造课程第三单元(规格化设计)的总结博客,围绕 JML 的理解与应用、JUnit 测试、三次作业的迭代过程、Bug 分析以及研讨课“击鼓传花”的感悟展开。


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

JML(Java Modeling Language) 是一种用于描述 Java 程序行为的形式化规格语言。它通过前置条件(requires)、后置条件(ensures)、模型字段(model)、异常行为(signals)和不变式(invariant)等语法,精确地定义了一个类或方法“应该做什么”,而不关心“怎么做”。

在三次作业中,我深刻体会到规格驱动开发(Design by Contract)带来的好处:

  1. 接口与实现分离:JML 作为调用方和实现方之间的契约,只要双方都严格遵守,就能保证系统的正确性。开发时无需查看他人源码,只需依据 JML 即可编写自己的代码。

  2. 精确性避免歧义:自然语言描述的需求容易产生二义性,而 JML 使用数学化的量词(forallexistsnum_of)严格定义了行为边界,例如 cleanSpamComments 中关键词出现次数的重叠匹配规则。

  3. 异常行为的明确约束signals 子句精确指出何时抛出何种异常,开发者不再需要猜测异常顺序或条件。

  4. 纯方法(pure)的状态不变性:标记为 pure 的方法不能修改对象状态,这为 JUnit 测试提供了天然的检查点。

当然,JML 也有局限性:复杂业务逻辑会导致规格冗长难读,且目前缺乏成熟的自动化验证工具,主要还是靠人工理解和测试验证。


二、JUnit 测试的经验总结

本单元要求为特定方法编写 JUnit 测试,我在过程中积累了以下经验:

1. 全面覆盖 JML 约束

测试不仅要验证 ensures,还要检查:

  • 异常行为:针对每个 signals 条件构造用例,确保异常类型和触发时机正确。
  • assignable 限定:使用 strictEquals 等方法确认非指定对象未被修改。
  • pure 方法状态不变:调用前后获取用户数组并逐个比较,确保无副作用。

2. 边界条件设计

  • 空集合、极值、重复操作、异常前置的严格满足等边界情况往往能暴露隐藏 bug。
  • 例如 cleanSpamComments 中空关键词、重叠匹配、无匹配等场景必须逐一验证。

3. 辅助方法的使用

指导书提供的 getUsers()strictEquals() 极大简化了状态比较,但需注意它们在内部逻辑中可能引入额外开销(如数组复制),测试代码中应合理使用,避免影响对被测方法性能的判断。

4. 独立性与可重复性

每个测试用例应在 @Before 中新建 Network 实例,确保用例之间无干扰,便于定位失败原因。


三、三次作业的迭代过程分析

3.1 第九次作业 → 第十次作业

第九次作业构建了基础社交网络模型(用户、视频、关注关系)。

第十次作业大规模扩展了业务功能:硬币经济、点赞/投币/转发/评论、勋章等,并引入了视频分区类型评论区清理等复杂逻辑。

如何发现已有方法/容器的变化?

  • 逐文件 diff 对比接口源码,标记出新增的模型字段(如 coinstype)、新增方法(如 likeVideo)以及原有方法后置条件的增强(如 uploadVideo 增加视频类型参数)。
  • 关注 assignable 子句的变化,它直接告诉我们需要维护哪些新的数据结构。

性能瓶颈的发现:

  • recommendVideo 被频繁调用时,我发现在循环中调用 getVideos().length 导致了 O(V²) 的数组复制开销。通过分析 getVideos() 的实现(toArray)和使用频率,确认了性能热点。
  • 解决方法:内部逻辑直接使用 videos.size()videos.values() 遍历,消除了不必要的数据拷贝。

3.2 第十次作业 → 第十一次作业

第十一次作业新增了兴趣画像影响力计算UP 推荐全局贡献者统计等高级功能。

迭代中的关键变化:

  • User 新增 typestypeCountsvideos 字段,需要在 watchVideouploadVideo 中分别维护。
  • likeVideos 必须从无序集合改为有序列表,以满足 JML 对点赞顺序的后置要求。
  • contributorscontributions 需同步维护为有序数组,不能简单用 HashMap 存储。
  • 新增了大量算法逻辑(推荐、影响力排序),必须严格按 JML 的数学描述实现,否则容易在排序规则和同分处理上出错。

四、程序 Bug 与分析

Bug 1:cleanSpamComments 空关键词处理

  • 现象:空关键词时返回 {0, 0},未删除任何评论。
  • 原因:错误地添加了 if (keyword.trim().isEmpty()) return 的防御代码,违背了 JML 语义(空字符串应匹配所有评论)。
  • 修复:删除该分支,并修改 countOccurrences 使空串返回 str.length() + 1

Bug 2:异常计数器污染

  • 现象:抛出的异常对象中静态计数器值异常大。
  • 原因:使用 validate(condition, new XxxException(...)) 时,无论条件是否成立都会创建异常对象,导致计数器提前累加。
  • 修复:改用 Supplier 延迟创建异常,或直接写 if 抛出。

Bug 3:recommendVideo 未使用 computeVideoScore

  • 现象:推荐结果与预期不符。
  • 原因:实现时错误地使用了视频自身热度比较,而 JML 要求用 computeVideoScore 计算得分。
  • 修复:改为遍历视频并调用 computeVideoScore 比较。

Bug 4:queryGlobalBestContributor 逻辑完全偏离

  • 现象:返回的是个人最高单笔贡献,而非“所有 UP 的最佳贡献者的众数”。
  • 原因:对 JML 后置条件理解错误,跳过了按 UP 统计最佳贡献者的步骤。
  • 修复:重写方法,先计算每个 UP 的 queryBestContributor,再统计频率取众数。

五、研讨课“击鼓传花”JML 游戏感悟

在研讨课的 JML “击鼓传花”游戏中,我们分组传递并补充彼此的 JML 规格。这个过程中我观察到:

  1. JML 的 Bug 极易产生:每个人对业务需求的理解有微妙差异,导致前置条件遗漏、后置条件不精确、异常覆盖不全等问题。例如,自己认为“用户必须已观看视频才能投币”是常识,却忘了写入 requires,导致他人实现时忽略了该约束。

  2. 需求边界在传递中发生偏移:第一棒同学可能只考虑了基本正常流程,第二棒补充了异常,第三棒却又误解了某条规则,最终导致规格无法还原原始需求。这说明用自然语言理解需求后再转写 JML,本身就是一种信息失真

  3. 统一团队理解的措施

    • 制定统一的术语表:确保所有人都理解“贡献者”、“影响力”、“兴趣值”等核心概念的精确定义。
    • 分层评审 JML:先评审前置条件是否完备、互斥,再检查后置条件是否精确描述状态变化,最后验证异常行为覆盖。
    • 自动化测试先行:在编码前先基于 JML 编写 Junit 测试骨架,用测试用例作为可执行的需求文档。
    • 持续集成与规范检查:利用静态检查工具(如果有)和代码审查,确保所有人的实现严格遵循同一份 JML,减少信息差。

通过这次游戏,我深刻认识到形式化规格不是银弹,但它是团队协作中减少歧义、提高代码质量的最有力工具之一。未来在多人编程项目中,我会更加注重契约的明确化和测试的严谨性。


以上就是本单元的学习总结。规格化开发让我对“设计先行、契约驱动”有了切身体会,也为今后构建更可靠的软件系统打下了基础。

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

307

社区成员

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

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