OO第三单元总结——JML规格化设计

朱雄伟-22373498 学生 2024-05-16 11:19:41

OO第三单元总结

写在前面

本单元的主题是“规格化设计”,通过学习JML语言体验了用规格化语言进行契约性设计的全流程。我觉得这个单元的目的并不是仅仅让我们掌握JML来进行规格约束:毕竟JML只是许多进行规格约束的语言之一。相比之下,我们应该把本单元的中心放在契约式编程的优点上:高可靠性、高可复用性、便于测试等等。

就编程复杂度而言,本单元与前两个单元相比的确有了明显的下降,似乎只需要忠实的实现官方包里的JML规格即可。但是这就是契约式编程的一个非常重要的概念:规格与实现分离,针对一种特定的规格可能会有很多实现方法,因此我们在编程时还需要特别注意代码运行的效率,否则很容易TLE。

关键概念

黑箱测试

黑箱测试(Black Box Testing)也被称为功能测试或数据驱动测试。在这种测试中,测试者不需要了解程序的内部代码和结构,只关注软件应用的功能性能是否符合需求规格说明。测试者将系统视为一个“黑箱”,通过输入值观察输出,以此来检验软件的行为。

黑箱测试的优点在于:

  1. 不需要编程知识,任何非技术背景的测试者都可以执行。

  2. 测试更加客观,专注于用户需求和软件功能。

当然,黑箱测试也有一些不足:

  1. 可能无法覆盖代码中的所有路径,因此有时可能会错过一些内部缺陷。

  2. 对性能问题的诊断较为困难,因为无法看到程序内部。

白箱测试

白箱测试(White Box Testing)也称为结构测试或透明箱测试。这种测试要求测试者具有对应用程序的内部工作知识,包括代码、结构、逻辑和内部设计。测试者需要根据程序的内部路径、条件分支等设计测试用例,以确保测试到代码的每一个部分。

优点:

  1. 可以发现一些隐藏的错误,例如逻辑错误、路径覆盖问题等。

  2. 有助于优化代码,提高代码的执行效率。

  3. 有助于检查代码是否符合设计规范。

缺点:

  1. 需要深厚的编程和内部代码结构知识,对测试者的要求较高。

  2. 测试过程可能较为复杂和耗时。

单元测试

单元测试(Unit Testing)主要针对程序中的最小可测试单元进行检查,通常是指一个函数或方法。单元测试的目的是验证这些单元在各种输入条件下是否按预期工作。这种测试通常由开发者在编码过程中进行,以确保每个部分都能正常工作。单元测试是白箱测试的一种形式,因为它需要对代码内部结构有所了解。

  • 可以帮助发现和解决单元级别的问题,在早期发现错误,减少后期修复成本。

  • 验证单个单元(类或方法)的功能是否正确,提高代码的可维护性和可靠性。

功能测试

功能测试(Functional Testing)聚焦于软件的业务需求是否得到满足。它通过测试软件的功能来确保每个功能都按照需求规格书执行。功能测试属于黑箱测试类型,测试者不需要知道应用程序的内部代码如何工作。

  • 确保软件产品的功能满足用户的实际需求。

  • 检查用户界面和用户体验是否符合设计规范。

集成测试

集成测试(Integration Testing)关注单独的模块或单元组合在一起时是否能正常协同工作。在单元测试后进行集成测试可以发现模块间接口的问题。这类测试可以采用自顶向下或自底向上等策略来执行。

  • 确认不同模块之间的数据传递和控制流程的正确性。

  • 检测在模块集成过程中可能出现的接口错误。

压力测试

压力测试(Stress Testing)是为了确定软件在超出正常业务负荷或极限情况下的稳定性和可靠性。它通过将系统资源如CPU、内存、网络等推向极限来评估软件的性能极限。

  • 确定软件的可扩展性和最大运行能力。

  • 揭示在高负载下可能出现的问题,如死锁和内存泄露。

回归测试

回归测试(Regression Testing)是在软件修改后进行的测试,以确保这些修改没有引入新的错误,并且既有功能仍然正常工作。回归测试对于持续维护和更新的软件项目尤为重要。

  • 验证改动点周围的代码仍然按预期工作。

  • 确保软件的稳定性和可靠性,在发布新版本前捕获问题。

数据构造

