面向对象程序设计第三单元作业总结

面向对象程序设计第三单元作业总结

本单元的任务是学习JML的相关知识。在三次作业中,我们根据JML规格实现了一个社交关系系统,深刻体会到了JML规格语言的严谨性,掌握了阅读较为复杂的JML规格的能力。

一、利用JML规格进行程序自测

我在本单元的测试基本通过Junit实现。由于单元作业中,实现的方法数量较多,且多数方法为较为简单的添加元素,查询元素操作,仅有少数方法为较为复杂的最短路径,最短生成树查询,因此我的测试分为两部分:对于操作的正确性测试以及对于复杂操作的性能测试。

  • 正确性测试
    对于简单指令的正确性测试,以public void addToGroup(int id1, int id2)这个方法为例。通过JML规格,我们可以知道这个指令有两个normal_behavior,并且会抛出三种异常。对于两种normal_behavior测试代码如下:
    MyNetwork network = new MyNetwork();
    MyGroup group = new MyGroup(1);
    network.addGroup(group);
    assertEquals(0, network.getGroup(1).getSize());
    for (int i = 0; i < 1200; i++) {
        if(i <= 1111) {
            assertEquals(i, network.getGroup(1).getSize());
        } else {
            assertEquals(1111, network.getGroup(1).getSize());
        }
        String s = String.valueOf(i);
        MyPerson p1 = new MyPerson(i, s, 20);
        network.addPerson(p1);
        assertEquals(false, network.getGroup(1).hasPerson(getPerson(i)));
        network.addToGroup(i, 1);
        if (i < 1111) {
            assertEquals(i + 1, network.getGroup(1).getSize());
            assertEquals(true, network.getGroup(1).hasPerson(getPerson(i)));
        } else {
            assertEquals(1111, network.getGroup(1).getSize());
            assertEquals(false, network.getGroup(1).hasPerson(getPerson(i)));
        }
    }

由JML描述可知,当小组id和人的id均存在且人不在组中时,方法为正常行为,此时当组内人数少于1111时,对该组会产生影响,而人数多于1111时则不会,因此我在设计测试时覆盖了两种情况,并利用assertEquals对添加人前后组的状态进行判断,以确保方法实现了正确的功能。
对于异常的测试重点在于不同异常状态能否抛出对应异常以及异常计数器是否工作正常
在测试时,我通过设计不同异常情况,让测试单元抛出异常的方式来判断以上两点。具体测试代码如下:

    MyNetwork network = new MyNetwork();
    MyGroup group = new MyGroup(1);
    network.addGroup(group);
    assertEquals(0, network.getGroup(1).getSize());
    for (int i = 0; i < 10; i++) {
        String s = String.valueOf(i);
        MyPerson p1 = new MyPerson(i, s, 20);
        network.addToGroup(i, 1); //PersonIdNotFoundException
        network.addPerson(p1);
        network.addToGroup(i, 2); //GroupIdNotFoundException
        network.addToGroup(i, 1); //normal_behavior
        network.addToGroup(i, 1); //EqualPersonIdException
    }

由于我的所有异常的计数方式是一样的,因此只需要对一种单参数异常和一种两个参数的异常进行检测就可以验证计数功能是否有效。通过这个例子可以看出,在测试一个指令是否正确时,难免会使用到一些基本指令,而这些指令的正确性也可以在测试中判断。

  • 性能测试
    性能测试重点在于几条时间复杂度较高的指令。如第一次作业的isCircle(),queryBlockSum(),第二次作业的queryGroupValueSum(),queryLeastConnection()以及第三次作业的sendIndirectMessage()。对于复杂指令的性能测试就是构造满足测试上限数量的数据,通过指令测试时的耗时判断是否可能出现超时的问题。
    例如第二次作业中复杂指令的性能测试:
    MyNetwork network = new MyNetwork();
    MyGroup group = new MyGroup(1);
    network.addGroup(group);
    for (int i = 0; i < 1200; i++) {
        String s = String.valueOf(i);
        MyPerson p1 = new MyPerson(i, s, i % 101);
        network.addPerson(p1);
        network.addToGroup(i, 1);
    }
    for (int i = 0; i < 1200; i++) {
        for (int j = i + 1; j < i + 4 && j < 1111; j++) {
            network.addRelation(i, j, j % 1000);
            System.out.println(network.queryGroupValueSum(1));
            System.out.println(network.queryLeastConnection(j));
        }
        System.out.println(network.queryLeastConnection(i));
    }

