OO-Unit3-总结

杨荣津-21375077 学生 2024-05-19 01:42:42

一、分析本单元的测试过程

1.1 谈谈你对黑箱测试、白箱测试的理解

  • 黑箱测试
    • 黑箱测试的特点: 测试者不需要了解被测程序的内部实现和结构,只关注被测程序的功能是否符合要求
    • 结合本单元,实际运用黑箱测试
      • 首先,我利用python搭建了一个数据生成器(代码量2000+行,在下文介绍),可以根据作业要求生成各种输入指令。
      • 然后将生成的数据作为输入,一起投喂给不同同学写的程序。
      • 最后再对比每个同学输出之间的差异,来判断这些程序是否符合功能预期,以及是否出现了问题。
  • 白箱测试
    • 白箱测试的特点: 测试者需要了解被测程序的具体代码和结构,并且基于代码逻辑来设计测试用例
    • 结合本单元,实际运用白箱测试
      • 由于白箱测试需要了解代码的具体内容,因此适用于在JUnit中进行白箱测试
      • 基于代码的分支逻辑,设计出可以覆盖所有情况分支的数据。
      • 然后运行JUnit代码,来查看被测试的某个或者多个方法是否符合预期,覆盖率是否达到要求。

1.2 对单元测试、功能测试、集成测试、压力测试、回归测试的理解

  • 单元测试

    • 单元测试的特点: 单元测试是对程序中一个函数、方法或类进行测试,在开发过程中编写和执行
    • 结合本单元,实际运用单元测试
      在三次作业中,使用JUnit分别完成了对MyNetwork类中queryTripleSum(), queryCoupleSum(), deleteColdEmoji()三个方法的单元测试
  • 功能测试

    • 功能测试的特点: 和黑箱测试相似,不关注代码内部实现,仅测试功能输出。
    • 结合本单元,实际运用功能测试
      在使用JUnit对方法进行单元测试,以及使用数据评测机的时候,我都使用了功能测试的思想,关注输入和期望的输出,而不关注被测程序的具体实现。
  • 集成测试

    • 集成测试的特点: 集成测试是将多个单元或模块集成在一起进行测试,验证它们之间的接口和交互是否正确,集成测试在单元测试之后进行
    • 结合本单元,实际运用集成测试
      三次作业累计涉及7个类:MyNetwork, MyPerson, MyTag, MyMessage, MyRedEnvepoleMessage, MyNoticeMessage, MyEmojiMessage。每个类都拥有各自的方法,因此对每个类进行整体的测试就是一种集成测试,可以使用JUnit来实现对每个类的整体测试。
  • 压力测试

    • 压力测试的特点: 压力测试是通过施加超出正常运行条件的负载来测试程序在极端条件下的表现。
    • 结合本单元,实际运用集成测试
      在使用数据生成器的时候,我运用了压力测试,对于总人数规模为20人的Network,生成了10w+指令,用大量的数据达到压力测试的目的,确保能测试到每一种分支。同时使程序算法的性能差距暴露得更加明显。
  • 回归测试

    • 回归测试的特点: 回归测试是对软件修改(如修复缺陷、功能改进)后进行的测试,确保修改没有引入新的缺陷或影响已有功能。
    • 结合本单元,实际运用回归测试
      在出现bug之后,需要对程序进行修复。然后再使用修复前同样的数据进行测试,确保错误已经被修复,并且没有出现新的bug。

      1.3 数据构造有何策略

  • 设计数据生成器

    • 首先把数据生成分为四个阶段

      阶段指令1指令2指令3指令4
      第一阶段:构建网络add personadd relation\\
      第二阶段:修改关系mofidy relation\\\
      第三阶段:标签相关add tagdelete tagadd person to tagdelete person from tag
      第四阶段:消息相关store emoji idadd messagesend message\
    • 然后把query指令分为3类
      查询一: 和图相关

      指令简写
      query_valueqv
      query_circleqci
      query_block_sumqbs
      query_triple_sumqts

      查询二: 和熟人相关

      指令简写
      query_tag_value_sumqtvs
      query_tag_age_varqtav
      query_best_acquaintanceqba
      query_couple_sumqcs
      query_shortest_pathqsp

      查询三: 和发送信息相关

      指令简写
      query_social_valueqsv
      query_received_messagesqrm
      query_popularityqp
      delete_cold_emojidce
      query_moneyqm
    • 根据JML确定每种指令涉及到的异常行为,在数据生成的时候要触发每一条指令的每一种异常行为

      • 以mr指令为例,生成mr指令的格式为mr id1 id2 value, 因此可能触发的错误有:

        1. 网络中不存在id1或者id2这个人
        2. 网络中存在id1和id2,但是id1和id2相等
        3. 网络中存在id1和id2,并且id1和id2不相等,但是两个人之间不是熟人
        4. 网络中存在这两个人并且是熟人,则进行mr的增值操作
        5. 网络中存在这两个人并且是熟人,则进行mr的删除操作
          def mr():
           choose = random.randint(1,5)
           if choose==1:
               mr_wrong_unexisted_id()
           elif choose==2:
               mr_wrong_unexisted_rel()
           elif choose==3:
               mr_wrong_same_id()
           elif choose==4:
               mr_set_value("add")
           elif choose==5:
               mr_set_value("delete")
          
      • 并且在许多指令中,都会涉及到:

        1. 获取已经存在的id,获取不存在的id
        2. 获取已经存在的关系,获取不存在的关系
        3. 获取某个人已经有的标签,获取某个人没有的标签
          因此,针对这些常用的操作,可以封装成方法统一使用。
        • id_set = []作为全局数组,存储了所有人的id,定义get_existed_id()来随机获取其中的id:

          def get_existed_id():
              index = random.randint(0, len(id_set) - 1)
              id = id_set[index]
              return id
          
        • 并且考虑到,有时我们需要获得不存在的id可能会在数据生成器所配置的参数的范围内,或者范围外,所以需要两种随机获取不存在的id的方法

          def get_unexisted_id_overlimit():
              cnt = 0
              id = random.randint(id_limit_down, id_limit + 100)
              while id in id_set:
                  id = random.randint(id_limit_down, id_limit + 100)
                  cnt += 1
                  if cnt == 2*id_limit:
                      sys.stderr.write("在id_limit内id已用尽\n")
                      exit()
              return id
          
          def get_unexisted_id_withinlimit():
              cnt = 0
              id = random.randint(id_limit_down, id_limit)
              while id in id_set:
                  id = random.randint(id_limit_down, id_limit)
                  cnt += 1
                  if cnt == 3*id_limit:
                      sys.stderr.write("在id_limit内id已用尽\n")
                      exit()
              return id
          
        • rel_set = []作为全局数组,存储了所有关系的键值对,并且为了保证唯一性,所有关系都按照{id_min:id_max}的键值对的形式存储在rel_set

        • rel_value = {}用于存储每个关系的value值,形式是使用一个二元组和value组成的键值对(id_min,id_max):value来存储每个关系的value值

        • 然后使用以下两个方法来随机获取存在的关系,或者不存在的关系:

          def get_unexisted_rel():
              id1 = get_existed_id()
              id2 = get_existed_id()
              while id1 == id2:
                  id2 = get_existed_id()
              idmin = id1 if id1 < id2 else id2
              idmax = id2 if idmin == id1 else id1
              cnt = 0
              while {idmin: idmax} in rel_set:
                  id1 = get_existed_id()
                  id2 = get_existed_id()
                  while id1 == id2:
                      id2 = get_existed_id()
                  idmin = id1 if id1 < id2 else id2
                  idmax = id2 if idmin == id1 else id1
                  cnt += 1
                  if cnt == 3 * len(id_set):
                      sys.stderr.write("无法再添加新的边\n")
                      exit()
              return {idmin:idmax}
          
          def get_existed_rel():
              index = random.randint(0, len(rel_set) - 1)
              return rel_set[index]
          
        • 其他指令的生成类似,这里不再赘述

      • 根据一开始设计的四个阶段,来生成指令,并且把各种查询指令穿插在其中,以保证数据的复杂性
        add person to tag为例,根据配置的参数,输出一定数量的att指令。
        然后再获取一个随机数,根据随机数的值来选择是否在att指令之后执行一次查询指令,以及执行那种查询指令
        并对不同的指令赋予权重,qtvsqtav指令对程序的算法效率要求更高,更耗时,为了测试程序的效率,给这两个查询指令更高的权重,以有目的生成更多的qtvsqtav指令

        for _ in range(times["att_times"]):
            att()
            # qtvs qtav      -> 3
            # qc qci qba qsp -> 2
            # qbs qts qcs    -> 1
            for _ in range(random_query_time3):
                possible = random.randint(-10, 11)
                if possible == -5 or possible == -4 or possible == -3:
                    query_times["qtvs_times"] -= 1
                    qtvs()
                elif possible == -2 or possible == -1 or possible == 0:
                    query_times["qtav_times"] -= 1
                    qtav()
                elif possible == 1 or possible == 2:
                    query_times["qv_times"] -= 1
                    qv()
                elif possible == 3 or possible == 4:
                    query_times["qci_times"] -= 1
                    qci()
                elif possible == 5 or possible == 6:
                    query_times["qba_times"] -= 1
                    qba()
                elif possible == 7 or possible == 8:
                    query_times["qsp_times"] -= 1
                    qsp()
                elif possible == 9:
                    query_times["qbs_times"] -= 1
                    qbs()
                elif possible == 10:
                    query_times["qts_times"] -= 1
                    qts()
                elif possible == 11:
                    query_times["qcs_times"] -= 1
                    qcs()
        
      • add message的阶段也是类似的设计,只不过测试涉及到更多查询指令,因此情况会多一些

        for _ in range(times["am_all_times"]):
            choose = get_choose(8)
            if choose == 1:
                am()
            elif choose == 2:
                arem()
            elif choose == 3:
                anm()
            else:
                aem()
            # sm: evenly spaced
            pos = random.randint(1,100)
            if pos <= 8:
                sm()
                times["sm_times"] -= 1
            # cn: evenly spaced
            pos = random.randint(1, 100)
            if pos <= 3:
                cn()
                times["cn_times"] -= 1
            # sei: evenly spaced
            possibility = 10 / times["am_all_times"]
            pos = random.randint(1,100) / 100
            if pos <= possibility:
                sei()
            # qsv qrm qp qm  -> 10
            # qtvs qtav      -> 3
            # qc qci qba qsp -> 2
            # qbs qts qcs    -> 1
            for _ in range(random_query_time3):
                possible = random.randint(-45, 11)
                if -45 <= possible and possible <= -36:
                    query_times["qsv_times"] -= 1
                    qsv()
                elif -35 <= possible and possible <= -26:
                    query_times["qrm_times"] -= 1
                    qrm()
                elif -25 <= possible and possible <= -16:
                    query_times["qp_times"] -= 1
                    qp()
                elif -15 <= possible and possible <= -6:
                    query_times["qm_times"] -= 1
                    qm()
                elif possible == -5 or possible == -4 or possible == -3:
                    query_times["qtvs_times"] -= 1
                    qtvs()
                elif possible == -2 or possible == -1 or possible == 0:
                    query_times["qtav_times"] -= 1
                    qtav()
                elif possible == 1 or possible == 2:
                    query_times["qv_times"] -= 1
                    qv()
                elif possible == 3 or possible == 4:
                    query_times["qci_times"] -= 1
                    qci()
                elif possible == 5 or possible == 6:
                    query_times["qba_times"] -= 1
                    qba()
                elif possible == 7 or possible == 8:
                    query_times["qsp_times"] -= 1
                    qsp()
                elif possible == 9:
                    query_times["qbs_times"] -= 1
                    qbs()
                elif possible == 10:
                    query_times["qts_times"] -= 1
                    qts()
                elif possible == 11:
                    query_times["qcs_times"] -= 1
                    qcs()
        
      • 配置参数,方便地控制测试数据的复杂度和条数
        为了更好的进行压力测试,同时也照顾到互测3000条指令的限制,我设计了几个参数,来控制数据生成的复杂度和指令数量
        同时为了更方便的看懂指令,这里对不同对象的id进行了范围划分
        person的id范围在[1,100]
        tag的id范围在[5000,5500]
        message的id范围在[1000,2000]

        # 1. id and tag range
        id_limits = [1,10,20,100,10000,1000000,1000000000,2100000000]
        id_limit_down = id_limits[0]
        id_limit = id_limits[3]
        
        tag_limits = [5000,5500]
        tag_line = tag_limits[0]
        tag_limit = tag_limits[1]
        
        msg_limits = [100,200,500,1000]
        msg_line = 1000
        msg_limit = msg_line + msg_limits[3]
        
        emj_limits = [10,20,100,1000]
        emj_line = 101
        emj_limit = emj_line + emj_limits[2]
        

        然后指令每条指令的生成数量,来直接控制最后生成的测试数据的规模

        # 2. operate times
        times={
            "ap_times":150,
            "ar_times":500,
            "mr_times":100,
        
            "at_times":500,
            "dt_times":100,
            "att_times":2000,
            "dft_times":200,
        
            "am_all_times":4000,
            "sm_times":800,
            "cn_times":50,
            "sei_times":100,
        
            "dce_times":50
        }
        query_times_ori = {
            "qv_times":5000,
            "qci_times":5000,
            "qtvs_times":5000,
            "qtav_times":5000,
            "qba_times":5000,
            "qsp_times":5000,
        
            "qbs_times":5000,
            "qts_times":5000,
            "qcs_times":5000,
        
            "qsv_times":5000,
            "qrm_times":5000,
            "qp_times":5000,
            "qm_times":5000
        }
        
      • 输出解析信息
        由于直接生成的指令难以阅读,即使出现了bug也很难快速定位
        因此在将生成的指令输出到标准错误的同时,相应的解析信息也会输出到标准错误,方便了解数据生成的真实情况

        print("ap " + str(id) + " " + name + " " + str(age))
        sys.stderr.write("add_person new:\tap " + str(id) + " " + name + " " + str(age) + "\n")
        

        并且设置了不同指令,在不同异常下的计数器,以查看每种分支的覆盖情况

        rel_counts  = {"new":0, "wrong_rel_duplicated":0, "wrong_unexisted_id":0, "wrong_same_id":0}
        qv_counts   = {"query successful":0, "query wrong(unexisted_id)":0, "query wrong(unexisted_rel)":0}
        sm_counts   = { "success nom":0,
                        "success red":0,
                        "success not":0,
                        "success emj":0,
                        "wrong(unexisted_mid)":0}
        cn_counts   = {"success":0, "wrong(unexisted_pid)":0}
        sei_counts  = {"new":0, "wrong(dup_eid)":0}
        
      • 完成后代码量在 2300+ 行,数据生成的效果如下:

        • 标准输出stdin.txt

          ...略
          qtvs 74 5174
          qci 58 16
          qtvs 145 5407
          qtvs 78 5302
          qci 49 40
          qp 132
          qp 296
          qsv 49
          aem 1545 200 0 880 881
          qm 64
          qp 157
          qrm 33
          qp 230
          qtav 65 5179
          qbs
          qm 6
          qci 41 8
          qrm 85
          qrm 32
          ...略
          
        • 标准错误stderr.txt

          ...略
          query_social_value success:    qsv 65
          query_popularity wrong(unexisted_eid):    qp 286
          query_money wrong(unexisted_eid):    qm 137
          query_popularity wrong(unexisted_eid):    qp 131
          
          #################################### stage eight: send message ####################################
          
          every new:            nom:106    red:114    notice:99    emoji:418
          sm_counts:            nom:82    red:69    notice:51    emj:200
          remaining message:    nom:24    red:45    notice:48    emj:218
          
          {165: 13, 129: 0, 171: 0, 125: 0, 162: 0, 139: 0, 111: 0, 137: 0, 142: 0, 180: 0, 149: 0, 161: 0, 200: 0, 198: 0, 123: 0, 115: 0, 193: 0, 105: 0, 182: 0, 121: 0, 138: 0, 164: 0, 124: 0, 177: 0, 130: 0, 135: 0, 159: 0, 114: 0, 187: 0, 166: 0, 181: 0, 167: 0}
          
          cn_counts:    {'success': 56, 'wrong(unexisted_pid)': 52}
          query_counts: qv_times: 8822, qci_times: 8959, qtvs_times: 11062, qtav_times: 11286, qba_times: 8705, qsp_times: 8738, qbs_times: 4606, qts_times: 4571, qcs_times: 4506, qsv_times: 15785, qrm_times: 15729, qp_times: 15575, qm_times: 15721
          
  • 如何进行测试的

    • 本测评机没有对输出进行正确性判断,而是通过多个同学的程序来进行输出的对拍,通过不同的输出来定位程序的问题

      # generate date
      os.system('python gene.py > stdin.txt 2> adjacent.txt')
      
      # run jar
      os.system(f'java -jar {jar_yrj} < stdin.txt > stdout_yrj.txt')
      os.system(f'java -jar {jar_zyt} < stdin.txt > stdout_zyt.txt')
      os.system(f'java -jar {jar_yyb} < stdin.txt > stdout_yyb.txt')
      
      # judge
      with open(file1,'r') as f1, open(file2,'r') as f2, open(file3,'r') as f3:
          lines1 = f1.readlines()
          lines2 = f2.readlines()
          lines3 = f3.readlines()
          cnt = 1
          hasDiff = False
          for line1, line2, line3 in zip(lines1, lines2, lines3):
              if line1 != line2 or line1 != line3:
                  print("第一个不同的行" + str(cnt) + ":")
                  print("文件-yrj:", line1.strip())
                  print("文件-zyt:", line2.strip())
                  print("文件-yyb:", line3.strip())
                  hasDiff = True
                  break
              cnt += 1
          if hasDiff==False:
              print("输出一样")
      
  • 数据生成器有什么缺陷

    • 由于每个指令是分阶段执行的,因此复杂度不够,有一些情况没有测试到
    • 问题1: mr指令只在ar之后出现,而在添加标签(at),设置标签(att)后不再出现mr,导致qtvs和qtav两种指令的测试不完全,没有暴露出程序在qtvs和qtav两种操作上算法的差距

