OO-2022-Unit3-BeihangCSE

第三单元总结


数据测试

副作用

在JML中,一个方法除了返回值即\result之外,还有存在许多限制条件,例如\not_assigned, \not_modified,这些条件通常限制了我们在一个方法内对被调用的对象的操作,这是测试数据的重点之一

异常处理

需要对\require 做出的对 normal_behaviour 和 exceptional_behaviour做到全面的测试。需要构造各种各样的情况以全面覆盖方法行为。通常而言,exceptional_behaviour 下规定的抛出异常的情况相对特殊且固定,是很容易想到的。我们还需要注意在 normal_behaviour 中可能存在的“不便处理”或者“需要特殊处理”的情况,这是在编程中容易出现错误的地方。从编程者的角度而言,需要思考自己的方法能否应对 normal_behaviour 下存在的所有情况。


架构设计与性能维护

架构的建设

总体上说,在类的设计上并没有多少我们能发挥的空间,大部分只需要按照文档新建一个My*类并且实现对应接口中的方法即可。

但在方法的实现和类管理数据的策略上,JML并不做要求。

在一开始看到类似Person[] people的描述,我下意识地会使用ArrayList甚至数组来管理数据。但会发现这样的数据结构在后续若干查询操作中会显得非常复杂,最后不得已还是选择了使用HashMap。

但其实这并不能说是“不得已”。正如前文所说,JML并不限制我们的实现方法,Person[] people这样的表述也不过只是要求了一个“容器”,在不要求顺序的场景下,任何的容器都是合理的。在频繁查询的需求下,使用HashMap是自然的事。

hw9中的并查集

hw9中存在一个查询联通的指令,isCircle,以及一个在JML中依靠它完成描述的指令,queryBlockSum,查询联通分支的数量。

isCircle的实现,我们很自然能想到使用BFS或者DFS等算法搜索整个图来实现。由于指导书中限制了相关指令的次数,这种O(V+E)的算法是合理的。但如果在此基础上还要按照JML的描述使用双重循环遍历来实现queryBlockSum那就非常糟糕了。

为了优化整体的性能,我选择了并查集+动态维护queryBlockSum的方法来实现两个方法。要实现并查集,那么我们还需要另外几个数据结构和方法,具体如下

private HashMap<Integer,Integer> branches; //父节点
private HashMap<Integer, Integer> rank; //深度
private static int size; //blockSum

public int find(int i) { //查找根节点
        if (branches.get(i) == i) {
            return i;
        }
        else {
            return find(branches.get(i));
        }
}

public void union(int i, int j) { //合并点
        int b1 = find(i);
        int b2 = find(j);
        if (rank.get(b1) <= rank.get(b2)) {
            branches.put(b1, b2);
            if (rank.get(b1).equals(rank.get(b2)) && b1 != b2) {
                rank.put(b2, rank.get(b2) + 1);
            }
        } else { branches.put(b2, b1); }

        if (b1 != b2) {
            size--;
        }
}

在该算法中,我还添加了rank这个结构来尽量平衡树,使得复杂度缩减到了Olog2V。

hw10中的动态维护与Prim堆优化

在hw10中,query_least_connection,是一个比较明显的高复杂度的指令。在讨论区,助教也给出了相应的算法,

但值得注意的是,Prim算法是O(V2),Kruskal算法的复杂度是O(ElogV)。指导书中的数据限制中,我们可以知道E<<V2,因此Prim算法是存在风险的。最好使用堆优化。而且Java自带优先队列PriorityQueue这一数据结构,所谓堆优化实现起来是非常方便的。

下面就是这次作业最容易被忽略的query_group_value_sum。因为它的实现相当简单,很多同学下意识就按照JML给出的方式实现了,丝毫没有意识到它的O(V2)的复杂度。而且指导书中并没有对相关指令做限制,所以很容易就出现超时,这也是在互测阶段中大多数同学出问题的原因