性能测试是在正确性测试满足后展开的,因此这些测试指令主要在于检测复杂指令在每次数据发生改变后重新计算的时间复杂度。


二、架构设计与图的建立和维护

1.架构设计

本次作业的整体架构如下(省略了类内部的属性和方法):
image
从图中可以看出,在JML规格要求的8个类以外,我还增加了一个MyNetworkMethod类用于处理图的建立和维护问题。

2.图的建立和维护

在本次作业中,与图有关的指令为isCircle(),queryBlockSum(),queryLeastConnection(),sendIndirectMessage()这四个指令分别对应图的连通性图的个数最小生成树以及最短路径

  • 对于前两个指令,我使用并查集的思维,在初始化Person时将其顶级父结点设置为自身,添加关系时维护两个Person的顶级父结点,将两者顶级父结点中编号较小的作为更新后的顶级父结点,并遍历所有NetWorkPerson,将所有具有编号较大的顶级父结点的人的顶级父结点更新。这样就可以实现isCircle()的O(1)查询。同时在NetWork中维护一个顶级父结点的容器,当加入Person时,将其加入顶级父结点容器;当添加关系时,将编号较大的顶级父结点从容器中删除。这样这个容器的顶级父结点个数就代表了图的个数,实现了queryBlockSum()的O(1)查询。
  • 有了前两条指令的基础,我使用动态维护的方法解决最小生成树问题。对于边的表示,我使用一个HashMap,这个图的key表示当前结点的id,value表示这个点的直接父结点id,将无向图变为了一个具有特定方向的有向图。在每次添加关系时,维护所有人的最小生成树边的权重和,以及所在最小生成树的边。我首先将添加关系分为两类,一类是改变两个人的连通性(即两人原来不在一棵树下),另一类是不改变两个人的连通性(即两人已经处于同一棵树下)。
    • 对于第一类指令,由于这条边是两点连通的唯一路径,因此一定处在最小生成树中,所以可以直接更新所有位于同一棵树下的人的权重和。设两个人分别为A和B,在添加关系前两个人所在最小生成树的权重值分别为valueA和valueB,两人关系值为value。则所有原来与A在同一棵树下以及所有原来与B在同一棵树下的人,其最小生成树权重值更新为valueA + valueB + value。对于最小生成树边的维护见下图:
      image
      对于两个顶级父结点或一个顶级父结点和一个普通结点之间添加关系时,只需要让顶级父结点指向另一个点,其余点的边不要变化。而对于两个非顶级父结点之间添加关系时,则需要改变一棵树内的某些边的方向。如图中在26之间增加了一个关系,由于2的顶级父结点(1)小于6的(3),因此改变6原来所在最小生成树的边。改变方式为,将从该节点到顶级父结点的所有边key与value对调,即改变边的方向。
    • 而对于第二类关系,则需要先判断新添加关系的权值是否小于最小生成树中权值最大的边,如果大于最大权值边则不需要替换,否则有可能需要替换。替换时需要满足两个条件:一是替换后不改变树节点的连通性,二是在满足一的前提下替换权值最大的边。具体替换思路如图:
      image
      如图,假设在结点5和7之间增加了一个权值为3的关系。由于3小于最小生成树中的边,因此需要判断是否需替换。从图中我们可以看出,在添加关系后,能够删除的边存在于由3,4,5,6,7构成的环中。因此需要先寻找图中的环。寻找环的方法是分别从结点5和7开始,遍历其父结点直至顶级父结点。从遍历开始,到两个遍历过程访问的第一个相同结点为止,经历的所有点在一个环内。如5的遍历过程为5,3,4,6,2,1,7的遍历过程为7,6,在一个环内的结点为5,3,4,6以及7,6。在7的遍历过程中,可以记录最大边,此时再重新遍历一次5,3,4,6的边,更新最大边,就可以得到环中需要被替换的边。在找到需要替换的边并删除后,需要将部分边的方向改变。例如图中4,6边被删除,需要修改的边为3,45,3。将两个边方向改变后,最小生成树维护完成。假设新增权值为valueNew,替换边的权值为valueMax,原来最小生成树的权值为value,则此时最小生成树权值更新为value + valueNew - valueMax
    • 利用动态维护的方式能够实现O(n)维护,以及O(1)查询。
  • sendIndirectMessage()需要寻找图中的最短路径。由于在社交系统中,关系值非负,因此我使用堆优化的Dijkstra算法实现了O((m+n) log n)的复杂度。