二、梳理本单元的架构设计,分析自己的图模型构建和维护策略

2.1 图的构建

  • 本单元所构建的社交网路的本质是一个存在多个联通子图的无向图
  • MyNetwork中的属性private final HashMap<Integer,Person> persons = new HashMap<>();使用HashMap容器存储了所有的MyPerson对象,也就是图中的所有节点
  • 然后属性private final int[][] matrix = new int[3100][3100];作为领接矩阵,存储了整张图的信息,以备重建并查集的时候使用

2.2 并查集

  • 针对isCircle()方法,也就是判断两个节点是否处于同一个连通子图中,采用并查集的算法具有良好的时间复杂度

  • 首先创建了Node类,用于表示并查集中存储的每个节点的信息,具有三个属性

    private Node father;
    private int rank;
    private int id;
    

    MyNetwork类中的属性private final HashMap<Integer, Node> nodes = new HashMap<>();存储了所有Node对象
    然后创建了UniFind类,并且采用单例模式,来实现对并查集find()操作和merge()操作的封装

    public class UniFind {
        public static Node find(Node node) {
            /* code */
        }
    
        /*@ requires node1 != node2 @*/
        public static void merge(Node node1, Node node2) {
            // 1. find father
            /* code */
    
            // 2. get rank
            /* code */
    
            // 3. merge
            /* code */
        }
    }
    
  • 并查集的更新和合并

    • 当进行addPerson()操作和isCircle()操作的时候,就会在并查集中加入新的节点,并进行更新
    • 当进行addRelation()操作的时候,就会对并查集进行合并操作
  • 并查集的重构

    • 当进行modifyRelation()的删除操作的时候,由于此时情况较为复杂,所以直接舍弃之前的并查集;然后基于matrix来重构新的并查集