数据构造核心目标是增加覆盖率,减少无效数据。可以构造没人、有人没关系、有人有关系无三角关系全关系等数据类型,尽可能做到所有情况全覆盖。

架构设计

hw9:查找两人是否联通

本次作业的热点函数是isCircle函数,判断id分别为id1id2的两个人是否连接。

     /*@ public normal_behavior
       @ requires containsPerson(id1) &&
       @          containsPerson(id2);
       @ assignable \nothing;
       @ ensures \result == (\exists Person[] array; array.length >= 2;
       @                     array[0].equals(getPerson(id1)) &&
       @                     array[array.length - 1].equals(getPerson(id2)) &&
       @                      (\forall int i; 0 <= i && i < array.length - 1;
       @                      array[i].isLinked(array[i + 1])));
       @ also
       @ public exceptional_behavior
       @ signals (PersonIdNotFoundException e) !containsPerson(id1);
       @ signals (PersonIdNotFoundException e) containsPerson(id1) &&
       @                                       !containsPerson(id2);
       @*/
     public /*@ pure @*/ boolean isCircle(int id1, int id2) throws PersonIdNotFoundException;

最简单的方法是使用dfs单次查找,但是根据我的尝试,这样做极可能会TLE。根据学长博客的提示,我在实现这个函数的时候采用了一种类似于“并查集”的思想来处理连通块。

什么是连通块呢?用离散2的话来说,也可以叫他“连通分量”。我们在addPersonaddRelationmodifyRelation的时候,需要维护连通块。按照不同的维护方式,使用setBlocksdeleteBlock两个私有方法。

 public class MyNetwork implements Network {
     private final ArrayList<HashMap<Integer, MyPerson>> blocks = new ArrayList<>();
     
     public void addPerson(Person person) throws EqualPersonIdException {
         // ---
         HashMap<Integer, MyPerson> tempBlock = new HashMap<>();
         tempBlock.put(person.getId(), p);
         blocks.add(tempBlock);
     }
     
     public void addRelation(int id1, int id2, int value)
             throws PersonIdNotFoundException, EqualRelationException {
         // ---
         setBlocks(id1, id2);
     }
     
     public void modifyRelation(int id1, int id2, int value)
             throws PersonIdNotFoundException, EqualPersonIdException, RelationNotFoundException {
         // ---
         if (/* something */) {
         } else {
             // ---
             deleteBlock(id1, id2);
         }
     }
 }

setBlocks用来添加关系后合并连通块,deleteBlock用于删除关系后重建连通块。

 private void setBlocks(int id1, int id2) {
     HashMap<Integer, MyPerson> tempBlock1 = new HashMap<>();
     for (HashMap<Integer, MyPerson> block : blocks) {
         if (block.containsKey(id1)) {
             tempBlock1 = block;
             break;
         }
     }
     if (tempBlock1.containsKey(id2)) {
         return;
     }
     HashMap<Integer, MyPerson> tempBlock2 = new HashMap<>();
     Iterator<HashMap<Integer, MyPerson>> it = blocks.iterator();
     while (it.hasNext()) {
         HashMap<Integer, MyPerson> block = it.next();
         if (block.containsKey(id2)) {
             tempBlock2 = block;
             it.remove();
             break;
         }
     }
     tempBlock1.putAll(tempBlock2);
 }
 ​
 private void deleteBlock(int id1, int id2) {
     HashMap<Integer, MyPerson> tempBlock = null;
     for (HashMap<Integer, MyPerson> block : blocks) {
         if (block.containsKey(id1) && block.containsKey(id2)) {
             tempBlock = block;
             break;
         }
     }
     if (tempBlock != null) {
         HashMap<Integer, MyPerson> newBlock = new HashMap<>();
         getNewBlock(id1, tempBlock, newBlock);
         if (!newBlock.containsKey(id2)) {
             tempBlock.keySet().removeAll(newBlock.keySet());
             blocks.add(newBlock);
         }
     }
 }
 ​
 private void getNewBlock(int id, HashMap<Integer, MyPerson> persons,
                          HashMap<Integer, MyPerson> newBlock) {
     MyPerson p = (MyPerson) getPerson(id);
     newBlock.put(id, p);
     for (Map.Entry<Integer, MyPerson> entry : p.getAcquaintanceMap().entrySet()) {
         MyPerson person = entry.getValue();
         if (persons.containsKey(person.getId()) && !newBlock.containsKey(person.getId())) {
             getNewBlock(person.getId(), persons, newBlock);
         }
     }
 }

