面向对象第三单元总结:JML规格、图模型与测试驱动开发

林子淇-23371263 2025-05-18 08:11:10

前言

面向对象课程的第三单元将我们带入了基于规格的软件构造世界,核心围绕Java建模语言(JML)展开。本单元作业通过模拟一个社交网络系统,让我们在实践中学习和理解规格化设计、图模型的构建与维护、以及各类测试方法。本文将结合三次作业的实践,对本单元的测试过程、大模型辅助、架构设计、性能优化、Junit测试以及个人学习体会进行总结和分析。

一、本单元的测试过程

在本单元中,有效的测试是保证代码正确性和鲁棒性的关键。我主要结合了以下几种测试方法:

1. 对各类测试的理解

  • 单元测试 (Unit Testing):

    • 理解: 针对代码中的最小可测试单元(通常是方法或类)进行正确性检验。目的是隔离每个部分,确保其独立工作正常。
    • 实践:Person 类的 addTag, isLinkedNetwork 类的 addPerson, addRelation,以及各个异常类的构造和 print 方法等进行单独测试。例如,测试向 Person 添加一个已存在的 Tag 是否会按预期抛出 EqualTagIdException
  • 功能测试 (Functional Testing):

    • 理解: 从用户(或在此单元中,是指令调用者)的角度出发,验证软件的各项功能是否符合需求规格说明。不关心内部实现,只关心输入和输出的正确性。
    • 实践: 针对每条指令(如 ap, ar, qts, sm, dce 等)设计测试用例。例如,构造一系列 apar 指令构建一个小型网络,然后用 qts 指令验证三元组数量是否计算正确;或者发送不同类型的消息,验证接收者的状态(社交值、金钱、消息列表)是否按规格更新。
  • 集成测试 (Integration Testing):

    • 理解: 在单元测试的基础上,将已测试过的模块组合起来进行测试,检查模块间的接口和交互是否正确。
    • 实践: 测试 addMessage 后调用 sendMessage,涉及 Network, Person, Message (及其子类), Tag (如果是群组消息) 等多个类的交互。需要验证消息是否正确传递、状态是否正确更新(如发送者扣钱、接收者加钱、社交值更新、表情热度增加等)。
  • 压力测试 (Stress Testing):

    • 理解: 测试系统在极端负载(大量数据、高并发请求)下的稳定性和性能表现,找出性能瓶颈。
    • 实践: 构造大规模社交网络(例如,成百上千个 Person,数万条关系),然后执行大量查询指令(如 qci, qsp, qts)或修改指令(如 mr, dce),观察程序运行时间和内存消耗,检查是否有超时 (TLE) 或内存溢出 (MLE) 的情况。
  • 回归测试 (Regression Testing):

    • 理解: 在修改了旧代码(如修复bug或优化性能)或添加了新功能后,重新运行之前的所有测试用例,以确保修改没有引入新的错误或导致原有功能失效。
    • 实践: 每次作业迭代(例如,从第九次作业升级到第十次,增加了公众号相关功能),都需要用之前的测试用例(针对人和普通消息的)对新代码进行测试,确保原有功能不受影响。修复性能问题或bug后,也必须用导致问题的测试数据和覆盖全面的旧数据进行测试。

2. 数据构造策略