2.3 最短路径算法:FloydBFS

  • 除了重构并查集的时候需要利用到领接矩阵matrix,在查询两个节点之间最短路径的长度queryShortestPath()的时候,也需要依赖领接矩阵matrix
  • Floyd算法
    • 是一种用于解决所有顶点对最短路径问题的算法
    • 通过一个三层循环来更新图中任意两点之间最短路径的长度
      for (int k = 0; k < V; k++) {
          for (int i = 0; i < V; i++) {
              for (int j = 0; j < V; j++) {
                  if (dist[i][k] != INF && dist[k][j] != INF && dist[i][k] + dist[k][j] < dist[i][j]) {
                      dist[i][j] = dist[i][k] + dist[k][j];
                  }
              }
          }
      }
      
    • 但是该算法的缺点是时间复杂度达到了(O(n^3)),容易导致CTLE
  • BFS算法
    • BFS算法通过直接遍历某个点的所有熟人,以及熟人的熟人,最终找到需要寻找的另一个点,来获得两点之间最短路径的长度
    • BFS算法虽然比较基础,但是却拥有良好的时间复杂度,在本次作业中不会导致CTLE

三、分析作业中出现的性能问题及其修复情况,谈谈自己对规格与实现分离的理解

3.1 Floyd算法带来的性能问题

  • 出现在第二次作业中
  • 在一开始,每次add person以及add relation,modifyRelation之后都需要对matrix进行一遍Floyd算法,来更新最短路径的长度,而由于Floyd的时间复杂度过高,导致在第二次作业的强测中所有强测点都CTLE
  • 然后就放弃了使用Floyd算法来动态维护图中任意两点之间的最短路径的情况,而是使用广度优先遍历BFS,来临时计算两点直接最短路径的长度,避免了Floyd算法的时间开销
    // 1. 通过matrix得到最短路的节点数
        if (id1 == id2) {
        return 0;
    } else {
        // 1. Array和HashSet
        ArrayList<Integer> ids = new ArrayList<>();
        HashSet<Integer> idsVisited = new HashSet<>();
        // 2. get acquaintance
        ids.addAll(((MyPerson) myNetwork.getPerson(id1)).getAcquaintanceIdList());
        int cnt = 0;
        int index = 0;
        while (!ids.contains(id2)) {
            // 3. add to HashSet
            idsVisited.addAll(ids);
            int idsSizeOld = ids.size();
            for (;index < idsSizeOld; index++) {
                MyPerson p = (MyPerson) myNetwork.getPerson(ids.get(index));
                for (int j : p.getAcquaintanceIdList()) {
                    if (!idsVisited.contains(j)) {
                        ids.add(j);
                    }
                }
            }
            // 4. cnt++;
            cnt++;
        }
        return cnt;
    }
    

