OO第三单元总结

第三单元总结

一、利用JML规格构造测试数据

构造思路

本单元的接口提供了详细的JML说明,可以以此为根据进行测试数据的构造。在JML中我们主要关注以下三个部分:

  • exceptional_behavior:除了正常的测试数据,这些会导致异常的输入也一定要全部覆盖。如果一个方法存在exceptional_behavior,如id重复、id不存在、关系不存在等等,都需要我们在生成数据时一一枚举。
  • requires:即前置条件,构造的数据一定要满足方法的前置条件,不论是正常的还是异常的,否则就是不合格的数据。
  • ensures:即后置条件,虽然测试数据不必考虑后置条件,但根据后置条件可以构造查询语句。比如addPerson保证人数加一,那么我们就可以用qps来查询人数,判断方法执行的正确性。

数据生成器的具体实现

在本单元的三次作业中,我都是用python实现了数据生成器,三次作业的数据生成思路大致相同,只是有一定的迭代。

覆盖率

为了使生成的数据覆盖所有的情况,随机产生符合不同requires的数据是必要的。以queryValue为例,它的JML规格如下:

    /*@ public normal_behavior
      @ requires contains(id1) && contains(id2) && getPerson(id1).isLinked(getPerson(id2));
      @ ensures \result == getPerson(id1).queryValue(getPerson(id2));
      @ also
      @ public exceptional_behavior
      @ signals (PersonIdNotFoundException e) !contains(id1);
      @ signals (PersonIdNotFoundException e) contains(id1) && !contains(id2);
      @ signals (RelationNotFoundException e) contains(id1) && contains(id2) &&
      @         !getPerson(id1).isLinked(getPerson(id2));
      @*/
    public /*@ pure @*/ int queryValue(int id1, int id2) throws
            PersonIdNotFoundException, RelationNotFoundException;

那么生成的数据就要保证覆盖到正常情况和三种异常情况,我的python代码大致如下:

def query_value():
    t = random.randint(0, 8)
    if t == 0:  # id1不存在
        id1 = random.randint(100, 107)
        id2 = people[random.randint(0, len(people)) - 1].id
        update_exception(PersonIdNotFoundException, id1, True)
    elif t == 1:  # id2不存在
        id1 = people[random.randint(0, len(people)) - 1].id
        id2 = random.randint(100, 107)
        update_exception(PersonIdNotFoundException, id2, True)
    elif t == 2:  # 都不存在
        id1 = random.randint(100, 107)
        id2 = random.randint(100, 107)
        update_exception(PersonIdNotFoundException, id1, True)
    elif t == 3:  # 没有关系
        while True:
            index1 = random.randint(0, len(people) - 1)
            index2 = random.randint(0, len(people) - 1)
            if relation[index1][index2] == 0:
                id1 = people[index1].id
                id2 = people[index2].id
                break
        update_exception(RelationNotFoundException, id1, True)
        update_exception(RelationNotFoundException, id2, False)
    else:	# 正常情况
        if len(relation_list) == 0:
            id1 = people[random.randint(0, len(people) - 1)].id
            id2 = id1
            ans.append("0")
        else:
            tmp = relation_list[random.randint(0, len(relation_list) - 1)]
            index1 = tmp[0]
            index2 = tmp[1]
            id1 = people[index1].id
            id2 = people[index2].id
            ans.append(str(relation[index1][index2]))

高质量

为了提高生成器的效率,尽量少的产生无用的数据(正常或异常情况占绝大多数,比如100条询问最短路径的指令有99条都会出异常),我定义了一些全局数据,用来记录当前的状态,根据状态来产生数据可以有效避免上述情况。定义的全局数据如下:

people = []		# 网络中人的集合,包含("id", "name", "age", "money", "socialValue")
people_list = []	# 没有被添加到groups里面的人的集合,可以根据此列表直接生成异常数据
relation = []  		# 关系的邻接矩阵
relation_list = []  # 每成功添加一对关系,就将这两个人的(id1的序号, id2的序号)添加进去,便于直接生成一组关系
groups = []  		# 组的集合
block = []		# 连通块的集合
messages = []		# 消息集合
people_message = []	# 未发送的消息集合,可以根据此列表获得一条未发送的消息

可控性

好的数据生成器可以通过修改某些变量从而产生不同类型的数据。我定义了不同语句的条数作为全局变量,通过改变该全局变量即可直接控制产生的数据的类型和规模。比如想专门针对最小生成树来生成数据,只需将NUM_QLC调大,无关指令条数设为0即可。