构造有效的测试数据是测试成功的关键。我的策略主要包括:

  • 边界条件数据:

    • 空网络、只有一个/两个人的网络。
    • 关系:无关系、仅一条关系、全连接图。
    • ID:合法的最小/最大ID、临界ID。
    • 数值:0、1、最大值、负值(如果规格允许或需要测试异常)。
    • 容器大小:空容器、只有一个元素的容器、满容器(如 Tag 内人数达到1000上限)。
  • 等价类划分数据:

    • 根据输入参数的有效性、类型、范围等划分等价类。例如,addRelation 时,id1和id2是否存在、是否相同、是否已关联。
    • 针对 sendMessage,消息类型(普通、红包、表情、转发)、发送目标(个人、标签)、发送者与接收者/标签的关系(是否存在、是否连接)。
  • 异常情况数据:

    • 专门构造会触发JML中定义的各种异常的数据。例如,添加重复ID的Person (EqualPersonIdException),查询不存在路径的两个Person (PathNotFoundException),删除没有权限的公众号文章 (DeleteArticlePermissionDeniedException)。
    • 检查异常抛出后,相关的统计信息(如 ErrorCount)是否正确更新。
  • 特殊结构数据:

    • 链状图、星型图、环形图、多个不连通子图,用于测试图算法如 isCircle, queryShortestPath
    • 构造特定的三元组和“情侣对”结构来验证 queryTripleSumqueryCoupleSum
  • 组合指令数据:

    • 设计一系列指令,前序指令为后续指令创造特定状态。例如,先添加人、关系、标签,再向标签添加人,然后发送群组消息。
  • 随机大数据与手动构造数据结合:

    • 使用脚本生成大量随机合法数据进行压力测试和覆盖度测试。
    • 针对特定逻辑和复杂功能点,手动构造精巧的数据进行深入测试。例如,deleteColdEmoji 指令,需要构造不同热度的表情、包含这些表情的消息,以及普通消息,然后设置不同的 limit 值进行测试。
  • 参照JML规格构造:

    • 仔细阅读JML的 requires 子句来构造触发正常行为和异常行为的输入。
    • 阅读 ensures 子句来确定预期的输出和状态变化,从而验证结果。

二、大模型辅助规格化设计与代码实现的体验

在本单元中,虽然我未直接全程使用大模型辅助完成JML规格撰写和代码实现,但结合以往经验和对大模型能力的理解,我认为可以从以下方面引导大模型完成复杂任务:

1. 规格化设计(JML辅助)

  • 场景一:从自然语言描述生成初步JML

    • 引导方式: 提供清晰、无歧义的方法功能描述、参数说明、预期的正常行为和异常行为。
    • 示例提示: “为一个社交网络中的 addRelation(int id1, int id2, int value) 方法生成JML规格。该方法在两个已存在的、未关联的用户间添加一条值为 value 的双向关系。如果用户id1或id2不存在,抛出 PersonIdNotFoundException。如果id1和id2已存在关系,抛出 EqualRelationException。确保更新双方的熟人列表和关系值。”
    • 分析: 大模型可以生成JML骨架,包括 requires, assignable, ensures, signals 子句。但需要人工审查其完整性(如是否覆盖所有路径)和准确性(如 \old 的使用、副作用的精确描述)。
  • 场景二:解释和优化现有JML

    • 引导方式: 输入一段已有的JML规格,要求大模型解释其含义,或者指出潜在的冗余、不清晰之处,并提出优化建议。
    • 示例提示: “解释以下JML规格的含义,并判断 assignable子句是否可以更精确:public normal_behavior requires containsPerson(id1) && containsPerson(id2) && !getPerson(id1).isLinked(getPerson(id2)); assignable persons[*]; ...
    • 分析: 大模型能帮助理解复杂的JML表达式,但其优化建议可能需要结合具体上下文和性能考量进行甄别。

2. 代码实现(基于JML)

  • 场景一:根据JML生成代码骨架或完整实现

    • 引导方式: 提供完整的JML规格和相关的类/接口定义。
    • 示例提示: “根据以下JML规格为 Network 类中的 queryTripleSum 方法生成Java实现。规格:ensures \result == (\sum int i; 0 <= i && i < persons.length; (\sum int j; i < j && j < persons.length; (\sum int k; j < k && k < persons.length && getPerson(persons[i].getId()).isLinked(getPerson(persons[j].getId())) && getPerson(persons[j].getId()).isLinked(getPerson(persons[k].getId())) && getPerson(persons[k].getId()).isLinked(getPerson(persons[i].getId())); 1)));。考虑性能,维护一个成员变量 tripleSum 并在 addRelationmodifyRelation (删除关系时) 中更新它。”
    • 分析: 大模型可以直接生成迭代实现,但对于性能优化(如增量更新 tripleSum),需要明确提示。生成的代码仍需仔细测试。
  • 场景二:代码逻辑检查与调试辅助

    • 引导方式: 提供JML规格、自己的代码实现以及遇到的问题或错误信息。
    • 示例提示: “我的 sendMessage 方法在处理群发红包消息时,金额分配似乎有误。这是JML规格,这是我的代码。当群组大小为0时,发送者不应扣钱,但现在好像扣了。请帮我分析问题。”
    • 分析: 大模型可以辅助定位逻辑错误,尤其是在对比代码与JML规格时。但它可能无法理解非常细微的业务逻辑或复杂的算法瓶颈。