3.2 queryVauleSum()带来的性能问题

  • 出现在第二次作业中
  • 在一开始,每次进行queryValueSum()都对Tag中所有的Person进行双层遍历,时间复杂度为(O(n^2)),导致CTLE
  • 但是,实际上可以在Tag中用一个属性private int valueSum = 0;来存储valueSum的值,那在什么时候对这个值进行更新呢?
    • 当Tag中添加了新的节点,那么就要对Tag中所有的人进行一次遍历,来查看新增加的节点是否和Tag中的其他节点相连,然后更新valueSum
    • 当Tag中删除了一个节点,也需要对Tag中所有的人进行一次遍历,来查看删除的节点是否之前和Tag中的其他节点相连,然后更新valueSum
    • 当NetWork中进行了addRelation()操作
      • 由于对任意Tag,都有可能需要更新valueSum,因此首先想到的方式是当涉及到关系的修改,就要作废所有的valueSum,然后重新计算
      • 但是这样明显会带来极高的时间复杂度,因此我们考虑在MyNetwork使用private final HashMap<Integer, ArrayList<Tag>> personTags = new HashMap<>();来保存所有节点拥有这个节点的Tag的信息
      • 这样,当增加关系,修改关系,删除关系的时候,只需要在personsTag中找到同时包含id1和id2的Tag,然后更新其valueSum就可以
        if (personTags.containsKey(id1)) {
          for (Tag tag1 : personTags.get(id1)) {
              if (personTags.containsKey(id2)) {
                  if (personTags.get(id2).contains(tag1)) {
                      MyTag myTag = (MyTag) tag1;
                      myTag.addValueSum(2 * value);
                  }
              }
          }
        }
        

