面向对象设计与构造U3总结

曾芸-24373393 2026-05-28 11:27:02

OO第三单元总结

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

JML 是一种形式化的规格描述语言,它将“契约式设计”落到 Java 代码上。通过 requiresensuresassignablesignals 等关键字,我们能精确地定义方法的前置条件(调用者的义务)、后置条件(实现者的承诺)、副作用(允许修改的范围)以及异常行为

规格驱动开发的核心价值在于:

  • 消除二义性。自然语言中的“通常”“尽量”等模糊词在 JML 中不复存在。
  • 设计与实现分离。可以先写规格,再写实现,测试也基于规格,保证正确性。
  • 支持自动化验证。结合 JUnit 和工具链,可以自动检查实现是否满足规格。

三次作业下来,我深刻体会到:好的规格非常有用,让调用者和实现者各司其职,一旦违反契约,要么是调用方输入不合法(抛异常),要么是实现方逻辑有误(测试失败)。这种“按契约编程”的思维方式,让代码的可靠性有了坚实的理论支撑。

二、JUnit 测试经验

在实践“基于规格的自动化测试”的过程中,我积累了以下方法:

  1. 状态快照 + 副作用验证
    TestNetwork 中,我编写了 assertJMLBehavior() 方法:每次调用纯查询方法前,保存所有 Useridagename、关注关系、粉丝关系,以及所有 Video 的评论列表等完整快照;调用后逐项比对,确保 pure 方法和 assignable \nothing 的方法没有“偷偷”修改任何数据。在 HW11RecommendNthUpTest 中,我还更加严格地验证了推荐方法不会改变用户状态。

  2. 结果正确性断言
    将 JML 中的 ensures 表达式字面意思用最简单粗暴的方式直接翻译成 Java 代码。例如 queryMutualFollowingSum 的期望值通过双重循环手工计算,与返回值对比。对于 cleanSpamComments,我手动统计了期望的删除数量和最大关键字出现次数,并与方法返回值比对。

  3. 幂等性检查
    pure 方法应当不改变对象任何状态,所以对 pure 方法连续调用两次,断言结果应当相同。如果不相同则可以说明方法内部偷偷改变了对象数据。

  4. 异常优先级测试
    在第三次作业中,针对 recommendNthUp 方法,我专门写了测试用例分别触发 UserIdNotFoundExceptionInvalidRankExceptionNoVideoUploadedExceptionColdStartUserException,验证异常抛出的顺序与规格一致。

这些测试帮助我在迭代重构时迅速发现问题,保证了代码的正确性。

三、三次作业迭代分析

3.1 迭代过程与容器变化

作业新增功能容器 / 数据结构变化
HW9基础用户/视频管理、关注、互关计数、未观看视频队列HashMap 存用户/视频,HashSet 存关注/粉丝,LinkedList+HashSet 存未观看视频
HW10点赞/投币/转发、贡献者统计、勋章、评论、垃圾清理、最长递减序列增加 HashMap 类型的 coinRecord 增量维护最佳贡献者;最长递减序列增加缓存失效机制
HW11推荐视频、推荐UP主、影响力、兴趣画像、全局最佳贡献者未观看视频队列改为手写双向链表+HashMap;增加 typeCountstypeInfluences 数组;queryLongestDecSeq 增加缓存;queryGlobalBestContributor 临时构建统计

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

每次阅读新规格时,我会先对比新增的方法和已有的数据字段,思考:

  • 现有容器能否支持新操作?
    例如 HW11 的 watchVideo 需要从未观看队列中删除任意视频(不仅是头部)。原先的 LinkedList + remove(Integer) 是 O(n),而且需要遍历。我意识到可以改造成双向链表 + 映射,于是实现了 VideoNode 内部类,用 head/tail 指针和 receivedVideoMap 实现 O(1) 删除。
  • 新增的查询是否大幅增加时间复杂度?
    queryLongestDecSeq,我最初每次调用都 O(n²) 计算,后来发现图变化(增删用户、关注关系)远少于查询次数,于是增加 isCacheValid 标记,在 addUser、followUser、unfollowUser 中置为 false,查询时按需计算一次并缓存。

3.3 如何发现程序的性能瓶颈?

  • 理论预估:根据数据范围,检查每个方法的时间复杂度是否在可接受范围。例如 recommendVideo 每次 O(m) 遍历所有视频,但指令条数在 10000 条以内,可以接受。
  • 测试观察:在本地生成大量指令用来测试,如果某方法耗时明显过长,就检查其实现。

四、Bug 分析

三次作业我均顺利通过强测和互测,未出现功能性 bug。

五、第二次研讨课“规格传声筒”游戏感悟

5.1 传递中的bug和变化

游戏中,我注意到几个常见现象:

  • 边界遗失:原始 NL 中限制某变量数据范围为 <= 8,但经过几轮传递之后,最终 NL 还原为 < 8 ,遗失了边界值 8 。
  • 需求和操作混淆:在阅读复杂 JML 时可能出现理解偏差,把前置条件混淆为后置条件,从而导致还原为 NL 时发生畸变。

5.3 多人组队编程如何统一理解、减少信息差?

  • 使用形式化规格作为“通用语言”:关键接口和核心方法必须用 JML 写规格,而不是自然语言注释。JML 是精确的、可执行的契约。
  • 建立“规格评审”机制:在编码前,团队一起 review JML 规格,确保大家对前置条件、后置条件、异常行为的理解一致。。
  • 自动化测试作为“裁判”:为每个核心方法编写基于规格的 JUnit 测试,任何修改都必须通过测试。这能强制保证实现与规格一致,减少因理解偏差导致的错误。

六、大模型使用体验

在作业中,我尝试过用大模型辅助编程,在 JML 的约束下大模型能快速生成结构基本正确的代码。但是大模型很容易忽略效率问题和可扩展性,在我询问性能优化建议时也很容易分析错误,给出没有必要甚至负向优化的方案,往往需要我批判地采纳,并重新设计数据结构和优化方式。

对于单元测试,大模型根据 JML 中的 ensuressignals 生成的断言代码往往检查不够全面,仍然需要我多次排查快照对比、副作用验证中遗漏的部分。

七、总结

Unit3 让我真正理解了“契约式设计”的力量。JML 将模糊的自然语言需求转化为精确的、可验证的逻辑公式,而基于规格的 JUnit 测试则确保了实现与规格的一致性。三次作业的迭代过程也锻炼了我根据规格推导数据结构和算法的能力,不是机械地翻译规格,而是设计高效的增量维护策略。规格驱动开发,不仅是编程技巧,更是一种可靠的工程思想。

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

305

社区成员

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

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