三、性能问题和修复情况

1.第一次作业

第一次作业中我出现性能问题的指令是queryBlockSum()。使用并查集方法后,isCircle()指令的复杂度为O(1),因此我就利用isCircle()进行复杂度为O(n^2)的遍历来获得图的个数,导致超时。修改思路就是利用父结点数量来判断图的个数 ,实现了O(1)查询。

2.第二、三次作业

后两次作业我将所有时间复杂度为O(n^2)的方法均进行了优化,因此没有出现性能问题。

四、Network拓展

假设出现了几种不同的Person
Advertiser:持续向外发送产品广告
Producer:产品生产商,通过Advertiser来销售产品
Customer:消费者,会关注广告并选择和自己偏好匹配的产品来购买。所谓购买,就是直接通过Advertiser给相应Producer发一个购买消息
Person:吃瓜群众,不发广告,不买东西,不卖东西

1.扩展思路

Advertiser:广告商除了发送广告外,还可以调查消费者的偏好,调整发送广告内容。
Producer:产品生产商除了生产产品外,还可以调查产品销售情况,调整产品或广告商。
Customer:消费者增加年龄,性别等属性。
Person:吃瓜群众同样可以接受广告,如果接受广告达到一定数量,则产生产品偏好,变为消费者。
Product: 产品必须由生产商生产,产品在某一时刻只能由一位广告商发送广告。一个人可以有多个产品。