解决办法也很简单,在atg\dtg\ar等指令中动态维护一个groupValueSum即可。

hw11中的最短路径

hw11并没有什么需要特别注意的,只要使用Dijkstra实现sendIndirectmessage就好。当然,为了保险起见,最好加上之前使用过的堆优化。

边的表示

为了实现Dijkstra和Prim算法,我另外设计了一个类Link

public class Link implements Comparable<Link> {
    private int id1;
    private int id2;
    private int value;

    public Link(int id11, int id21, int value1) {
        id1 = id11;
        id2 = id21;
        value = value1;
    }

    public boolean containsTo(int id) { return id2 == id; }

    public int getTo() { return id2; }

    public int getValue() { return value; }

    public int compareTo(Link link) {
        return value - (link.getValue()) > 0 ? 1 : -1;
    }

}

并且在MyPerson中实现了一个方法返回它的所有Link

public ArrayList<Link> allLink() {
        ArrayList<Link> links = new ArrayList<>();
        for (Integer i : acquaintance.keySet()) {
            links.add(new Link(id, acquaintance.get(i).getId(), value.get(i)));
        }
        return links;
}

关于代码规模

有些同学在最后依次作业中遇到了MyNetwork代码超过500导致checkStyle出问题的情况,为了缩减代码花了很多心思。在此我提供几个思路:

  • 边、并查集等“工具”,可以在抽象到单独的类中实现,不必放在MyNetwork来实现其操作。甚至类似于Prim、Dijkstra等算法也可以单独封装到另一个类的方法实现,只要提供一个接口给MyNetwork访问即可。
  • 动态维护的量,建议在对应的类中实现,而不要放到MyNetwork中来。Person、Group类本身就保留许多自身的信息,可以直接实现对自己vauleSum等值的动态维护,MyNetwork只需要调用方法,具体实现就放在具体的位置最好。如果有需要的数据,完全可以在方法中传值,甚至使用单例模式直接访问MyNetwork的数据。

拓展

数据管理

首先我们规定各种Person各自管理的一些数据。

我们认为Producer与Customer无法建立直接的关系,只和Advertiser“打交道”,同理Customer也无法直接与Producer连接。

一个Producer只生产一种商品,但可以有多个Advertiser为他打广告;一个Advertiser只经营一种商品,并且向多个Customer推送,Customer根据preference选择从哪个Advertiser那里购买、购买哪种产品

public interface Advertiser extends Person {

    /*@ public instance model int[] sales
      @ public instance model Person[] customers
      @ public instance model Person producer
      @*/
}

public interface Producer extends Person {

    /*@ public instance model Product product
      @ public instance model Person[] advertisers
      @ public instance model int[] sales
      @ public instance model double ratio
      @*/
}

public interface Customer extends Person {
    
    /*@ public instance model int id;
      @ public instance model Person[] advertisers
      @ public instace model int[] bought
      @ public instance model int perferece
      @ public instance model Product 
      @*/
}

下面给出推送广告、购买产品和计算提成三个方法的JML规格

计算提成

/*@ public normal_behavior
  @ requires containsProducer(id1);
  @	ensures \results = (\sum int i; 0<=i && i<getProducers(id1).sales.length; 
  				(getProducers(id1).sales[i]*getProducers(id1).product.price*getProducers(id1).ratio)/1);
  @ public exceptional_behavior
  @ signals (CustomerIdNotFoundeption e) !containsProducer(id1);
  @*/
public int pushMoney(int id1)

推送广告