3.3 ModifyRelation重构并查集带来的性能问题

  • 出现在第三次作业中
  • 由于每次ModifyRelation之后到需要重构并查集,导致时间复杂度过高而CTLE,因此需要改进这个方法
  • 实际上,当两个节点之间的关系被删除之后,如果MyNetworktripleSum,也就是三元环的数量减少的话,就说明了此时所删除的关系是某个三元环三条边中的一条,因此这个时候,这两个被删除关系的节点一定还在同一个连通子图中,因为他们有一个共同的第三个点,所以此时连通子图数量不变,也就不需要重构并查集
  • 只有当删除关系后,tripleSum没有变化,才有可能新增了连通子图,这个时候再基于领接矩阵matrix来重构并查集
    final int oldTripleSum = tripleSum;
    tripleSum -= p1.dupAcquaintance(p2);
    if (tripleSum == oldTripleSum) {
        /* code: rebuild unifind */
    }
    

3.4 对规格与实现分离的理解

  • 规格规定了什么?
    • JML实际上只是对类中的各个方法进行了功能上的规范化约束
    • 比如增加操作之后,必须保证容器长度只增加1,并且所有先前存在的元素都要继续存在,有时还会约束这些元素之间的相对位置不能发生变化
    • 规格化的目的是为了消除不同程序员在实现不同方法上出现功能上的差异,便于程序的合作开发和维护
  • 规格没有规定什么?
    • 但是JML没有规定实现各个功能所使用的算法
    • 而对于同一个功能,尤其是设计图的问题,存在许多不同的算法可以达到一样的目的,但是他们的时间复杂度和空间复杂度是不一样的,所侧重的使用场景也不同
    • 比如我一开始采用的Floyd算法,虽然适用于查询图中任意两点之间的最短路径,但是他侧重于在稠密图中使用这个算法,可以达到良好的效果。而在稀疏图中的效率反而不如直接使用BFS来查询两点之间的最短路径
  • 实现需要考虑什么?
    • 因此,在各个功能的实现上,我们必须考虑采用有更低时间复杂度,更少空间开销的算法,在实现功能的前提下,来提高程序的性能
  • 规格和实现分离
    • 因此,规格和实现分离,就是意味着程序在功能设计上的两个方面
    • 从规格的层面,我们只关系一个方法的输入和输出,以及带来的副作用,而不关心性能
    • 从实现的层面,完成规格所要求的只是实现的基础,而在算法的有优化上可能需要花更多的考虑
    • 规格和实现分离的优点:
      提高了程序的可维护性,可扩展性,代码的重用性,利于协作开发