总结引导策略:

  1. 明确、具体的任务描述: 避免模糊不清的指令。
  2. 提供充足的上下文: 包括相关的JML、类定义、接口定义、已有代码片段。
  3. 迭代式提问与反馈: 不要期望一次完美生成。对模型的输出进行评估,并给出修正指令。
  4. 分解复杂任务: 将大任务拆解成小步骤,逐步引导。例如,先生成JML,再根据JML生成代码。
  5. 强调约束和要求: 如性能要求、特定数据结构的使用、异常处理逻辑。
  6. 利用其模式识别能力,但警惕“一本正经胡说八道”: 大模型擅长模仿和生成模式化的内容,但其输出的正确性必须由人工严格把关。

大模型在简单、模式化的JML生成和基于规格的代码实现方面能提供不错的辅助,但对于复杂逻辑、性能优化和深层语义理解,仍需开发者主导,并将其作为高效的助手而非完全依赖的决策者。

三、本单元的架构设计与图模型维护策略

1. 架构设计

本单元的架构设计遵循了典型的面向接口编程和分层思想。

  • 接口层 (com.oocourse.spec3.main): 由课程组提供,包含 NetworkInterface, PersonInterface, MessageInterface, TagInterface, OfficialAccountInterface 等。这些接口通过JML严格定义了各个组件的行为和契约。
  • 实现层 (根目录下的 Network.java, Person.java 等): 我们需要实现的具体类。
    • Network 类是核心,作为整个社交网络的容器和管理器,负责处理大部分指令,维护 Person, OfficialAccount, Message, Emoji 等对象的集合,并协调它们之间的交互。
    • Person 类表示网络中的用户节点,包含其属性(id, name, age, money, socialValue)、关系(acquaintance, value)、持有的标签(tags)、接收的消息和文章。
    • Message 类及其子类(EmojiMessage, RedEnvelopeMessage, ForwardMessage)封装了消息的属性和特定行为逻辑。
    • Tag 类表示用户标签,内部维护一组 Person
    • OfficialAccount 类表示公众号,维护所有者、关注者、文章、贡献者等信息。
    • Runner 类 (课程组提供) 负责解析输入指令,并通过反射调用我们实现的 Network 中的方法。
    • 异常类 (com.oocourse.spec3.exceptions):自定义的各种异常,部分异常类内部通过 ErrorCount 实现了简单的异常统计。

这种设计将规格与实现分离,使得我们可以专注于实现细节,而不必担心破坏高层调用逻辑,同时也为后续的扩展和维护提供了便利。

2. 图模型构建与维护策略

