BUAA_OO第三单元总结

七仔^_^ 学生 2024-05-17 14:15:54

OO第三单元总结

代码架构

UML图

img

这个是最终作业的UML图,实现了一个社交关系网。

架构分析

除了官方包中提供的Network、Person、Tag、Exception、Message等类,我还新建了Counter计数类,Operations操作类、Map并查集类。下面我将对每次作业的功能实现进行分析。
图构建过程:Network类中的persons容器作为图中点的集合,Person类中的acquaintance容器作为连通关系的集合。在添加人和关系的时候动态维护点和关系的容器即可实现图构建。后续的作业描述其实就是图维护过程,不过,维护的是图中存在的信息,而不是图本身

第一次作业

  • 主要目标:

    • 关系网中三角关系(tripleSum)
    • 最大连通图个数(blockSum)
    • 两人在关系网中是否连通(isCircle)
  • 实现方式:

    • 三角关系:添加关系时遍历二人的熟人,寻找是否有相同的熟人,若有,则tripleSum++。

    • 最大连通图:通过动态维护并查集实现。下面给出并查集的简单架构实现。

    • public class Map {
        private HashMap<Integer, Integer> pre; // id1 -> id2(id2 is root)
        private HashMap<Integer, Integer> rank; // id1 -> rank
      
        public Map() {
            this.pre = new HashMap<>();
            this.rank = new HashMap<>();
            this.blockSum = 0;
            this.tripleSum = 0;
        }
      
        public void add(int id) { // 添加新人后的并查集更新方法
            if (!pre.containsKey(id)) {
                pre.put(id, id);
                rank.put(id, 0);
                Counter.getInstance().addBlockSum();
            }
        }
      
        public void delete(int id1, int id2,
                          HashMap<Integer, HashMap<Integer, Person>> graph) { // 删除id1,id2的关系后,处理并查集的更新过程。
            Set<Integer> visited = new HashSet<>(); // 存储熟人
            pre.put(id2, null);
            bfs(id1, visited, graph);
            for (Integer integer : visited) {
                pre.put(integer, id1);
            } // 将id1设置为他所有熟人的根节点
            if (!pre.get(id1).equals(pre.get(id2))) { // id2此时与id1不连通,重构并查集
              // ... 
              // ... blockSum++;
            }
        }
      
        public int find(int id) { // 寻找id的根节点,顺便压缩路径
            if (pre.get(id) == id) {
                return id;
            } else {
                int head = id;
                while (pre.get(head) != head) {
                    head = pre.get(head);
                }
                int temp = id;
                while (temp != head) {
                    int temp1 = pre.get(temp);
                    pre.put(temp, head);
                    temp = temp1;
                }
                return head;
            }
        }
      
        public void join(int id1, int id2) { // 添加id1和id2的关系后,更新并查集的过程
            int headOf1 = this.find(id1);
            int headOf2 = this.find(id2);
            if (headOf1 != headOf2) { // 二人根节点不同,不相连通
                pre.put(headOf2, headOf1);
                Counter.getInstance().subBlockSum();
            }
        }
      
        private void bfs(int node, Set<Integer> visited,
                        HashMap<Integer, HashMap<Integer, Person>> graph) { // bfs不采用递归,速度比dfs更快
            visited.add(node);
            ArrayDeque<Integer> queue = new ArrayDeque<>();
            queue.add(node);
            while (!queue.isEmpty()) {
                int v = queue.pollFirst();
                HashMap<Integer, Person> acquaintance = graph.get(v);
                for (Integer id : acquaintance.keySet()) {
                    if (!visited.contains(id)) {
                        visited.add(id);
                        queue.add(id);
                    }
                }
            }
        }
      }
      
    • 是否连通:通过并查集实现,通过find函数查询根节点,若相等,则二者连通,若不等,反之。

      第二次作业

  • 主要目标

    • 求解bestAcquaintance,寻找最好关系。
    • 新增标签(Tag),求某人的tag下相互认识的人的value的和(queryTagValueSum)
    • 求某人tag下相互认识的人互为最好关系(在各自熟人中value最大)的个数(queryCoupleSum)
  • 实现方式

    • queryBestAcquaintance:身边许多同学都是采用treeMap求解的,时间复杂度是每次新增关系时都要遍历整个树,故为O(n*logn),我觉得这样可能有点复杂且不必要了,于是采用的是动态维护,在Person类中维护bestId和bestValue,并设置一个脏位,负责一些难以维护———减小了最大关系的value值,不得不重新遍历的情况。这个架构可以在较少出现上述难以维护情况时保持O(1)的复杂度。出现上述情况,也仅仅在出现queryBestAcquaintance指令是才会遍历一遍容器Acquaintance。所以实际测试样例中基本不可能有如此复杂刁钻的样例可能攻击到此方法。

    • queryTagValueSum:本方法最初我用的直接求解法,时间复杂度为o($n^2$),无法通过强测,于是改成动态维护,将复杂度减小到o(n),才得以通过。下面将简述我的动态维护的策略。

      • 综合考虑所有情况,可知valueSum的值只会在特定指令时才会发生改变——addRelation, addPersonToTag, modifyRelation, delPersonFromTag。

      • addRelation:添加关系时,可能会增加tag中的熟人关系,会改变value和。添加上新增的关系value即可。

      • addPersonToTag:在tag中添加人时,此人可能与原来的人存在关系,所以需要加上这些新增的关系value的和。

      • modifyRelation:第一种情况是修改关系value值但是不删除,那么只需要判断两人是否同时存在于某tag中,如果存在,则减少tag中的valueSum相应值即可。第二种情况是删除关系,若二人共同存在于某个tag中,则在tag中减去2*value值即可,若有一人是tag的拥有者,另一人是tag中的人,则需要减去后者于tag中所有的关系value值的和。

      • 上面的理论叙述可能比较复杂晦涩,我将给出一些代码供参考了解。

        private HashMap<Integer, HashMap<Integer, HashSet<Integer>>> tagFound; // 这个是我自建的索引表,可以从人出发,寻找到他包含在那些tag中。逻辑为:人->熟人->该人存在于熟人的tags集合
        // 这个动态维护过程与图维护并存,不给出详细介绍
        public void test(int id1, int id2, int value) {
          for (Integer personId : this.tagFound.get(id1).keySet()) {
              if (this.tagFound.get(id2).containsKey(personId)) { //二者有共同的熟人
                  for (Integer tagId : this.tagFound.get(id1).get(personId)) {
                      ((MyTag) this.persons.get(personId).getTag(tagId)).
                              modifyValueSum(id1, id2, value);
                  }
              }
          }
        } // 该函数用于addRelation和modifyRelation第一种情况,仅包含修改value  值但不包含删除tag中的人的情况。
        
        // in Class Tag
        public /*@ safe @*/ void delPerson(/*@ non_null @*/Person person) {
            if (this.persons.containsKey(person.getId())) {
                this.persons.remove(person.getId());
                this.ageSum -= person.getAge();
                this.ageSquareSum -= person.getAge() * person.getAge();
                for (Integer personId : persons.keySet()) {
                    if (persons.get(personId).isLinked(person)) {
                        this.valueSum -= 2 * persons.get(personId).queryValue(person);
                    }
                }
            }
        } // 每次删除都要遍历一次persons,删除所有相关的value值。
        
      • queryCoupleSum: 这一块我没有进行深度的动态维护,仅仅设置了一个脏位,仅维护了这个脏位。

        private boolean coupleSumValid;
        // 接下来是在addRelation和modifyRelation中修改脏位的过程
        public void addRelation(int id1, int id2, int value)
                throws MyEqualRelationException, MyPersonIdNotFoundException {
            // 省略 ......
            boolean flag1 = ((MyPerson) persons.get(id1)).addAcquaintance(persons.get(id2), value);
            boolean flag2 = ((MyPerson) persons.get(id2)).addAcquaintance(persons.get(id1), value);
            this.coupleSumValid = (flag1 || flag2) ? false : this.coupleSumValid;
            test(id1, id2, value);
            map.join(id1, id2);
        }
        
        public void modifyRelation(int id1, int id2, int value) throws MyEqualPersonIdException,
                MyPersonIdNotFoundException, MyRelationNotFoundException {
            // ...
            boolean f1 = ((MyPerson) (persons.get(id1))).modifyValue(id2, value);
            boolean f2 = ((MyPerson) (persons.get(id2))).modifyValue(id1, value);
            this.coupleSumValid = (f1 || f2) ? false : this.coupleSumValid;
            // ...
        }
        // 接下来就是求解coupleSum的方法
        public int queryCoupleSum() {
            if (this.coupleSumValid) {
                return this.coupleCnt;
            }
            this.coupleCnt = 0;
            for (Integer id0 : this.persons.keySet()) {
                if (!((MyPerson) getPerson(id0)).getIsValid()) {
                    ((MyPerson) getPerson(id0)).adjustTheBest();
                }
                Integer bestAqOfId0 = ((MyPerson) getPerson(id0)).getBestId();
                if (bestAqOfId0 != null && !((MyPerson) getPerson(bestAqOfId0)).getIsValid()) {
                    ((MyPerson) getPerson(bestAqOfId0)).adjustTheBest();
                }
                Integer bestAqOfId1 = null;
                if (bestAqOfId0 != null) {
                    bestAqOfId1 = ((MyPerson) getPerson(bestAqOfId0)).getBestId();
                }
                if (bestAqOfId1 != null && bestAqOfId1.equals(id0)) {
                    this.coupleCnt++;
                }
            }
            return this.coupleCnt /= 2;
        }
        

        第三次作业

  • 主要目标

    • 新增Message类,实现普通消息、红包消息、提醒消息、表情消息
  • 实现方式

    • 本次作业实现方式不难,按照jml规格书写即可。

      各种测试方法的理解

      黑盒测试

      黑盒测试,它是通过测试来检测每个功能是否都能正常使用。在测试中,把程序看作一个不能打开的黑盒子,在完全不考虑程序内部结构和内部特性的情况下,在程序接口进行测试,它只检查程序功能是否按照需求规格说明书的规定正常使用,程序是否能适当地接收输入数据而产生正确的输出信息。黑盒测试着眼于程序外部结构,不考虑内部逻辑结构,主要针对软件界面和软件功能进行测试。在本单元中,即是中测、强测以及评测机测试,综合评测程序是否存在bug。

      白盒测试

      白盒测试又称结构测试、透明盒测试、逻辑驱动测试或基于代码的测试。白盒测试是一种测试用例设计方法,盒子指的是被测试的软件,白盒指的是盒子是可视的,即清楚盒子内部的东西以及里面是如何运作的。“白盒”法全面了解程序内部逻辑结构、对所有逻辑路径进行测试。“白盒”法是穷举路径测试。在使用这一方案时,测试者必须检查程序的内部结构,从检查程序的逻辑着手,得出测试数据。贯穿程序的独立路径数是天文数字。

      单元测试

      单元测试是软件开发中最低级别的测试活动,其中测试人员针对代码中的最小可测试单元(如一个方法或函数)进行验证。在本单元中即为Junit针对每个方法的正确性的测试,例如每次作业中对某个方法的test函数编写。

      功能测试

      功能测试是验证系统是否满足规定需求(包括功能、性能、可靠性等)的测试。

      集成测试

      集成测试是在单元测试之后进行的,用于验证不同的软件模块在组合起来后是否能够正常工作。

      压力测试

      压力测试是评估系统在不同负载下性能表现的测试。

      回归测试

      回归测试是确保在修改或添加新功能后,软件的原有功能没有受到不利影响的测试。

      测试数据构造策略

      本次作业的测试数据主要是针对代码的运行时间进行攻击,因为代码的架构为图结构,不同的算法的时间复杂度相差较大。例如针对第二次作业的qtvs指令进行压力测试。

