307
社区成员
发帖
与我相关
我的任务
分享本文是面向对象设计与构造课程第三单元(规格化设计)的总结博客,围绕 JML 的理解与应用、JUnit 测试、三次作业的迭代过程、Bug 分析以及研讨课“击鼓传花”的感悟展开。
JML(Java Modeling Language) 是一种用于描述 Java 程序行为的形式化规格语言。它通过前置条件(requires)、后置条件(ensures)、模型字段(model)、异常行为(signals)和不变式(invariant)等语法,精确地定义了一个类或方法“应该做什么”,而不关心“怎么做”。
在三次作业中,我深刻体会到规格驱动开发(Design by Contract)带来的好处:
接口与实现分离:JML 作为调用方和实现方之间的契约,只要双方都严格遵守,就能保证系统的正确性。开发时无需查看他人源码,只需依据 JML 即可编写自己的代码。
精确性避免歧义:自然语言描述的需求容易产生二义性,而 JML 使用数学化的量词(forall、exists、num_of)严格定义了行为边界,例如 cleanSpamComments 中关键词出现次数的重叠匹配规则。
异常行为的明确约束:signals 子句精确指出何时抛出何种异常,开发者不再需要猜测异常顺序或条件。
纯方法(pure)的状态不变性:标记为 pure 的方法不能修改对象状态,这为 JUnit 测试提供了天然的检查点。
当然,JML 也有局限性:复杂业务逻辑会导致规格冗长难读,且目前缺乏成熟的自动化验证工具,主要还是靠人工理解和测试验证。
本单元要求为特定方法编写 JUnit 测试,我在过程中积累了以下经验:
测试不仅要验证 ensures,还要检查:
signals 条件构造用例,确保异常类型和触发时机正确。assignable 限定:使用 strictEquals 等方法确认非指定对象未被修改。pure 方法状态不变:调用前后获取用户数组并逐个比较,确保无副作用。cleanSpamComments 中空关键词、重叠匹配、无匹配等场景必须逐一验证。指导书提供的 getUsers() 和 strictEquals() 极大简化了状态比较,但需注意它们在内部逻辑中可能引入额外开销(如数组复制),测试代码中应合理使用,避免影响对被测方法性能的判断。
每个测试用例应在 @Before 中新建 Network 实例,确保用例之间无干扰,便于定位失败原因。
第九次作业构建了基础社交网络模型(用户、视频、关注关系)。
第十次作业大规模扩展了业务功能:硬币经济、点赞/投币/转发/评论、勋章等,并引入了视频分区类型和评论区清理等复杂逻辑。
如何发现已有方法/容器的变化?
diff 对比接口源码,标记出新增的模型字段(如 coins、type)、新增方法(如 likeVideo)以及原有方法后置条件的增强(如 uploadVideo 增加视频类型参数)。assignable 子句的变化,它直接告诉我们需要维护哪些新的数据结构。性能瓶颈的发现:
recommendVideo 被频繁调用时,我发现在循环中调用 getVideos().length 导致了 O(V²) 的数组复制开销。通过分析 getVideos() 的实现(toArray)和使用频率,确认了性能热点。videos.size() 和 videos.values() 遍历,消除了不必要的数据拷贝。第十一次作业新增了兴趣画像、影响力计算、UP 推荐、全局贡献者统计等高级功能。
迭代中的关键变化:
User 新增 types、typeCounts、videos 字段,需要在 watchVideo 和 uploadVideo 中分别维护。likeVideos 必须从无序集合改为有序列表,以满足 JML 对点赞顺序的后置要求。contributors 和 contributions 需同步维护为有序数组,不能简单用 HashMap 存储。cleanSpamComments 空关键词处理{0, 0},未删除任何评论。if (keyword.trim().isEmpty()) return 的防御代码,违背了 JML 语义(空字符串应匹配所有评论)。countOccurrences 使空串返回 str.length() + 1。validate(condition, new XxxException(...)) 时,无论条件是否成立都会创建异常对象,导致计数器提前累加。Supplier 延迟创建异常,或直接写 if 抛出。recommendVideo 未使用 computeVideoScorecomputeVideoScore 计算得分。computeVideoScore 比较。queryGlobalBestContributor 逻辑完全偏离queryBestContributor,再统计频率取众数。在研讨课的 JML “击鼓传花”游戏中,我们分组传递并补充彼此的 JML 规格。这个过程中我观察到:
JML 的 Bug 极易产生:每个人对业务需求的理解有微妙差异,导致前置条件遗漏、后置条件不精确、异常覆盖不全等问题。例如,自己认为“用户必须已观看视频才能投币”是常识,却忘了写入 requires,导致他人实现时忽略了该约束。
需求边界在传递中发生偏移:第一棒同学可能只考虑了基本正常流程,第二棒补充了异常,第三棒却又误解了某条规则,最终导致规格无法还原原始需求。这说明用自然语言理解需求后再转写 JML,本身就是一种信息失真。
统一团队理解的措施:
通过这次游戏,我深刻认识到形式化规格不是银弹,但它是团队协作中减少歧义、提高代码质量的最有力工具之一。未来在多人编程项目中,我会更加注重契约的明确化和测试的严谨性。
以上就是本单元的学习总结。规格化开发让我对“设计先行、契约驱动”有了切身体会,也为今后构建更可靠的软件系统打下了基础。