此外,本次作业涉及到对连通块数量和三元组数量的查询,我们需要采取一些方法降低其复杂度。对于连通块数量,我们只需要返回blocks.size()即可;而对于三元组数量,我采用的是动态维护的方法。

三元组数量只可能在addRelationmodifyRelation的时候发生改变,所以只要在这两个方法里调用setQts方法维护三元组数量即可。

 private void setQts(int id1, int id2, int mode) {
     for (int id : persons.keySet()) {
         if (id == id1 || id == id2) {
             continue;
         }
         if (persons.get(id1).isLinked(persons.get(id))
             && persons.get(id2).isLinked(persons.get(id))) {
             qts = qts + mode;
         }
     }
 }

hw10:Tag相关

本次作业主要有queryTagValueSumqueryTagAgeVarqueryBestAcquaintance需要在实现规格的时候需要进行优化的设计。与上一次作业的qts相似,我们都可以采用动态维护的思想。

对于前两个方法,我们在Tag对象里进行如下操作:

 public void setAge(int age, boolean op) {
     int ageVarSum = 0;
     if (op) {
         ageSum += age;
         ageMean = ageSum / persons.size();
         for (int id : persons.keySet()) {
             int personAge = persons.get(id).getAge();
             ageVarSum += (personAge - ageMean) * (personAge - ageMean);
         }
         ageVar = ageVarSum / persons.size();
     } else {
         ageSum -= age;
         ageMean = persons.isEmpty() ? 0 : ageSum / persons.size();
         for (int id : persons.keySet()) {
             int personAge = persons.get(id).getAge();
             ageVarSum += (personAge - ageMean) * (personAge - ageMean);
         }
         ageVar = persons.isEmpty() ? 0 : ageVarSum / persons.size();
     }
 }
 ​
 public void setValueSum(Person person, int opcode, int value) {
         MyPerson myPerson = (MyPerson) person;
         if (opcode == 1) { // add
             for (int id : persons.keySet()) {
                 if (myPerson.isLinked(persons.get(id))) {
                     valueSum += 2 * myPerson.queryValue(persons.get(id));
                 }
             }
         } else if (opcode == 2) { // sub
             for (int id : persons.keySet()) {
                 if (myPerson.isLinked(persons.get(id))) {
                     valueSum -= 2 * myPerson.queryValue(persons.get(id));
                 }
             }
         } else if (opcode == 3) { // set
             valueSum += 2 * value;
         }
     }

对于queryBestAcquaintance,在MyPerson中维护一个私有变量bestId,初始值为-1,并另外标记是否找到。

当每次加边时,分别对person1和person2更新该值。每次删边时操作大致相同。

 public void setAcquaintance(Person person, int value) {
     if (queryValue(person) + value > 0) {
         valueMap.merge(person.getId(), value, Integer::sum);
     } else {
         valueMap.remove(person.getId());
         acquaintanceMap.remove(person.getId());
     }
     if (person.getId() == bestId && value >= 0) {
         bestValue += value;
     } else if (person.getId() == bestId && value < 0) {
         findBestAcq();
     } else if (person.getId() != bestId && value > 0) {
         if (valueMap.get(person.getId()) > bestValue
             || (valueMap.get(person.getId()) == bestValue
                 && person.getId() < bestId)) {
             bestValue = valueMap.get(person.getId());
             bestId = person.getId();
             gotBestId = true;
         }
     }
 }

至于大家经常优化的queryShortestPath方法,我只采用了比较简单的bfs来实现,看来时间复杂度也没有超。

 public int queryShortestPath(int id1, int id2)
             throws PersonIdNotFoundException, PathNotFoundException {
         if (noContainsPerson(id1)) {
             throw new MyPersonIdNotFoundException(id1);
         } else if (noContainsPerson(id2)) {
             throw new MyPersonIdNotFoundException(id2);
         } else if (!network.isCircle(id1, id2)) {
             throw new MyPathNotFoundException(id1, id2);
         } else if (id1 == id2) {
             return 0;
         }
         LinkedList<Integer> queue = new LinkedList<>();
         HashMap<Integer, Integer> distanceMap = new HashMap<>();
         HashSet<Integer> visited = new HashSet<>();
         queue.addLast(id1);
         distanceMap.put(id1, 0);
         visited.add(id1);
         while (!queue.isEmpty()) {
             // ~~~
         }
         return -1;
     }