NUM_AP = 500      # add_person
NUM_AR = 2000     # add_relation
NUM_QV = 100      # query_value
NUM_QPS = 50      # query_people_sum
NUM_QC = 100      # query_circle
NUM_QBS = 100      # query_block_sum
NUM_AG = 20      # add_group
NUM_ATG = 500    # add_to_group
NUM_DFG = 50    # del_from_group

NUM_QGPS = 50      # query_group_people_sum
NUM_QGVS = 500    # query_group_value_sum
NUM_QGAV = 100    # query_group_age_var
NUM_AM = 2000     # add_message
NUM_SM = 1000     # send_message
NUM_QSV = 100     # query_social_value
NUM_QRM = 100     # query_received_messages
NUM_QLC = 50     # query_least_connection

二、架构分析

数据结构

刚写第一次作业的时候,我并没有太关注数据结构,JML规格中有对数据模型的声明,我就直接拿来用了。如Person中的以下模型:

@ public instance model non_null Person[] acquaintance;
@ public instance model non_null int[] value;

看到是数组我就直接用ArrayList了。这样当然也能实现全部功能,但是效率肯定是很低的,因为作业中需要多次查询,而采用ArrayList查询的效率是O(n)。在意识到这一点之后我将大多数的数据结构由ArrayList改成HashMap,这样查询效率就大大提高,插入删除的效率基本不变。这也使我更加理解JML中的数据模型只是个便于描述的方式,而真正的实现需要根据实际情况作出考虑。

而对于以下模型:

@ public instance model non_null Message[] messages;

根据Network中定义的操作,messages需要是连续存储的,并且需要多次从头添加数据。所以根据这一属性,我选择了LinkedList来实现,因为它比ArrayList有更高的头部插入性能。

图的构建

关于图的构建我并没有去建立邻接表等数据结构去单独存储图,而是就利用接口中提供的模型和方法。

第一次作业的qbs我使用的是并查集,当然用广度优先搜索也是可以做的,但事实证明广搜在数据量大的时候会超时。并查集虽然在每次添加关系的时候都会进行合并的操作,但它的查询复杂度几乎是O(1),所以qbs语句再多也不会超时。

第二次作业的qlc又需要用到最小生成树算法,我用的是Prim,因为Prim算法不需要预处理所有的点和边。

第三次作业考察了最短路径,单源最短路径的最优算法当然是堆优化的Dijkstra算法。开始我没有使用堆优化,然后我自己专门为此构造了一组数据,结果不优化要40s才能运行完,而堆优化只需要1s,这就是\(O(n^2)\)\(O(nlogn)\)的区别。

维护策略

大多数的查询还是查询一次就进行相应的计算,但对于某些查询复杂度高的语句,就要考虑维护策略了。比如刚刚提到的并查集,就是一种维护的方法。再比如qgvs查询组员的价值和,每一次查询都要双重循环,一旦查询次数很多就会超时,因此我在MyGroup类中维护了groupValue这一变量,每次添加删除成员或者添加关系时就计算它的值,使得查询的时间复杂度为O(1)

三、性能问题及修复情况

我还是按作业时间顺序来依次说明我所遇到的性能问题:

性能问题 修复情况
根据id查询人或组复杂度高 采用HashMap
qbs多次查询复杂度高 采用并查集
qgvs多次查询复杂度高 采用维护变量的方法
message插入效率低 采用LinkedList
最短路径复杂度高 采用堆优化

可以发现本单元的性能问题要么是数据结构的不合理导致的,要么是查询算法过于复杂导致的。对于前者采取最合适的数据结构就可以解决,而后者则需要考虑优化算法。

四、Network扩展

为实现对Network的扩展,先进行一些约定。

相关数据规格

Producer

一个生产者只生产一种商品。数据规格如下:

public instance model String name;		// 生产的商品名
public instance model int value;		// 价格
public instance model int sales;		// 销售额

Advertiser

广告商可以为多种商品打广告,并且可以将商品推销给所有的Customer。数据规格如下:

public instance model Producer producers[];	// 所推销商品的生产者列表

Customer

消费者从Advertiser处购买商品。数据规格如下:

public instance model Record records[];	// 购买的记录

Record

订单记录。

public instance model Customer customer;		// 消费者
public instance model Advertiser advertiser;	// 广告商
public instance model Producer producer;		// 生产者

方法JML

购买商品

id为id1的消费者从id为id2的广告商购买id为id3的生产者的商品。

具体实现:生产者的销售额增加,消费者的订单加一。