Junit测试

参数化测试

本次Junit我采用了实验课上学习的参数化测试方法,下面将简要介绍一下参数化测试的写法。

import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized; 
// 三个导入包
@RunWith(Parameterized.class)
public class MyNetworkTest {
  private Network network; // 代表执行方法后的network
  private Network oldNetwork; // 代表执行方法前的network
  // 相当于深克隆,前后对比,用于pure的判断
  public MyNetworkTest(Network network, Network oldNetwork, int limit) { // 构造方法
        this.network = network;
        this.oldNetwork = oldNetwork;
    }

    @Test
  public /*@ pure @*/ void queryCoupleSum() throws AcquaintanceNotFoundException, PersonIdNotFoundException {
    // 这个是被测试的方法
      Network network = this.network;
      Network oldNetwork = this.oldNetwork;
      int coupleSum = network.queryCoupleSum();
      Person[] personsInNetwork = ((MyNetwork) network).getPersons();
      Person[] personsInOldNetwork = ((MyNetwork) oldNetwork).getPersons();
      boolean flag = true;
      // code is here ...
      // two for circle to check if network changed
      flag = flag && personsInNetwork.length == personsInOldNetwork.length;
      assertTrue(flag); // 前后network对比,测试pure
      int sum = 0;
      HashSet<Integer> hasAcquaintance = new HashSet<>();
      // code is here ...
      // calculate the value by JML, then compare with the method we made.
      assertEquals(coupleSum, sum / 2);
  }

