2022面向对象设计与构造第三单元总结

一、测试策略

本单元作业我基本的测试策略就是单元测试 + 情况覆盖

  • 单元测试:使用JUnit工具,对每一个类建立一个测试类,然后再在测试类内编写各方法对应的测试方法。
  • 情况覆盖:由于本单元需要实现的接口都由JML描述,所以在编写测试方法时,只需要覆盖各方法的JML中写明的各种情况即可。比如对于Network接口中的storeEmojiId方法,它的JML描述为:
 /*@ public normal_behavior
      @ requires !(\exists int i; 0 <= i && i < emojiIdList.length; emojiIdList[i] == id);
      @ assignable emojiIdList, emojiHeatList;
      @ ensures (\exists int i; 0 <= i && i < emojiIdList.length; emojiIdList[i] == id && emojiHeatList[i] == 0);
      @ ensures emojiIdList.length == \old(emojiIdList.length) + 1 &&
      @          emojiHeatList.length == \old(emojiHeatList.length) + 1;
      @ ensures (\forall int i; 0 <= i && i < \old(emojiIdList.length);
      @          (\exists int j; 0 <= j && j < emojiIdList.length; emojiIdList[j] == \old(emojiIdList[i]) &&
      @          emojiHeatList[j] == \old(emojiHeatList[i])));
      @ also
      @ public exceptional_behavior
      @ signals (EqualEmojiIdException e) (\exists int i; 0 <= i && i < emojiIdList.length;
      @                                     emojiIdList[i] == id);
      @*/
    public void storeEmojiId(int id) throws EqualEmojiIdException;

由JML可知,这个方法有两种执行的可能,一种是emojiIdList不存在输入的id,此时将id加入emojiIdList,并将对应的emojiHeatList置零。另一种是emojiIdList存在输入的id,此时则会抛出EqualEmojiIdException异常。基于上面的复习,我编写了以下的测试方法:

    @org.junit.jupiter.api.Test
    void storeEmojiIdTest() throws Exception {
        network.storeEmojiId(1);
        System.out.println(network.containsEmojiId(1));
        network.storeEmojiId(1);
    }

前两行是为了测试第一种情况,第三行是为了测试第二种情况。若方法实现无误,则测试方法执行之后应该会先输出一个true,然后再抛出一个EqualEmojiIdException异常。

当然,这个测试方法并不完备,它并没有全面覆盖JML中的每一个ensure语句,我只是以它举例说明测试的基本思路。

二、架构设计

由于本单元作业的基础架构已由官方包决定,所以下面主要分析高时间复杂度方法的优化策略。

2.1 第一次作业

2.1.1 MyNetwork类isCircle()方法的优化

  • 优化前:
    • 算法:深度优先搜索。
    • 时间复杂度:O(n!)
  • 优化策略:
    使用并查集,用树来组织并查集,在MyPerson类中添加一个father属性,表示该对象在并查集树中的父节点,初始值为自身。当调用addRelation方法时,首先判断该relation两端的person在不在同一个并查集树中。若在,则直接添加,否则合并两个树。合并的原则是:将较高的那棵树的根节点作为新树的根节点,另一棵树的根节点作为新树根节点的子节点。
  • 优化后时间复杂度:
    addRelation时间复杂度不变,isCircle时间复杂度降为O(log2n),整体时间复杂度大大降低。

2.1.2 MyNetwork类queryBlockSum()方法的优化

  • 优化前:
    • 算法:二重循环 + isCircle
    • 时间复杂度:O(n^2*log2n)
  • 优化策略:
    为MyPerson类设置一个minIndex属性,表示该MyPerson对象所在的连通分量中所有MyPerson对象在people数组中的最小下标值。对象刚加入Network时,minIndex设为此时该对象的下标。当不同连通分量合并时,新的根节点的minIndex值设为原连通分量两个根节点的最小minIndex值。而查询一个MyPerson对象的block时,只需要找到该对象所在连通分量的根节点,查询根节点的minIndex值,将起与此对象的下标值相比较,若两者相等,则说明该对象之前的所有MyPerson对象都不与其连通,block为真。
  • 优化后时间复杂度:
    addRelation时间复杂度不变,queryBlockSum时间复杂度降为O(log2n),整体时间复杂度大大降低。

