OO第三单元总结
一、利用JML规格准备测试数据
JML规格能够助力于规格化设计,规格化设计的核心是方法的前置条件、方法后置条件和对象的不变式。
方法的前置条件包含了normal_behavior
和exceptional_behavior
两种情况,因此我们在构造测试数据时需要覆盖到这两种情况,具体表现为在生成测试数据时,使用一个createId方法来让id随机触发异常,并通过参数来控制异常出现的频率。
public static int createId(int type, ArrayList<Integer> set) {
Random random = new Random();
int id;
if (set.size() == 0) { //此时只能触发异常
id = random.nextInt(100); //此时任何数都可以触发异常
} else {
if (type == 0) { //不产生异常
id = set.get(random.nextInt(set.size()));
} else { //可能产生异常
int flag = random.nextInt(50); //用于改变异常出现的频率
if (flag == 0) {
id = random.nextInt(1000) + set.size();
} else {
id = set.get(random.nextInt(set.size()));
}
}
}
return id;
}
方法的正确性保证了在规定的输入范畴内给出满足规定要求的输出,即如果"你"能够保证前置条件成立,"我"就能够保证后置条件成立。但在有些方法中,后置条件并不以显式的输出给出(比如说addPerson
方法normal_behavior
对应的后置条件改变的是people
数组),因此对某些方法的后置条件是否成立的判断需要我们在程序运行过程中,时不时自行调用相关函数进行检测(比方说可以通过调用int queryPeopleSum()
方法或者与新加入的Person有关联的方法来检测addPerson
方法后置条件的正确性)。
因此我们在构造测试数据时,需要有意识地将指令分为构造指令和查询指令。为了保证数据强度,我们可以先使用一系列构造指令生成图的基本框架,之后采用构造指令和查询指令交替的方式,通过检测后置条件的正确性来检测方法的正确性。
关于对象不变式的检测,与后置条件正确性的检测类似。我们通过改变对象和查询对象不断交替的方式,来判断对象不变式是否恒成立。
二、三次作业的架构设计
1、第三次作业图模型构建和维护策略
图模型构建
-
package main
-
MainClass
为主类,实现了Runner实例并运行 -
MyNetWork
是顶层类,指令对应的方法都在该类中 -
MyEmojiMessage
、MyGroup
、MyMessage
、MyNoticeMessage
、MyPerson
、MyRedEnvelopeMessage
为图中的相关元素 -
PeopleAndSides
是自己创建的类,存储了图中一个连通分支的节点和边,主要用于实现各种有关图的算法 -
Side
是自己创建的类,存储了图中的一条边 -
Count
是自己创建的类,用于异常类的计数,使用方法见下public class MyEmojiIdNotFoundException extends EmojiIdNotFoundException { private static Count count = new Count(); private int id; public MyEmojiIdNotFoundException(int id) { this.id = id; count.recordCnt(); count.recordId(id); } public void print() { System.out.println("einf-" + count.getCnt() + ", " + id + "-" + count.getIdMsg(id)); } }
-
-
package exceptions
包含10个异常类MyEmojiIdNotFoundException
、MyEqualEmojiIdException
、MyEqualGroupIdException
、MyEqualMessageIdException
、MyEqualPersonIdException
、MyEqualRelationException
、MyGroupIdNotFoundException
、MyMessageIdNotFoundException
、MyPersonIdNotFoundException
、MyRelationNotFoundException
维护策略
-
MyNetWork
是顶层类,其中采用了多个HashMap存储图中的相关元素public class MyNetwork implements Network { private HashMap<Integer, Person> people; private HashMap<Integer, Group> groups; private HashMap<Integer, Message> messages; private HashMap<Integer, Integer> emojis; //id-heat private HashMap<Integer, PeopleAndSides> blocks; //连通分支 …… }
-
一个
PeopleAndSides
对应一个连通分支,其中存储了图中一个连通分支的节点和边,主要用于实现各种有关图的算法public class PeopleAndSides { private HashSet<Person> people; private TreeSet<Side> sides; …… }
-
其余各个元素类与上述两个类类似,都需要存储与自身信息有关的属性,在图的构建过程中不断维护这些属性
-
异常类不断统计异常数量,动态维护
2、第三次作业类图
不包含异常类和异常计数类Count
3、第三次作业指标度量分析
(1)总代码规模
(2)类复杂度和方法复杂度
-
类复杂度
其中MyNetWork
复杂度较高,原因是该类含有所有指令对应的方法。
-
方法复杂度
(仅截取标红部分)
其中与消息发送有关的方法复杂度较高,因为消息发送需要修改较多类的属性。
此单元的类和方法数都较多,但是由于使用了JML规格进行规范,我们可以很明显地感受到类和方法的复杂度都有所下降。
三、代码出现的性能问题和修复情况
本单元作业涉及到图的查询,多重循环极其容易出现CPU超时问题。可能的解决方法为:设置缓存机制或者改进相关算法。
-
query_block_sum
:此方法如果使用双层循环并且调用isCircle方法,则复杂度较高,容易超时。解决方案如下://在MyNetwork中添加如下属性 private HashMap<Integer, PeopleAndSides> blocks; //连通分支 //在MyNetwork中动态维护该属性 public void addPerson(Person person) throws EqualPersonIdException { …… blocks.put(person.getId(), peopleAndSides); } public void addRelation(int id1, int id2, int value) throws PersonIdNotFoundException, EqualRelationException { 合并连通分支或者更新连通分支 } //query_block_sum public int queryBlockSum() { return blocks.size(); }
-
query_group_value_sum id(int)
和query_group_age_var id(int)
:这两个方法是查询组里的相关信息,可以在构建图的时候动态维护Group中的相关属性(需要注意的是,Group中除了addPerson和delPerson需要更新valueSum,若add_relation id(int) id(int) value(int)添加的边的两端点属于同一个Group,也需要更新该Group的valueSum) -
query_least_connection id(int)
:该方法是最小生成树算法,使用并查集可以快速确定连通分支的所有节点和边,省去了这一步复杂的查询过程。 -
send_indirect_message id(int)
:该方法是最短路径算法,每更新一次节点到起始点的距离,都需要寻找未选中的节点中距离起始点最短的节点,这个过程是耗时最大的部分。此处可以使用课程组推荐的堆优化的Dijkstra方法来避免CPU超时。
四、NetWork的扩展
1、与添加对象有关的接口
-
在MyNetWork中设置Advertiser数组、Producer数组、Customer数组和Person数组,在addPerson中需要根据Person种类的不同对相关数组进行一定的修改
-
addProduct(Product product):添加产品
/*@ public instance model non_null Product[] products;*/ /*@ public normal_behavior @ requires !(\exists int i; 0 <= i && i < products.length; products[i].equals(product)); @ assignable products; @ ensures products.length == \old(products.length) + 1; @ ensures (\forall int i; 0 <= i && i < \old(products.length); @ (\exists int j; 0 <= j && j < products.length; products[j] == (\old(products[i])))); @ ensures (\exists int i; 0 <= i && i < products.length; products[i] == product); @ also @ public exceptional_behavior @ signals (EqualProductIdException e) (\exists int i; 0 <= i && i < products.length; @ products[i].equals(product)); @*/ public void addProduct(/*@ non_null @*/Product product) throws EqualProductIdException;
-
addAdvertisement(Advertisement advertisement, int id):添加广告,与添加产品的方法类似。但是需要注意广告宣传的产品是否存在
2、与建立对象之间的联系有关的接口
-
ProducerMakeProduct(int id1, int id2):产品生产商生产某种产品
/*@ public instance model non_null Producer[] producers;*/ /*@ public instance model non_null Product[] products;*/ /*@ public normal_behavior @ requires (\exists int i; 0 <= i && i < producers.length; producers[i].getId() == id1); @ requires (\exists int j; 0 <= j && j < products.length; products[j].getId() == id2); @ requires !getProducer(id1).hasProduct(getProduct(id2)); @ assignable producers; @ ensures producers.length == \old(producers.length); @ ensures (\forall int i; 0 <= i && i < \old(producers.length); @ (\exists int j; 0 <= j && j < producers.length; producers[j] == \old(producers[i]))); @ ensures (\forall int i; 0 <= i && i < producers.length && \old(producers[i].getId()) != id1; @ \not_assigned(producers[i])); @ ensures (\forall int i; 0 <= i && i < \old(getProducer(id1).products.length); @ \old(getProducer(id1).products[i]) == getProducer(id1).products[i]); @ ensures getProducer(id1).hasProduct(getProduct(id2)); @ ensures \old(getProducer(id1).products.length) == getProducer(id1).products.length - 1; @ also @ public exceptional_behavior @ assignable \nothing; @ requires !(\exists int i; 0 <= i && i < producers.length; producers[i].getId() == id1) @ || !(\exists int j; 0 <= j && j < products.length; products[j].getId() == id2) @ || getProducer(id1).hasProduct(getProduct(id2)); @ signals (ProducerIdNotFoundException e) @ !(\exists int i; 0 <= i && i < producers.length; producers[i].getId() == id1); @ signals (ProductIdNotFoundException e) @ (\exists int i; 0 <= i && i < producers.length; producers[i].getId() == id1) @ && !(\exists int j; 0 <= j && j < products.length; products[j].getId() == id2); @ signals (EqualProductException e) @ (\exists int i; 0 <= i && i < producers.length; producers[i].getId() == id1) @ && (\exists int j; 0 <= j && j < products.length; products[j].getId() == id2) @ && getProducer(id1).hasProduct(getProduct(id2)); @*/ public void ProducerMakeProduct(int id1, int id2) throws ProducerIdNotFoundException, ProductIdNotFoundException, EqualProductException;
-
arrangeAdvertisement(int id, int id1, int id2):Producer给Advertiser分派"发放产品广告"的任务(某个广告与一个产品有关,需要注意Producer是否生产该产品)
-
pullCustomer(int id1, int id2):Advertiser拉Customer,即Advertiser与Customer建立联系
3、与行为有关的接口
-
advertise(int id1, int id2):Advertiser发送某个广告给其所有的Customer。为了降低复杂度,不设置Customer收到的广告(与本单元作业中群发消息的处理方案一致)
/*@ public instance model non_null Advertiser[] advertisers;*/ /*@ public normal_behavior @ requires (\exists int i; 0 <= i && i < advertisers.length; advertisers[i].getId() == id1); @ requires getAdvertiser(id1).hasAdvertisement(id2); @ assignable getAdvertiser(id1).getAdvertisement(id2); @ ensures getAdvertiser(id1).advs.length == \old(getAdvertiser(id1).advs.length); @ ensures (\forall int i; 0 <= i && i < \old(getAdvertiser(id1).advs.length); @ (\exists int j; 0 <= j && j < getAdvertiser(id1).advs.length; @ getAdvertiser(id1).advs[j] == \old(getAdvertiser(id1).advs[i]))); @ ensures (\forall int i; 0 <= i && i < getAdvertiser(id1).advs.length @ && \old(getAdvertiser(id1).advs[i].getId()) != id2; @ \not_assigned(getAdvertiser(id1).advs[i])); @ ensures getAdvertiser(id1).queryAdvertisementSend(id2) == true; @ also @ public exceptional_behavior @ assignable \nothing; @ requires !(\exists int i; 0 <= i && i < advertisers.length; advertisers[i].getId() == id1) @ || !getAdvertiser(id1).hasAdvertisement(id2); @ signals (AdvertiserIdNotFoundException e) @ !(\exists int i; 0 <= i && i < advertisers.length; advertisers[i].getId() == id1); @ signals (AdvertisementIdNotFoundException e) @ (\exists int i; 0 <= i && i < advertisers.length; advertisers[i].getId() == id1) @ && !getAdvertiser(id1).hasAdvertisement(id2); @*/ public void advertise(int id1, int id2) throws AdvertiserIdNotFoundException, AdvertisementIdNotFoundException;
-
buyProduct(int id, int id1, int id2):Customer通过Advertiser购买某种广告推销的产品,需要判断Customer是否为Advertiser的客户,且Advertiser是否有该广告且该广告是否发送过(为了降低方法的复杂度,不需要判断Customer是否收到过该广告)
五、心得体会
通过本单元的学习,我了解到了图的相关算法,并在实现相关算法的过程中学会了处理相关的性能问题,初步尝试了图的构建和维护。
同时,我接触到了JML规格以及JML规格化设计,学会了阅读JML规格编写代码,也初步尝试了自己编写JML规格。将JML规格利用好可以帮助我们进行模块化编程,让我们清楚地了解到每个方法的前置条件和后置条件、对象的不变式等关键信息;同时由于JML规格复杂度的限制,我们的方法复杂度能够控制在一定的范围内,在一定程度上降低方法的复杂度。这些都是有利于我们正确编程的。
JML规格是非常严谨的,因此阅读JML时需要非常细心,要弄清楚括号的匹配关系,要注意\old等细节处理。而编写JML规格则需要我们更加细致,将各个细节考虑全面并描述清楚。JML的知识博大精深,还有很多东西可以探索,希望今后能够不断学习JML的相关知识,更好地进行规格化设计,提高架构设计能力和编程能力。