社交网络本身就是一个图,其中 Person 是节点,Person 之间的关系是边。

  • 节点 (Person) 存储:

    • Network 类中,我使用 private final Map<Integer, PersonInterface> persons; (初始化为 HashMap) 来存储所有的 Person 对象。Key是 personId,Value是 Person 对象。这保证了通过ID可以O(1)平均时间复杂度快速查找用户。
  • 边 (关系) 存储与维护:

    • 关系是双向的。每个 Person 对象内部维护自己的熟人列表和对应的关系值。
    • Person 类中使用:
      • private final Map<Integer, PersonInterface> acquaintance; (HashMap) 存储熟人对象。
      • private final Map<Integer, Integer> value; (HashMap) 存储与对应熟人的关系值。
    • addRelation(id1, id2, value)
      1. Networkpersons 中获取 person1person2 对象。
      2. person1acquaintancevalue 中添加 person2 及关系值。
      3. person2acquaintancevalue 中添加 person1 及关系值。
      4. 维护 tripleSum 这是关键。在添加关系时,需要检查新形成的边 (person1, person2) 是否与 person1 的其他邻居 pxperson2 的其他邻居 py 形成新的三元组。具体策略是遍历 person1 的所有邻居 neighbor1 (除去 person2),检查 neighbor1 是否也是 person2 的邻居。如果是,则 tripleSum++。或者,遍历较少邻居的一方,以优化检查。我的代码中采取了后者:
        // Network.java addRelation
        // ...
        Map<Integer, PersonInterface> acq1 = ((Person) person1).getAcquaintanceMap();
        Map<Integer, PersonInterface> acq2 = ((Person) person2).getAcquaintanceMap();
        boolean condition = acq1.size() < acq2.size();
        Map<Integer, PersonInterface> acq = condition ? acq1 : acq2;
        int id = condition ? id2 : id1; // The other person in the new relation
        PersonInterface person = condition ? person2 : person1; // The other person
        for (PersonInterface neighbor : acq.values()) {
            if (neighbor.getId() != id && neighbor.isLinked(person)) { tripleSum++; }
        }
        
    • modifyRelation(id1, id2, value)
      1. 获取 person1person2
      2. 计算新的关系值 newValue = oldValue + value
      3. 如果 newValue > 0:更新双方 value map 中的值。
      4. 如果 newValue <= 0删除关系
        • 从双方的 acquaintancevalue map 中移除对方。
        • 维护 tripleSum 遍历 person1 的所有邻居 neighbor1 (在删除前,不包括 person2),如果 neighbor1 也是 person2 的邻居,则 tripleSum--
        • 维护Tag内成员关系: JML要求如果因为关系删除导致两人不再是好友,则需要将他们在对方Tag中的记录也删除。
          // Network.java modifyRelation (when removing relation)
          Map<Integer, PersonInterface> acq1 = ((Person) person1).getAcquaintanceMap();
          Map<Integer, PersonInterface> acq2 = ((Person) person2).getAcquaintanceMap();
          for (PersonInterface neighbor : acq1.values()) {
              if (neighbor.getId() != id2 && acq2.containsKey(neighbor.getId())) { tripleSum--; }
          }
          // ... unlink logic ...
          Collection<TagInterface> pt1 = ((Person) person1).getAllTags();
          for (TagInterface tag : pt1) { if (tag.hasPerson(person2)) { tag.delPerson(person2); } }
          // similar for pt2
          
  • 图算法实现:

    • isCircle(id1, id2) (连通性查询) 和 queryShortestPath(id1, id2) (最短路径查询): 均采用广度优先搜索 (BFS) 算法。
      • BFS从起点开始,逐层扩展,直到找到目标节点或遍历完所有可达节点。
      • isCircle:找到目标节点即返回 true
      • queryShortestPath:在BFS过程中记录每个节点到起点的距离,找到目标节点时其距离即为最短路径长度(边权为1)。
      • 使用 Queue (如 ArrayDeque) 辅助BFS,使用 Set (如 HashSet) 或 Map 记录已访问节点和距离。

这种通过在 Person 对象中存储邻接信息,并在 Network 层面进行协调和特定属性(如 tripleSum)维护的策略,在查找和局部修改方面有较好的效率。对于全局图属性的查询,则依赖经典的图算法。

四、性能问题分析与修复及对规格与实现分离的理解

1. 作业中出现的性能问题及其修复