2.接口规格

  • Network中具有的属性:
    /*@ public instance model non_null Person[] people;
      @ public instance model non_null Advertiser[] advertisers;
      @ public instance model non_null Customer[] customers;
      @ public instance model non_null Producer[] producers;
      @ public instance model non_null Product[] products;
      @*/
  • sendAd()方法规格:
    /*@ public normal_behavior
      @ requires hasProduct(product) && 
      @		(\exists int i; 0 <= i && i < advertisers.length; advertisers[i].hasProduct(product)) && 
      @		!(\exists int i, j; 0 <= i && 0 <= j && i < advertisers.length && j < advertisers.length && j != i; advertisers[i].hasProduct(product) && advertisers[j].hasProduct(product)); 
      @ assignable people[*].receivedAd, customers[*].receivedAd
      @ ensures (\forall int i; 0 <= i && i < people.length; 
      @ 	(people[i].hasAd[product] ==> 
      @		((people[i].reveivedAd.length == \old(people[i].reveivedAd.length)) && 
      @         (\forall int j; 0 <= i && j < \old(people[i].reveivedAd.length);
      @         (\exists int k; 0 <= k && k < people[i].reveivedAd.length; people[i].reveivedAd[k].equals(\old(people[i].reveivedAd[j])))))) &&
      @		(!people[i].hasAd[product] ==>
      @		((people[i].reveivedAd.length == \old(people[i].reveivedAd.length)+1) && 
      @		(\forall int j; 0 <= j && j < \old(people[i].reveivedAd.length);
      @         (\exists int k; 0 <= k && k < people[i].reveivedAd.length; people[i].reveivedAd[k].equals(\old(people[i].reveivedAd[j])))) && 
      @		(\exists int j; 0 <= j && j < people[i].reveivedAd.length; people[i].reveivedAd[j].getProduct == product)));
      @ ensures (\forall int i; 0 <= i && i < customers.length; 
      @ 	(customers[i].hasAd[product] ==> 
      @		((customers[i].reveivedAd.length == \old(customers[i].reveivedAd.length)) && 
      @         (\forall int j; 0 <= i && j < \old(customers[i].reveivedAd.length);
      @         (\exists int k; 0 <= k && k < customers[i].reveivedAd.length; customers[i].reveivedAd[k].equals(\old(customers[i].reveivedAd[j])))))) &&
      @		(!customers[i].hasAd[product] ==>
      @		((customers[i].reveivedAd.length == \old(customers[i].reveivedAd.length)+1) && 
      @		(\forall int j; 0 <= j && j < \old(customers[i].reveivedAd.length);
      @         (\exists int k; 0 <= k && k < customers[i].reveivedAd.length; customers[i].reveivedAd[k].equals(\old(customers[i].reveivedAd[j])))) && 
      @		(\exists int j; 0 <= j && j < customers[i].reveivedAd.length; customers[i].reveivedAd[j].getProduct == product)));
      @	also
      @ public exceptional_behavior
      @ signals (ProductNotFoundException e) !hasProduct(product);
      @ signals (AdvertiserNotFoundException e) hasProduct(product) &&
      @		!(\exists int i; 0 <= i && i < advertisers.length; advertisers[i].hasProduct(product));
      @ signals (EqualAdvertisementException e) hasProduct(product) &&
      @		(\exists int i; 0 <= i && i < advertisers.length; advertisers[i].hasProduct(product)) && 
      @		(\exists int i, j; 0 <= i && 0 <= j && i < advertisers.length && j < advertisers.length && j != i; advertisers[i].hasProduct(product) && advertisers[j].hasProduct(product));
      @*/
    public void sendAd(Product product) throws
            ProductNotFoundException, AdvertiserNotFoundException, EqualAdvertisementException;
  • purchase()方法规格
    /*@ public normal_behavior
      @ requires hasProduct(product) && hasCustomer(customer) &&
      @          (\exists int i; 0 <= i && i < advertisers.length; advertisers[i].hasProduct(product)) && 
      @		!(\exists int i, j; 0 <= i && 0 <= j && i < advertisers.length && j < advertisers.length && j != i; advertisers[i].hasProduct(product) && advertisers[j].hasProduct(product));
      @ assignable customer.getProduct, getAdvertiser(product).salesVolume, getProducer(product).salesVolume
      @ ensures  getAdvertiser(product).salesVolume == \old(getAdvertiser(product).salesVolume)+ 1;
      @ ensures getProducer(product).salesVolume == \old(getProducer(product).salesVolume) + 1;
      @ ensures customer.getProducts.length == \old(customer.getProducts.length) + 1;
      @ ensures (\forall int i; 0 <= i && i < \old(customer.getProducts.length); 
		(\exist int j; 0 <= j && j <= customer.getProducts.length; customer.getProducts[j] == \old(customer.getProducts[i])));
      @ ensurse (\exist int i; 0 <= i && i < customer.getProducts.length; customer.getProducts[i] == product);
      @ also
      @ public exceptional_behavior
      @ signals (ProductNotFoundException e) !hasProduct(product);
      @ signals (CustomerNotFoundException e) hasProduct(product) && !hasCustomer(customer);
      @ signals (AdvertiserNotFoundException e) hasProduct(product) && hasCustomer(customer) &&
      @		!(\exists int i; 0 <= i && i < advertisers.length; advertisers[i].hasProduct(product));
      @ signals (EqualAdvertisementException e) hasProduct(product) && hasCustomer(customer) &&
      @		(\exists int i; 0 <= i && i < advertisers.length; advertisers[i].hasProduct(product)) && 
      @		(\exists int i, j; 0 <= i && 0 <= j && i < advertisers.length && j < advertisers.length && j != i; advertisers[i].hasProduct(product) && advertisers[j].hasProduct(product));
      @*/
    public void purchase(Customer customer, Product product) throws
        CustomerNotFoundException, ProductNotFoundException,
	AdvertiserNotFoundException, EqualAdvertisementException;

querySalesVolume()方法规格

    /*@ public normal_behavior
      @ requires hasProduct(product);
      @ ensures /result == getProducer(product).getSalesVolume;
      @ also
      @ public exceptional_behavior
      @ signals (ProductNotFoundException e) !hasProduct(product);
      @*/
    public /*@ pure @*/ int querySalesVolume(Product product) throws
       ProductNotFoundException;

五、学习体会

本单元的作业任务量相较前两个单元有所减少,程序的整体架构也不需要自行设计。但根据JML规格编程首先需要我们正确理解JML的含义,如果理解产生偏差,则程序不可能正确。其次,即使实现了JML规格描述的功能,程序的性能还需要我们不断优化。通过本单元的学习,我认识到JML是一种逻辑严谨,表述清晰的形式化语言,利用JML可以将设计人员的意图无二义性的传递给开发人员,同时开发人员也可以利用JML规格对程序进行全面的测试,判断程序在所有输入情况下能否得到预期的结果。此外,在自己尝试对方法写JML规格时,我也经常发现自己对方法的行为存在考虑不周全的情况。虽然在本单元后我可能就不会再主动使用JML规格,但我会不断使用JML规格对于类和方法的规格化设计思路来进一步规范和完善自己的程序。

posted @ 2022-06-03 20:17  yysrW  阅读(25)  评论(0编辑  收藏  举报