2.2 第二次作业

2.2.1 MyNetwork类queryLeastConnection()方法的优化

  • 优化前:
    • 算法:普通的Kruskal算法
    • 时间复杂度:O(mn)
  • 优化策略:
    建立一个Relation类,用来存储Network中的关系(即图的边),再在MyNetwork中增加一个connectGroups属性,用来存储各连通分量根节点和分量边集合的一一映射。当addRelation时,首先判断该relation两端的person在不在同一个连通分量中。若在,则直接将新的relation加进对应的connectGroups项,否则合并两个根节点对应的relation集合,再把新的relation加进去。这样,使用Kruskal算法时就可以先直接得到该点所处的连通分量的所有边,然后使用该边集进行快排。此外,同样可以将并查集应用到Kruskal算法连通分量的检验中,具体实现不再赘述。
  • 优化后时间复杂度:
    addRelation时间复杂度不变,queryLeastConnection的时间复杂度降为O(mlog2n),整体时间复杂度明显降低。

2.2.2 MyGroup类getValueSum()方法的优化

  • 优化前:
    • 算法:二重循环 + isLinked
    • 时间复杂度:O(n^3)
  • 优化策略:
    为MyPerson类设置一个inGroups属性,它是一个列表,表示该对象所在的所有群组。同时为MyGroup设置一个valueSum属性。当向群组加人时,先把该群组的id加进该person的inGroups列表,然后遍历该person的所有熟人,如果熟人也在该群组里,则将valueSum加上二倍的权值,从群组删除人时同理。特别地,当addrelation时,如果两端的person存在公共群组,则将这些群组的valueSum都加上二倍权值。同时,为了尽可能地减小时间复杂度,我将能使用Hashset/hashmap的地方都做了替换。
  • 优化后的时间复杂度:
    考虑到指导书给出的限制:一个person所在是群组最多为20个,所以可以认为addRelation时间复杂度不变,addToGroup和delFromGroup的时间复杂度增加到O(n),getValueSum的时间复杂度降为O(1),整体时间复杂度大大降低。

2.3 第三次作业

2.3.1 MyNetwork类sendIndirectMessage()方法的优化

  • 优化前:
    • 算法:普通的单源点最短路径算法(Dijkstra算法)
    • 时间复杂度:O(n^2 + m)
  • 优化策略:
    使用小根堆来维护Dest数组,降低寻找最小值操作的时间复杂度。
  • 优化后时间复杂度:
    sendIndirectMessage的时间复杂度降为O((m + n) * log2n),考虑到点和边的实际添加方式,所以时间复杂度实际上是从O(n^2)降为了O(nlog2n),整体时间复杂度明显降低。

三、代码性能问题

本单元我仅在第一次作业出现过一次性能问题,就是前文已述的queryBlockSum()方法,此处不再赘述。

四、Network扩展

4.1 基本扩展思路

新建MyAdvertiser、MyProducer、MyCustomer类,分别用来表示Advertiser、Producer、Customer,并在MyNetwork中建立advertisers、producers、customers属性。为了实现这些新增人物的特殊属性,MyAdvertiser应该有一个存储当前正在推送广告的产品id属性和雇主id属性,以及customers和producers的一个副本;MyProducer的属性应该包含产品的id、价格、销售量,以及customers的一个副本;MyCustomer的属性应该有自己收到的所有广告组成的列表。同时实现这三个类内部以及MyNetwork的相关方法。