hw11

这次作业需要优化的地方很少,只需要实现JML规定的规格即可。

规格仅仅是一种契约,规格指明了方法需要实现的功能是什么,相应的变与不变的约束。但是,针对一种特定的规格可能会有很多实现方法,正所谓“条条大路通罗马”。因此在编程实现规格的时候时要记住规格与实现分离,特别注意代码运行的效率,否则很容易TLE。

Bug分析

我在hw10和hw11中均出现了bug,其中hw11的bug较为惨痛。前者是因为当Person未找到/不存在bestId的时候,就默认将bestId设置为-1,然而要是真有一个人的id是-1,那就会在未预期的情况下报Exception

hw11的问题是因为自己过于想当然,没有完全按照JML的规格设计造成大量报错。这应该是个可以避免的错误。

Junit测试

Junit本质就是检查各种各样的条件是否正确,因此我们需要将JML的所有ensure利用Junit的方法进行断言操作。

以hw11中的deleteCodeEmoji方法为例:

     /*@ public normal_behavior
       @ assignable emojiIdList, emojiHeatList, messages;
       @ ensures (\forall int i; 0 <= i && i < \old(emojiIdList.length);
       @          (\old(emojiHeatList[i] >= limit) ==>
       @          (\exists int j; 0 <= j && j < emojiIdList.length; emojiIdList[j] == \old(emojiIdList[i]))));
       @ ensures (\forall int i; 0 <= i && i < emojiIdList.length;
       @          (\exists int j; 0 <= j && j < \old(emojiIdList.length);
       @          emojiIdList[i] == \old(emojiIdList[j]) && emojiHeatList[i] == \old(emojiHeatList[j])));
       @ ensures emojiIdList.length ==
       @          (\num_of int i; 0 <= i && i < \old(emojiIdList.length); \old(emojiHeatList[i] >= limit));
       @ ensures emojiIdList.length == emojiHeatList.length;
       @ ensures (\forall int i; 0 <= i && i < \old(messages.length);
       @          (\old(messages[i]) instanceof EmojiMessage &&
       @           containsEmojiId(\old(((EmojiMessage)messages[i]).getEmojiId()))  ==> \not_assigned(\old(messages[i])) &&
       @           (\exists int j; 0 <= j && j < messages.length; messages[j].equals(\old(messages[i])))));
       @ ensures (\forall int i; 0 <= i && i < \old(messages.length);
       @          (!(\old(messages[i]) instanceof EmojiMessage) ==> \not_assigned(\old(messages[i])) &&
       @           (\exists int j; 0 <= j && j < messages.length; messages[j].equals(\old(messages[i])))));
       @ ensures messages.length == (\num_of int i; 0 <= i && i < \old(messages.length);
       @          (\old(messages[i]) instanceof EmojiMessage) ==>
       @           (containsEmojiId(\old(((EmojiMessage)messages[i]).getEmojiId()))));
       @ ensures \result == emojiIdList.length;
       @*/
     public int deleteColdEmoji(int limit);