/*@ public normal_behavior
  @ requires containsCustomer(id1) && containsAdvertiser(id2)
  @ 		&& containsProducer(id3) 
  @ 		&& getAdvertiser(id2).producers.contains(getProducer(id3));
  @ assignable getProducer(id3).sales, getCustomer(id1).records;
  @ ensures \old(getProducer(id3).sales) + getProducer(id3).value == getProducer(id3).sales;
  @ ensures \old(getCustomer(id1).records.length) + 1 == getCustomer(id1).records.length;
  @ ensures getCustomer(id1).records[getCustomer(id1).records.length].customer 
  @ 		== getCustomer(id1);
  @ ensures getCustomer(id1).records[getCustomer(id1).records.length].advertiser 
  @			== getAdvertiser(id2);
  @ ensures getCustomer(id1).records[getCustomer(id1).records.length].producer 
  @ 		== getProducer(id3);
  @ ensures (\forall int i; 0 <= i && i < \old(getCustomer(id1).records.length);
  @ 		getCustomer(id1).records[i] == \old(getCustomer(id1).records[i]));
  @ also
  @ public exceptional_behavior
  @ signals (PersonIdNotFoundException e) !containsCustomer(id1);
  @ signals (PersonIdNotFoundException e) containsCustomer(id1) && !containsAdvertiser(id2);
  @ signals (PersonIdNotFoundException e) containsCustomer(id1) && containsAdvertiser(id2)
  @ 		&& !containsProducer(id3);
  @ signals (NotAdvertiseException e) containsCustomer(id1) && containsAdvertiser(id2)
  @ 		&& containsProducer(id3) 
  @ 		&& !getAdvertiser(id2).producers.contains(getProducer(id3));
  @*/
public void buy(int id1, int id2, int id3);

查询销售额

查询id为id的生产者的销售额。

具体实现:直接获取生产者的sales属性。

/*@ public normal_behavior
  @ requires containsProducer(id);
  @ assignable \nothing;
  @ ensures \result == getProducer(id).sales;
  @ also
  @ public exceptional_behavior
  @ signals (PersonIdNotFoundException e) !containsProducer(id);
  @*/
public /*@ pure @*/ int getSales(int id);

查询销售路径

查询id为id的消费者的第index条订单记录,订单记录中即包含了销售路径。

具体实现:直接获取消费者的第index条订单信息。

/*@ public normal_behavior
  @ requires containsCustomer(id) && index < getCustomer(id).records.length;
  @ assignable \nothing;
  @ ensures \result == getCustomer(id).records[index];
  @ also
  @ public exceptional_behavior
  @ signals (PersonIdNotFoundException e) !containsCustomer(id);
  @ signals (RecordsIndexOutOfBoundException e) containsCustomer(id) &&
  @ 		index >= getCustomer(id).records.length;
  @*/
public /*@ pure @*/ Record getRecord(int id, int index);

五、学习体会

JML是Java的一种行为接口规格语言,我也是第一次学习类似的表示方式。刚开始学习的时候感觉JML是把简单的事情复杂化,明明几句话就能说清楚的方法却要用大段的语法描述,而且有些表述可能很复杂,需要许多时间来理解。

其实这三次作业都只是根据JML语言来实现方法。有了JML的提示,对于一些简单的方法,几乎直接将JML复制下来就可以完成;而对于复杂的方法,需要仔细理解,然后用一定的算法去实现。三次作业写下来,我对JML的态度也是逐渐好转,我们只需要理解JML并实现方法,因为整体的架构已经设计好了,填写方法就变得相对容易了。

在具体实现时,需要考虑两个方面:其一是对数据结构的选择,JML只是给出了模型,这是为了JML的表示,但我们并不一定完全按照JML的表示来,我们只需要实现这些模型,至于数据结构完全可以自己选择;其二是对方法中算法的选择,JML只给出了条件,并没有限制算法,所以我们应该尽量使用高效的算法。

从对接口/方法的描述上来看,JML肯定是比自然语言要准确的,它明确给出了前置条件、后置条件、正常情况、异常情况、哪些变量在方法中可以被修改。这使得编写者可以对方法的作用了如指掌,从而不会在实现上产生偏差。

当然只是实现JML所表述的方法是比较简单的,难的是自己设计程序的架构和写JML语言。这其实就反映了程序设计的两个方面——架构和实现,我们当然不止要会根据JML实现方法,还应该去学习代码中的JML表示方法,尝试自己去进行程序架构、写JML,这样才算是真正有收获。

posted @ 2022-06-01 16:26  天机晓梦  阅读(40)  评论(0编辑  收藏  举报