/*@ public normal_behavior
  @ requires containsCustomer(id1) && containsAdvertiser(id2)
  @				&& getCustomer.containsAdvertiser(id2);
  @ assignable getCustomer(id1).bought, getCustomer(id1).advertisers, getAdvertisers(id2).customers,
  @				getAdvertisers(id2).sales;
  @ ensures getCustomer(id1).bought.length = \old(getCustomer(id1).bought.length) + 1;
  @ ensures getCustomer(id1).advertisers.length = \old(getCustomer(id1).advertisers.length) + 1;
  @ ensures (\forall int i; 0<=i && i<getCustomer(id1).advertisers.length;
  @				getCustomer(id1).advertisers[i] == \old(getCustomer(id1).advertisers[i]));
  @ ensures getCustomer(id1).advertisers[\old(getCustomer(id1).advertisers.lenght)] == getAdvertiser(id2);
  @ ensures (\forall int i; 0<=i && i<getCustomer(id1).bought.length; 
  @				getCustomer(id1).bought[i] == \old(getCustomer(id1).bought[i]));
  @ ensures getCustomer(id1).bought[\old(getCustomer(id1).bought.length)] == 0;
  
  @ ensures getAdvertiser(id2).sales.length = \old(getAdvertiser(id2).sales.length) + 1;
  @ ensures getAdvertiser(id2).customers.length = \old(getAdvertiser(id2).customers.length) + 1;
  
  @ ensures (\forall int i; 0<=i && i<getAdvertiser(id2).customers.length;
  @				getAdvertiser(id2).customers[i] == \old(getAdvertiser(id2).customers[i]));
  @ ensures getAdvertiser(id2).customers[\old(getAdvertiser(id2).customers.lenght)] == getCustomer(id1);
  @ ensures (\forall int i; 0<=i && i<getAdvertiser(id2).sales.length; 
  @				getAdvertiser(id2).sales[i] == \old(getAdvertiser(id2).sales[i]));
  @ ensures getAdvertiser(id2).sales[\old(getAdvertiser(id2).sales.length)] == 0;
  @ also
  @ public exceptional_behavior
  @ signals (CustomerIdNotFoundeption e) !containsCustomer(id1);
  @ signals (AdvertiserIIdNotFoundException e) containsCustomer(id1) && !containsAdvertiser(id2);
  @ signals (AdvertisingDuplicateException e) containsCustomer(id1) && containsAdvertiser(id2)
  @ 		&& getCustomer(id1).containsAdvertiser(id2);
  @*/
public void pushingMessage(int id1, int id2);

购买商品

/*@ public normal_behavior
  @ requires containsCustomer(id1) && containsProduct(id3) && containsAdvertiser(id2)
				&& getCustome(id1).containsAdvertiser(id2) && getProducer(id3).containsAdvertiser(id2);
  @ assignable getProducer(id2).sales, getCustomer(id1).bought, getAdvertiser(id2).sales;
  @ ensures (\forall int i; 0<=i && i < getCustomer(id1).bought.length 
  @				&& getAdvertiser(id2) == \old(getCustomer(id1).advertisers[i]);
  @				getCustomer(id1).bought[i] = \old(getCustomer(id1).bought[i]) + 1);
  @ ensures (\forall int i; 0<=i && i < getCustomer(id1).brought.length 
  @				&& getAdvertiser(id2) != \old(getCustomer(id1).advertiser[i]);
  @				getCustomer(id1).bought[i] = \old(getCustomer(id1).bought[i]));
  @ ensures (\forall int i; 0<=i && i < getAdvertiser(id2).sales.length 
  @				&& getCustomer(id1) == \old(getAdvertiser(id2).customers[i]);
  @				getAdvertiser(id2).sales[i] = \old(getAdvertiser(id2).sales[i]) + 1);
  @ ensures (\forall int i; 0<=i && i < getAdvertiser(id2).sales.length 
  @				&& getCustomer(id1) != \old(getAdvertiser(id2).customers[i]);
  @				getAdvertiser(id2).sales[i] = \old(getAdvertiser(id2).sales[i]));					
  @ ensures (\forall int i; 0<=i && i < getProducer(id3).sales.length 
  @				&& getAdvertiser(id2) == \old(getProducer(id3).advertiser[i]);
  @				getProducer(id3).sales[i] = \old(getProducer(id3).sales[i]) + 1);                      
  @ ensures (\forall int i; 0<=i && i < getProducer(id3).sales.length 
  @				&& getAdvertiser(id2) != \old(getProducer(id3).advertiser[i]);
  @				getProducer(id3).sales[i] = \old(getProducer(id3).sales[i]));                   
  @ also
  @ public exceptional_behavior
  @ signals (CustomerIdNotFoundeption e) !containsCustomer(id1);
  @ signals (AdvertiserIIdNotFoundException e) containsCustomer(id1) && !containsAdvertiser(id2);
  @ signals (ProducerIdNotFoundException e) containsCustomer(id1) && containsAdvertiser(id2)
  @ 		&& !containsProducer(id3);
  @ signals (NoAdvertisingException e) containsCustomer(id1) && containsAdvertiser(id2)
  @ 		&& containsProducer(id3) 
  @ 		&& !getCustomer(id1).containsAdvertiser(id2);
  @ signals (NoEmpolyingException e) containsCustomer(id1) && containsAdvertiser(id2)
  @ 		&& containsProducer(id3) && getCustomer(id1).containsAdvertiser(id2)
  @ 		&& !getProducer(id3).containsAdvertiser(id2); 
  @*/
