面向对象设计与构造 第三单元总结
第三单元博客作业
针对第三单元的三次作业和课程内容,撰写技术博客
(1) 分析在本单元自测过程中如何利用JML规格来准备测试数据
(2) 梳理本单元的架构设计,分析自己的图模型构建和维护策略
(3) 按照作业分析代码实现出现的性能问题和修复情况
(4) 请针对下页ppt内容对Network进行扩展,并给出相应的JML规格
(5) 本单元学习体会
根据JML撰写代码以及测试的一些心得
JML在我看来实际上就是一套具有精确性的语言描述,通过JML规格,可以避免用自然语言描述代码功能需求时产生的模糊性,进而可以成为代码需求方与代码实现方之间一种可靠的交流媒介。JML通过设置前置条件与后置条件严格规定方法的输入域及其正确性定义,通过设置副作用范围限定控制方法的影响范围,正是这样的一种“协议”,从逻辑上对类或方法进行了封装(或者说底层实现与外部使用的隔离),使代码需求者和实现者得以专注于各自的问题域。
在根据JML实现代码时,我大致按照以下步骤进行:
-
根据方法名以及参数初步判断方法功能
-
根据normal_behavior与exceptional_behavior(如果有的话)中的前置条件确定方法的输入域及其划分
-
在方法体中根据输入域的不同子集进行相应的处理,令结果满足后置条件,注意副作用的控制以及不变式的要求
-
对上一步中的具体处理进行优化,包括数据结构优化、算法优化、利用缓存进行性能优化等等
测试上我采用随机数据对拍以及手动构造特殊数据相结合的方式,随机数据对拍在验证方法正确性上比较方便好用,但是不好测试性能,还是需要特殊构造数据进行补充。
关于构造数据的技巧:
正如前面提到的,JML将方法的输入域都做了划分,我们便可以利用这一点,通过保证测试数据覆盖所有划分来保证数据的覆盖性,这用来检查代码实现的遗漏或基本正确性比较有效;但是我个人觉得性能上的压力测试还是需要针对复杂度高的方法进行“重点轰炸”,这是出于数据强度的考虑。
三次作业的迭代架构
由于三次作业重点仅仅是迭代实现了一些交互和查询方法,故不再给出类图,这里先讨论一下基本的框架,很多性能相关的优化放在后面一起讲。
主体的MyNetwork类如其名需要我们需要维护一个图结构,用以管理社交信息,Person可以抽象为图中的点,Relation可以抽象为图中的边,Group可以抽象为点集。图中保存用户和群组等信息的属性很自然地使用以id为key的HashMap,保证根据id查询元素的复杂度为o(1)。另外,像在MyGroup中管理MyPerson,或MyPerson中管理acquaintance等情况,基本都是使用以id为key的HashMap,因为根据id查询元素的操作实在是很多。设置好这些HashMap后,实际上就建立起了一个相互联系的图结构。
各个类中所要实现的方法,可以分为交互方法以及查询方法,其中查询方法没有副作用。外界输入实际上是对MyNetwork的一个实例进行操作,实现的重点是根据JML中交互方法的功能维护自己的数据结构。值得注意的是JML基本用list进行功能的表述,但是实际上实现的数据结构并不需要与其一致,应当根据需要实现的各类查询方法设计合适的数据结构以优化查询时的时间代价。
几个主要类的属性:
//MyNetwork
private HashMap<Integer, Person> people;
private HashMap<Integer, Group> groups;
private HashMap<Integer, Message> messages;
private HashMap<Integer, Integer> emojis;
private HashMap<Integer, HashSet<Integer>> emojiLog;
private int blockCnt;
private HashMap<Integer, Integer> buffer;
private int lastSrc;
private boolean available;
//MyGroup
private int id;
private HashMap<Integer, Person> people;
private BigInteger ageSum;
private BigInteger ageSum2;
private int valueSum;
//MyPerson
private int id;
private String name;
private int age;
private int socialValue;
private int money;
private Person father;
private LinkedList<Message> messages;
private HashMap<Integer, Person> acquaintance;
private HashMap<Integer, Integer> value;
另外提一下异常类的实现:
public class MyPersonIdNotFoundException extends PersonIdNotFoundException {
private static HashMap<Integer, Integer> COUNTER = new HashMap<>();
private static int SUM = 0;
private int id;
public MyPersonIdNotFoundException(int id) {
this.id = id;
if (!COUNTER.containsKey(id)) {
COUNTER.put(id, 1);
} else {
COUNTER.replace(id, COUNTER.get(id) + 1);
}
SUM += 1;
}
@Override
public void print() {
System.out.println("pinf-" + SUM + ", " + id + "-" + COUNTER.get(id));
}
}
由于要求维护某异常发生的总次数以及异常来源的触发次数,我选择在异常类中维护相应的static属性来进行存储。
一些性能问题与解决
个人感觉这个单元的作业中最需要重视的还是方法的时间优化,某种意义上所有方法的实际实现都要考虑这一点。第一次作业开始我就自然地使用了hashmap进行数据的管理,isCircle选择的是剪枝的DFS方法,强互测没发现问题,这在当时给了我作业对时间复杂度要求不高的错觉,导致我对第二次作业只进行了一点简单的优化,甚至还没使用并查集,导致强互测大翻车。第三次作业中貌似我最短路径的堆优化有些问题,还是超时了,不过其他方法都做了尽可能的优化,接下来对各个重点考察性能的查询指令和相关方法做一些讨论。
qci和qbs:分别查询图中两人是否可达(判断是否在同一连通分支)以及连通分支数。这两个指令通过建立并查集可以达到很高的效率,判断两者祖先是否相同即可完成qci,而连通分支数可以在加人和加边的时候进行维护(加入单独的人使分支数加1,因加边产生并查集中的祖先合并时使分支数减1),直接查询值即可。
qgvs和qgav:都是通过动态缓存数据来避免重复计算,在MyGroup加人时维护年龄和和年龄平方和,在加边时维护边的总权值(注意边是双向的,要乘2),这样查询操作都简化成了o(1)
qlc:求某人所在分支的最小生成树总长。选择堆优化的prim算法即可,复杂度o(nlogn)。
dce:考虑到直接根据要删除的emoji的id查找含有该emoji的message需要遍历messages代价很高,故在加入含有emoji的message时就维护一个emoji的id和含有该emoji的message的id集合的对应表,同时别忘了在发送message后也从这个集合中删除(我第三次作业就犯了这个错导致互测被hack),这样就可以避免遍历所有message。
sim:实际上就是求两人间的最短路径长度。选择堆优化的迪杰斯特拉算法即可,复杂度o(nlogn)。另外考虑到查询往往密集且类似,我对最短路径算法的结果做了缓存,保证在不改变图结构的情况下多次查询同源的最短路径只需计算一次。
总的来说,优化时间代价的方法首先是根据操作的契合度选择合适的数据结构,同时尽量动态维护有关数据,并缓存起来,避免每次查询要重复高时间复杂度的过程。
对Network的扩展
假设出现了几种不同的Person
- Advertiser:持续向外发送产品广告
- Producer:产品生产商,通过Advertiser来销售产品
- Customer:消费者,会关注广告并选择和自己偏好匹配的产品来购买
-- 所谓购买,就是直接通过Advertiser给相应Producer发一个购买消息 - Person:吃瓜群众,不发广告,不买东西,不卖东西
如此Network可以支持市场营销,并能查询某种商品的销售额和销售路径等
请讨论如何对Network扩展,给出相关接口方法,并选择3个核心业务功能的接口方法撰写JML规格(借鉴所总结的JML规格模式)
Advertiser[] advertisers;
Producer[] producers;
Customer[] customers;
Product[] products;
//生产商雇佣广告商
public void hire(Producer producer, Advertiser advertiser);
//消费者关注广告商
public void subscribe(Customer customer, Advertiser advertiser);
//消费者购买收到的广告中符合自身偏好的产品
public void purchase(Customer customer);
//生产商品
/*@ public normal_behavior
@ requires (\exists int i; i>=0 && i<producers.length; producers[i] == producer);
@ assignable products, producer.products;
@ ensures products.length = \old(products.length) + 1;
@ ensures (\exists int i; i>=0 && i<products.length; products[i] == \old(producer.produce()));
@ ensures (\forall int i; i>=0 && i<\old(products.length); (\exists int j; j>=0 && j<products.length; @ products[j] == \old(products[i])));
@ ensures producer.products.length = \old(producer.products.length) + 1;
@ ensures (\exists int i; i>=0 && i<producer.products.length; producer.products[i] ==
@ \old(producer.produce()));
@ ensures (\forall int i; i>=0 && i<\old(producer.products.length); (\exists int j; j>=0 &&
@ j<producer.products.length; producer.products[j] == \old(producer.products[i])));
@ also
@ public exceptional_behavior
@ signals (ProducerNotFoundException e) !(\exists int i; i>=0 && i<producers.length; producers[i] ==
@ producer);
@*/
public void produce(Producer producer);
//为商品发送广告
/*@ public normal_behavior
@ requires (\exists int i; i>=0 && i<products.length; products[i] == product) &&
@ product.producer.advertisers.length > 0;
@ assignable customers[*].productsKnown;
@ ensures (\forall int i; i>= 0 && i<customers.length;
@ (\exists int j; j>=0 && j<product.producer.advertisers.length;
@ customers[i].knowsAdvertiser(product.producer.advertisers[j]))==>customers[i].knowsProduct(product));
@ also
@ public exceptional_behavior
@ signals (ProductNotFoundException e) !(\exists int i; i>=0 && i<products.length; products[i] ==
@ product);
@ signals (NoAdvertiserAvailableException e) (\exists int i; i>=0 && i<products.length; products[i] ==
@ product) && product.producer.advertisers.length == 0;
@*/
public void advertise(Product product);
//查询商品销售额
/*@ public normal_behavior
@ requires (\exists int i; i>=0 && i<products.length; products[i] == product);
@ ensures \result == product.getSellCount();
@ also
@ public exceptional_behavior
@ signals (ProductNotFoundException e) !(\exists int i; i>=0 && i<products.length; products[i] ==
@ product);
@*/
public /*@ pure @*/ int getSellCount(Product product);
...
一些体会
本单元的主题是JML规格,我们学习了一种契约化编程的基本方法。我认为在工程实践中这种规范是十分重要的,通过对代码功能进行精确化的约束,可以避免许多由于误解或者理解混乱带来的效率低下。另外,我在作业的迭代中逐渐发现,这种约束对代码实现者来说实际上非但不是限制而是一种解放:由于JML的隔离,代码实现的目标清晰,反而可以更加灵活。例如在我们的作业中,功能的正确性JML已经描述得十分清楚了,所以代码实现上可以专注于性能的优化。
不过总感觉作业设计上JML的重要性貌似被算法和数据结构的优化方法给盖过了,希望以后能做一些改变。