本单元作业对性能有较高要求,尤其是在处理大规模数据和复杂查询时。

  • queryTripleSum (QTS):

    • 潜在问题: 如果每次调用都严格按照JML定义的三重循环去遍历所有人来计算,时间复杂度将是 O(N^3),其中N是总人数。这在大数据量下是不可接受的。
    • 修复策略: 采用增量维护的思想。在 Network 类中维护一个成员变量 private int tripleSum;
      • addRelation(p1, p2, value) 时,遍历 p1 (或 p2,取邻居少的一方) 的所有已有邻居 px,如果 px 也与 p2 (或 p1) 相连,则 (p1, p2, px) 构成一个新的三元组,tripleSum++
      • modifyRelation(p1, p2, value) 导致关系值 <=0 而删除关系时,同样遍历 p1 (或 p2) 的邻居 px,如果 px 也与 p2 (或 p1) 相连,则 (p1, p2, px) 这个三元组被破坏,tripleSum--
    • 效果: QTS指令的时间复杂度变为O(1)。addRelationmodifyRelation (删除时) 的复杂度会增加O(degree),但在社交网络稀疏图的场景下通常优于O(N^3)的查询。
  • queryCoupleSum (QCS):

    • 潜在问题: QCS依赖于 queryBestAcquaintance(id) (QBA)。如果QBA每次都遍历某人的所有熟人来查找value最大且id最小者,其复杂度为O(degree)。那么QCS的复杂度可能是O(N*degree),最坏情况下O(N^2)。
    • 修复策略:Person 类中缓存最佳熟人信息。
      • 增加成员变量 private int bestAcquaintanceId;private int bestAcquaintanceValue; (以及一个标记 hasAcquaintances)。
      • link(person, val) (内部方法,对应 addRelation) 或 modifyValue(person, newValue) (内部方法,对应 modifyRelation) 或 unlink(person) (内部方法,对应关系删除) 时,动态更新这两个缓存值。
        • 添加新关系或修改关系值变大时,直接比较并更新。
        • 删除关系或修改关系值变小,且被影响的是当前最佳熟人时,则需要重新遍历该 Person 的所有熟人来找到新的最佳熟人 (recomputeBestAcquaintance)。
    • 效果: 大部分情况下 queryBestAcquaintance 变为O(1)。QCS的复杂度显著降低,接近O(N),除非频繁发生导致最佳熟人失效的删除操作。
  • Tag.getAgeVar()Tag.getAgeMean()

    • 潜在问题: 如果每次调用都遍历 Tag 内所有 Person 计算年龄总和及平方和,则复杂度为O(TagSize)。
    • 修复策略:Tag 类中维护 private long sumAge;private long sumAgeSquare;
      • Tag.addPerson(person) 时,sumAge += person.getAge(); sumAgeSquare += person.getAge() * person.getAge();
      • Tag.delPerson(person) 时,相应减去其年龄和年龄的平方。
    • 效果: getAgeMean()getAgeVar() 变为O(1)。
  • deleteColdEmoji(limit) (DCE):

    • 潜在问题: JML规格要求删除热度低于 limit 的表情,并删除 messages 列表中所有引用了这些被删除表情的 EmojiMessage。如果实现不当,可能导致多次遍历或低效的删除。
    • 修复策略:
      1. 遍历 emojiHeatMap,找出所有热度低于 limit 的表情ID,存入一个临时集合 emojisToDelete
      2. 遍历 emojisToDelete,从 emojiHeatMap 中移除这些表情。
      3. 使用迭代器遍历 messages 集合。对每个消息,如果它是 EmojiMessageInterface 类型,并且其 emojiId 存在于 emojisToDelete 中,则通过迭代器的 remove() 方法将其从 messages 中删除。
    • 效果: 时间复杂度大致为 O(TotalEmojis + TotalMessages)。

2. 对规格与实现分离的理解

规格与实现分离是面向对象设计中的核心原则之一,JML在本单元中正是体现了这一点。

  • 规格 (JML): 描述了“做什么”(What)。它定义了一个模块(类或方法)的功能、行为契约、前置条件、后置条件、不变量以及可能抛出的异常。规格是模块对外公开的接口和承诺,使用者只需关心规格,无需了解内部如何工作。例如,NetworkInterface 中的JML。
  • 实现 (Java代码): 描述了“怎么做”(How)。它是规格的具体落地,包括数据结构的选择、算法的设计。同一份规格可以有多种不同的实现。例如,我们编写的 Network.java

分离带来的好处:

  1. 抽象性与封装性: 客户端代码仅依赖于稳定的规格(接口),实现细节被隐藏。这降低了模块间的耦合度。
  2. 可维护性与灵活性: 只要不违反规格,实现可以自由修改和优化(如上述性能优化),而不会影响到客户端代码。例如,queryTripleSum 的实现从O(N^3)优化到O(1)查询(配合增量更新),对调用者是透明的。
  3. 可测试性: 规格为测试用例的设计提供了清晰的依据。可以针对规格进行黑盒测试。
  4. 并行开发: 一旦规格确定,不同开发者可以并行实现不同模块,或者一方实现接口,另一方基于接口进行开发。
  5. 代码复用: 遵循统一规格的模块更容易被复用。