这里有八个ensures,最简单的实现方式就是对这几个ensures以及assignable进行测试即可,数据并不需要设计的特别复杂。

 public class Test {
 ​
     @org.junit.Test
     public void deleteColdEmoji() throws Exception {
         // 设计数据
         final int limit = 5;
         Message[] messagesBefore = network.getMessages();
         int[] emojiIdListBefore = network.getEmojiIdList();
         int[] emojiHeatListBefore = network.getEmojiHeatList();
         int ret = network.deleteColdEmoji(limit);
         Message[] messagesAfter = network.getMessages();
         int[] emojiIdListAfter = network.getEmojiIdList();
         int[] emojiHeatListAfter = network.getEmojiHeatList();
 ​
         assertEquals(emojiHeatListAfter.length, emojiIdListAfter.length);
         assertEquals(emojiHeatListBefore.length, emojiIdListBefore.length);
 ​
         boolean flag = true;
         // ensures 1
         for (int i = 0; i < emojiIdListBefore.length; i++) {
             int heat = emojiHeatListBefore[i];
             if (heat >= limit) {
                 boolean found = false;
                 for (int j = 0; j < emojiIdListAfter.length; j++) {
                     if (emojiIdListAfter[j] == emojiIdListBefore[i]) {
                         found = true;
                         break;
                     }
                 }
                 if (!found) {
                     flag = false;
                     break;
                 }
             }
         }
         assertTrue(flag);
 ​
         // ensures 2
         for (int i = 0; i < emojiIdListAfter.length; i++) {
             boolean found = false;
             for (int j = 0; j < emojiIdListBefore.length; j++) {
                 if (emojiIdListAfter[i] == emojiIdListBefore[j]
                         && emojiHeatListAfter[i] == emojiHeatListBefore[j]) {
                     found = true;
                     break;
                 }
             }
             if (!found) {
                 flag = false;
                 break;
             }
         }
         assertTrue(flag);
 ​
         // ensures 3
         int cnt = 0;
         for (int i = 0; i < emojiIdListBefore.length; i++) {
             if (emojiHeatListBefore[i] >= limit) {
                 cnt++;
             }
         }
         assertEquals(cnt, emojiIdListAfter.length);
 ​
         // ensures 4
         assertEquals(emojiHeatListAfter.length, emojiIdListAfter.length);
         assertEquals(emojiHeatListBefore.length, emojiIdListBefore.length);
 ​
         // ensures 5
         for (int i = 0; i < messagesBefore.length; i++) {
             if (messagesBefore[i] instanceof MyEmojiMessage
                     && network.containsEmojiId(((MyEmojiMessage) messagesBefore[i]).getEmojiId())) {
                 boolean found = false;
                 for (int j = 0; j < messagesAfter.length; j++) {
                     if (messagesBefore[i].equals(messagesAfter[j])) {
                         found = true;
                         break;
                     }
                 }
                 if (!found) {
                     flag = false;
                     break;
                 }
             }
         }
         assertTrue(flag);
 ​
         // ensures 6
         for (int i = 0; i < messagesBefore.length; i++) {
             if (!(messagesBefore[i] instanceof MyEmojiMessage)) {
                 boolean found = false;
                 for (int j = 0; j < messagesAfter.length; j++) {
                     if (messagesBefore[i].equals(messagesAfter[j])) {
                         found = true;
                         break;
                     }
                 }
                 if (!found) {
                     flag = false;
                     break;
                 }
             }
         }
         assertTrue(flag);
 ​
         // ensures 7
         cnt = 0;
         for (int i = 0; i < messagesBefore.length; i++) {
             if (!(messagesBefore[i] instanceof MyEmojiMessage)) {
                 cnt++;
             } else if (network.containsEmojiId(((MyEmojiMessage) messagesBefore[i]).getEmojiId())) {
                 cnt++;
             }
         }
         assertEquals(cnt, messagesAfter.length);
 ​
         // ensures 8
         assertEquals(ret, emojiIdListAfter.length);
     }
 }

最后

通过这个过程,我们可以体会到编写JML确实非常辛苦,应当意识到JML带来的多个优点。契约式编程不仅是辅助验证程序正确性的工具,更重要的是理解它在一些复杂且关键的系统(如航空航天控制系统)中的重要性。在这些领域,任何小的疏忽和错误都可能造成极其严重的后果,而规格化设计在这种情况下成为最佳的检测方法。

在这个单元中,我接触到了许多算法实践,如并查集、通过堆优化的优先队列、变量维护等,我从中学到了很多,也思考了很多,收获颇丰。

相较于前两个单元,这个单元的难度较小,因为思路已在JML中体现。即使是直接翻译JML,也能实现基本功能。但是,要在强测中获得高分,绝不能轻视任何一个方法。对JML理解不全面可能会忽视一些隐蔽的条件,或甚至完全违背JML的要求;未进行性能优化也可能导致运行时间过长。因此,本单元实际上考察的是我们是否足够细心,包括细心阅读规格和细心构造数据进行测试。只有做到这两点,才能较好地完成本单元的作业。然而,三次作业所能覆盖的知识有限,我对“契约式编程”的理解仍处于初学阶段,未来还需进一步深入探索和应用。

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

301

社区成员

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

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