public void buy(int id1, int id2, int id3);

心得体悟

在JML单元的学习中,我了解到了接口规格语言的魅力。相比可能产生的歧义 的自然语言,JML利用简单的规则,建构出了一套语言体系用以描述接口。在对方法的描述中,除了返回值,或者说除了要做什么 对JML这样一种逻辑严谨的语言来说,更重要的是不能做什么,或者说应当保留什么。而自然语言容易出现歧义,或者说容易出现漏洞的核心也总是在后者,例如老师上课举例所说的“给数组排序”这一操作,在这一要求下,直接清空数组似乎也完成了“排序”这一目标,即最后的数组是有序的。

但同样的,JML具有很高的复杂性,阅读JML远比阅读自然语言困难。从hw9开始,例如queryBlockSum这样的操作的JML,阅读起来不仅规模庞大而且难以理解,需要花费很多时间才能理解这几十行JML描述的只是个求联通分支数的操作。而且JML的描述仅仅是在功能上完成了规格设计,对接口的实现可以说没有太大的参考意义(从queryBlockSum中的遍历isCircle就可以看出)。从编写JML的一方而言,如何把自然语言描述下的功能转化为规范的JML也是一大难题。我们可以思考这样一个应用场景,功能设计者和程序员之间只靠“连通分支”四个字就能大致描述清楚的问题,如果使用JML的话,则需要双方都画上几十分钟思考如何转换这四个字和JML。我相信即使是对于JML熟练的程序员来说,这也是一件伤神的事情,更不要提对功能设计者了。

这点在我们周四的上机中也体现的非常明显。以往的上机大家都觉得时间富余,然而在第三单元的上机,无论是根据JML代码填空还是根据自然语言编写JML,都非常花时间,每次上机很多踩点提交。

在本单元的学习中,我认为自己还存在一个比较严重的问题:太过于将JML和JML的实现联系在一起。比如,到hw10时,我的代码里还存在相当多的ArrayList结构,这对一个频繁查找的图来说,是非常低效的。比如,在Exception的实现中,每个类的属性、构造函数和print()方法都非常相似,但我仍然“按照"JML的描述单独实现了,并且为了偷懒复制粘贴的了许多部分,最后出了问题。对于一个面向对象的工程来说,继承方法、复用代码应该是非常核心的概念,而我却使用了复制粘贴的这样低级的操作。

总而言之,这个单元让我较为深入的了解了JML的优势与短处,也认识到了自己在面向对象编程中还存在的问题。

posted @ 2022-06-04 22:07  Danny121008  阅读(17)  评论(1编辑  收藏  举报