在本单元中,我们严格按照JML规格来实现功能。这意味着,即使我们的具体实现(如使用 HashMap 还是 TreeMap,或者算法的具体步骤)有所不同,只要最终行为符合JML的描述,就被认为是正确的。这种分离使得我们可以专注于算法效率和数据结构的优化,而不必担心功能上的偏差(只要实现严格遵守规格)。

五、Junit测试与规格信息

本单元我们学习并实践了Junit测试。利用JML规格信息可以极大地提升Junit测试设计的针对性和有效性。

1. 如何利用规格信息设计Junit测试

JML规格为Junit测试提供了直接的指导:

  • 针对 requires 子句 (前置条件):

    • 正常情况: 构造满足所有 requires 子句的输入数据,验证方法是否能正常执行并产生 ensures 所描述的结果。
    • 异常/边界情况: 构造不满足某个 requires 子句的输入数据。如果JML中对应的 signals 子句指明了应抛出的异常,则使用 assertThrows(ExpectedException.class, () -> { methodCall; }); 来验证异常是否正确抛出。
      • 例如,对于 addRelation,测试当 !containsPerson(id1) 时是否抛出 PersonIdNotFoundException
      • 测试当 getPerson(id1).isLinked(getPerson(id2)) (即关系已存在) 时是否抛出 EqualRelationException
  • 针对 ensures 子句 (后置条件):

    • 验证 \result 对于有返回值的方法,检查返回值是否与 \result 表达式的预期一致。
    • 验证状态变化 (assignable / modifiable):
      • 使用 \old(expression) 来记录方法执行前的状态。
      • 在方法执行后,检查 assignable 子句中列出的变量/对象状态是否按 ensures 描述的那样发生了变化。
      • 例如,在测试 addPerson 后,验证 persons 数组确实包含了新添加的 person,并且其长度增加了1。
    • 验证未改变的状态 (\not_assigned / \not_modified implied): 检查 assignable 子句未列出的重要状态是否保持不变,防止意外副作用。
  • 针对 signals (异常抛出) 和 signals_only (仅抛出指定异常) 子句:

    • 如上所述,使用 assertThrows 来验证在特定条件下是否抛出了预期的异常。
    • 检查抛出的异常对象中的属性是否符合JML描述(虽然本单元的异常类构造函数参数固定,但更复杂的JML可能指定异常对象的状态)。
    • 例如,在 NetworkTest.java 中,虽然没有直接用 assertThrows,但其测试 deleteColdEmoji 时,很多方法(如 addMessage)在内部对异常进行了捕获和打印,这间接反映了对异常路径的考虑。一个更标准的Junit做法是直接断言异常。
  • 针对 invariant (不变量):

    • 虽然Junit主要测试方法级契约,但可以在 setUptearDown 或每个测试方法的开始和结束时检查类不变量是否保持。例如,persons.length == emojiHeatList.length
  • 构造复杂场景:

    • 根据JML中涉及多个对象或复杂逻辑交互的 ensures 子句,构造需要多步操作才能达成的状态,然后进行测试。例如 deleteColdEmoji 的JML规格非常复杂,NetworkTest.java 中的 testEmojisAndMessagesMixed 方法就试图覆盖其多种交互情况。

2. Junit测试检验代码实现与规格一致性的效果