4.2 相关接口方法

  • MyAdvertiser
    • getter-setter方法
    • sendAdvertise方法:向消费者发送广告(产品id)
    • sendBuyingMessage方法:消费者通过它向生产商发送购买需求
  • MyProducer
    • getter-setter方法
  • MyCustomer
    • getter-setter方法
    • buy方法:向广告商发送购买需求
  • MyNetwork
    • setAdvertiser方法:为生产商绑定一个广告商
    • advertise方法:让广告商向所有消费者发送一轮广告
    • buyProduct方法:让消费者购买一定数量的某产品
    • queryProductSales方法:查询产品销售额
    • queryProductPath方法:查询产品销售路径
    • 其它的get*、contains*查询方法,add*增添方法

4.3 核心业务接口JML

选择MyNetwork的前三个核心业务功能接口

  • setAdvertiser方法:
/*@ public normal_behavior
  @ requires (containsProducer(producerId) && containsAdvertiser(advertiserId));
  @ assignable getAdvertiser(advertiserId).productId, getAdvertiser(advertiserId).producerId;
  @ ensures getAdvertiser(advertiserId).productId == getProducer(producerId).productId;
  @ ensures getAdvertiser(advertiserId).producerId == producerId;
  @ also
  @ public exceptional_behavior
  @ signals (ProducerIdNotFoundException e) !containsProducer(producerId)
  @ signals (AdvertiserIdNotFoundException e) (containsProducer(producerId) && !containsAdvertiser(advertiserId));
  @*/
public void setAdvertiser(int producerId, int advertiserId);
  • advertise方法:
/*@ public normal_behavior
  @ requires containsAdvertiser(advertiserId);
  @ assignable customers[*].receivedAdvertise;
  @ ensures (\forall int i; i >= 0 && i < customers.length;
  @		(\forall int j; j >= 0 && j < \old(customers[i].receivedAdvertise.length);
  @			(\exist int k; k >= 0 && k < customers[i].receivedAdvertise.length; \old(customers[i].receivedAdvertise)[j] == customers[i].receivedAdvertise[k])));
  @ ensures (\forall int i; i >= 0 && i < customers.length;
  @		(\exist int j; j >= 0 && j < customers[i].receivedAdvertise.length; customers[i].receivedAdvertise[j] == getAdvertiser(advertiserId).productId));
  @ ensures (\forall int i; i >= 0 && i < customers.length;
  @		customers[i].receivedAdvertise.length = \old(customers[i].receivedAdvertise).length + 1);
  @ also
  @ public exceptional_behavior
  @ signals (AdvertiserIdNotFoundException e) !containsAdvertiser(advertiserId));
  @*/
public void advertise(int advertiserId);
  • buyProduct方法:
/*@ public normal_behavior
  @ requires (containsCustomer(customerId) && getCustomer(customerId).contains(productId) && amount >= 0);
  @ assignable getProducer(productId).sales;
  @ ensures getProducer(productId).sales == \old(getProducer(productId).sales) + getProducer(productId).price * amount;
  @ also
  @ public exceptional_behavior
  @ signals (CustomerIdNotFoundException e) !containsCustomer(customerId));
  @ signals (ProductIdNotFoundException e) (containsCustomer(customerId) && !getCustomer(customerId).contains(productId));
  @ signals (AmountException e) (containsCustomer(customerId) && getCustomer(customerId).contains(productId) && amount < 0);
  @*/
public void buyProduct(int customerId, int productId, int amount);

五、学习体会

本单元的学习给我带来了两大体会:

  • 一是考虑问题一定要全面。JML是一个严谨性很高的语言,能囊括一个方法的各种情况,但凡具体实现与JML有丝毫偏差,都会产生意想不到的错误。因此,在学习JML的过程中,我的考虑问题的思维也不免变得更加严谨、全面。
  • 而是性能与功能同样重要。在过去的代码实践中,我一直都只关注自己代码的功能是否符合要求,很少去思考算法是否最优、时间复杂度能否减小、是否存在冗余操作等问题。本单元的学习让我意识到了性能的重要性,也为我培养了一些降低时间复杂度的基本思维。
posted @ 2022-05-31 15:35  20231026  阅读(39)  评论(1编辑  收藏  举报