  @Parameterized.Parameters
  public static Collection prepareData() throws EqualPersonIdException,
          PersonIdNotFoundException, EqualRelationException {
            // 准备测试样例
      long seed = System.currentTimeMillis();
      Random random = new Random(seed);
      int testNum = 100;//测试次数,可根据需求调整
      //该二维数组的类型必须是Object类型的
      //该二维数组中的第一维代表有多少组测试数据,有多少次测试就会创造多少个PathTest对象
      //该二维数组的第二维代表MyNetworkTest构造方法中的参数,位置一一对应
      Object[][] object = new Object[testNum][];
      for (int i = 0; i < 50; i++) {
          List<Network> networkList = generateNetwork(100);
          Network network = networkList.get(0);
          Network oldNetwork = networkList.get(1);
          object[i] = new Object[]{network, oldNetwork};
      }
      for (int i = 50; i < 100; i++) {
          List<Network> networkList = generateNetwork(10);
          Network network = networkList.get(0);
          Network oldNetwork = networkList.get(1);
          object[i] = new Object[]{network, oldNetwork};
      }
      return Arrays.asList(object);
  } // 将generate产生的信息进行整合,参数化处理

  public static List<Network> generateNetwork(int percent) throws EqualPersonIdException,
            PersonIdNotFoundException, EqualRelationException {
      long seed = System.currentTimeMillis();
      Random random = new Random(seed);
      Network network1 = new MyNetwork();
      Network network2 = new MyNetwork();
      List<Network> networkList = new ArrayList<>();
      for (int i = 0; i < personNum; i++) {
          int id = i;
          String name = generateRandomString();
          int age = random.nextInt(upperAgeBound);
          Person person1 = new MyPerson(id, name, age);
          Person person2 = new MyPerson(id, name, age);
          network1.addPerson(person1);
          network2.addPerson(person2);
          // code here ...
          // 生成两个相同person,相当于深克隆
      }
      for (int i = 0; i < personNum; i++) {
          for (int j = i + 1; j < personNum; j++) {
              int truth = random.nextInt(percent);
              if (truth == 1) {
                  int value = random.nextInt(10 - 1) + 1;
                  network1.addRelation(i, j, value);
                  network2.addRelation(i, j, value);
              }
          }
      }// code here ...  添加network中的关系
      networkList.add(network1);
      networkList.add(network2);
      return networkList;
  } // 这个方法目的是产生测试对象的基本信息。

}

关于规格设计与实现的分离

规格设计是给出方法的基本设计目标,包含一些边界情况的细节考虑。
实现则是采用自己的方法进行代码编写,目的是实现规格给出的目标,并且考虑到其中的边界和细节问题。

一般情况实现都不会严格按照规格的思路编写,因为规格给的是最基础的方法,具体实现还需要考虑到性能上的需求,所以会选择最合适的方式来编写代码。

心得体会

本单元主要目标就是训练我们读写jml语言的能力,并且在作业任务中带我们回顾了数据结构中的图算法,针对性训练了动态维护,脏位设置相关的性能问题。
另外,我们还学习了Junit测试的编写,学习了参数化测试的写法和测试的针对性设计。

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

301

社区成员

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

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