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,这样才算是真正有收获。