Junit结合JML规格进行测试,对于检验代码实现与规格的一致性非常有效:

  • 自动化与可重复性: Junit测试可以自动执行,快速反馈,保证了修改代码后可以方便地进行回归测试。
  • 精确性: 基于JML的断言(如 assertEquals, assertTrue, assertThrows)可以精确地验证方法的输出、状态变化和异常行为是否符合规格。
  • 覆盖度提升: 有意识地针对JML的每个子句(尤其是边界条件和异常路径)设计测试用例,可以显著提高测试覆盖率,发现更多潜在的bug。
  • 促进对规格的理解: 编写Junit测试的过程,本身也是对JML规格深入理解和细化的过程。如果对规格理解有偏差,测试可能无法通过,从而反过来促进对规格的重新审视。
  • 示例 (NetworkTest.java):
    • NetworkTest.java 主要测试了 deleteColdEmoji 方法。它通过 setUp 创建了初始网络状态。
    • checkEmojiListPropertiescheckMessageListProperties 方法实际上是在验证 deleteColdEmoji JML中关于 emojiIdList, emojiHeatListmessages 状态变化的 ensures 子句。
      • 例如,ensures emojiIdList.length == (\num_of int i; 0 <= i && i < \old(emojiIdList.length); \old(emojiHeatList[i] >= limit)); 这一条,测试代码通过比较新旧列表长度和内容来验证。
      • ensures (\forall int i; 0 <= i && i < \old(messages.length); (\old(messages[i]) instanceof EmojiMessageInterface && containsEmojiId(\old(((EmojiMessageInterface)messages[i]).getEmojiId())) ==> \not_assigned(\old(messages[i])) && (\exists int j; 0 <= j && j < messages.length; messages[j].equals(\old(messages[i]))))); 这部分被 checkMessageListProperties 尝试验证,确保应保留的消息确实被保留且未被错误修改。
    • 测试用例覆盖了空网络、全冷、全热、冷热混合等多种情况。

局限性:
Junit测试无法穷尽所有可能的输入和执行路径,因此不能完全证明代码100%符合规格(这是形式化验证工具的目标)。测试的有效性高度依赖于测试用例的设计质量和覆盖广度。但它仍然是发现不一致性的强大实用工具。

六、本单元学习体会

第三单元是关于JML规格和基于规格开发的单元,对我而言充满了挑战与收获:

  1. JML的学习曲线: 初次接触JML,其语法和各类子句(requires, ensures, assignable, invariant, \old, \forall, \exists, \sum 等)的学习需要投入较多时间和精力。理解JML的精确语义,并能用它准确描述方法行为,是一大难点。但一旦掌握,它就成为理解和设计代码的有力工具。

  2. 规格与实现的分离思想深化: 理论上知道这个原则,但本单元通过JML强制我们首先思考“做什么”(规格),然后才是“怎么做”(实现),深刻体会到了这一思想的好处。它使得代码结构更清晰,也更容易进行模块化测试和维护。

  3. 图论算法的应用: 社交网络天然是图结构,本单元涉及的 isCircle (连通性判断)、queryShortestPath (最短路径)、queryTripleSum (三元闭包计数) 等,都是图论中的经典问题。这促使我复习和实践了BFS等算法,并思考如何在工程项目中应用它们。

  4. 性能优化的重要性: 简单的遵循JML字面意思去实现某些查询(如QTS)会导致严重的性能问题。这迫使我们思考如何通过缓存、增量计算等方式优化实现,同时又不违反规格。这让我理解到,好的实现不仅要正确,还要高效。

  5. 测试驱动的开发意识: JML规格为编写测试用例(尤其是单元测试)提供了非常好的指引。先理解规格,再构思测试数据,然后编写代码,最后用测试验证。这种流程能更早地发现问题,提高代码质量。Junit的使用也让测试过程更加系统化和自动化。

  6. 细节决定成败: JML规格非常严格,任何一个条件的疏忽或误解都可能导致实现错误。例如,assignable子句的范围,\old关键字的使用时机,各种边界条件的处理,都需要非常仔细。多次因为对JML细节理解不到位而导致bug。

  7. 代码的健壮性: 大量的自定义异常类和 signals 子句,强调了程序在遇到非法输入或状态时,应能优雅地处理并给出明确反馈,而不是直接崩溃。

总的来说,第三单元虽然难度较大,但对于提升抽象思维能力、严谨设计能力和测试能力都有很大帮助。它让我们从关注代码的“形”转向关注代码的“神”——即代码所应遵循的行为契约。这种基于规格的开发方法,是构建复杂、可靠软件系统的重要基石。

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

271

社区成员

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

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