四、本单元中同学们实现了Junit测试方法,总结分析如何利用规格信息来更好的设计实现Junit测试,以及Junit测试检验代码实现与规格的一致性的效果

  • 利用规格信息来更好的设计实现Junit测试
    • 首先,关注JML中的不变式,前置条件,后置条件
    • 然后根据这些条件编写测试数据
    • sendMessage()方法为例:
      /*@ public normal_behavior
      @ requires containsMessage(id) && getMessage(id).getType() == 0 &&
      @          getMessage(id).getPerson1().isLinked(getMessage(id).getPerson2()) &&
      @          getMessage(id).getPerson1() != getMessage(id).getPerson2();
      @ assignable persons[*], messages, emojiHeatList[*];
      @ assignable getMessage(id).getPerson1().socialValue, getMessage(id).getPerson1().money;
      @ assignable getMessage(id).getPerson2().messages, getMessage(id).getPerson2().socialValue, getMessage(id).getPerson2().money;
      @ ensures !containsMessage(id);
      @ ensures \old(getMessage(id)).getPerson1().getSocialValue() ==
      @         \old(getMessage(id).getPerson1().getSocialValue()) + \old(getMessage(id)).getSocialValue() &&
      @         \old(getMessage(id)).getPerson2().getSocialValue() ==
      @         \old(getMessage(id).getPerson2().getSocialValue()) + \old(getMessage(id)).getSocialValue();
      @ ensures (\old(getMessage(id)) instanceof RedEnvelopeMessage) ==>
      @         (\old(getMessage(id)).getPerson1().getMoney() ==
      @         \old(getMessage(id).getPerson1().getMoney()) - ((RedEnvelopeMessage)\old(getMessage(id))).getMoney() &&
      @         \old(getMessage(id)).getPerson2().getMoney() ==
      @         \old(getMessage(id).getPerson2().getMoney()) + ((RedEnvelopeMessage)\old(getMessage(id))).getMoney());
      @ ensures (!(\old(getMessage(id)) instanceof RedEnvelopeMessage)) ==> (\not_assigned(persons[*].money));
      @ ensures (\old(getMessage(id)) instanceof EmojiMessage) ==>
      @         (\exists int i; 0 <= i && i < emojiIdList.length && emojiIdList[i] == ((EmojiMessage)\old(getMessage(id))).getEmojiId();
      @         emojiHeatList[i] == \old(emojiHeatList[i]) + 1);
      @ ensures (!(\old(getMessage(id)) instanceof EmojiMessage)) ==> \not_assigned(emojiHeatList);
      @ ensures (\forall int i; 0 <= i && i < \old(getMessage(id).getPerson2().getMessages().size());
      @          \old(getMessage(id)).getPerson2().getMessages().get(i+1) == \old(getMessage(id).getPerson2().getMessages().get(i)));
      @ ensures \old(getMessage(id)).getPerson2().getMessages().get(0).equals(\old(getMessage(id)));
      @ ensures \old(getMessage(id)).getPerson2().getMessages().size() == \old(getMessage(id).getPerson2().getMessages().size()) + 1;
      
      • 针对type=0Message,前置条件要求person1和person2要是熟人,并且不能是同一个人
        然后后置条件要求不再包含这条Message,并且person1的socialValue在方法执行后要增加Message的socialValue值
      • 因此我们可以利用assert断言来判断在sendMessage(sampleId)方法执行后,网络中是否还含有这条Message
        也就是assertTrue(!myNetwork.containsMessage(sampleId))
      • 然后用assert断言判断person1的socialValue值是否增加了一个Message的socialValue值
        assertEquals(myNetwork.getPerson(personid1),
            theOldSocialValue + message.getSocialValue());
        
    • 对于其他的ensures条件我们也采取同样的方法,确保在JUnit中对所有的ensures方法都能覆盖到
  • Junit测试检验代码实现与规格的一致性的效果
    • 效果就是可以方便的确认所涉及的方法是否符合JML规范

五、本单元学习体会

  • JML将设计与实现分离
    • 在本单元中,我们通过JML,可以做到先完成程序设计,再实现程序代码
    • 虽然作业中的JML都是课程组提供的,但是在以后大型的程序设计中,我们也可以借鉴这种方法,这样就可以很快的完成程序的整体设计,从而较早得关注到一些代码耦合,代码结构设计方面的细节问题,而不是在边写代码的时候边调整
    • 这样也有利于协作开发,一份拟写好JML的程序,可以交由不同的程序员来分工实现,加快开发速度,缩短工程周期;同时也方便项目进行迭代,修改,模块复用等
  • 基于JML我们可以在设计好之后集中更多精力在算法选择上
    • 既然已经写好JML,我们在写代码的时候就不需要花那么多的精力去思考整体框架
    • 而是在JML的约束下,结合具体应用场景,选择性能更好的算法;这让我们在每个方法的算法选择上可以更加有思考,甚至可以选择多种算法,分别实现后比较哪一种算法比较有优势
    • 有利于项目的在性能上的开发
  • JML和测评机
    • 同时,基于JML,我们可以更全面得搭建测评机
    • JML中已经规定好了每个方法的异常行为,因此在数据生成的时候,我们可以根据这些异常行为来定向生成需要的数据,以此触发各种分支,这样更加全面,在搭建测评机的时候思路也更清晰
...全文
101 回复 打赏 收藏 转发到动态 举报
写回复
用AI写文章
回复
切换为时间正序
请发表友善的回复…
发表回复

301

社区成员

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

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