摘要:本单元的人物社交网络构建作业,聚焦于JML规格阅读与实现,让我对于工程代码的严谨性有了深入的理解,也强化了我对于异常处理、抛出与接收的理解。

 

1、设计构造:
由于本单元的主要内容为实现课程组提供的数个接口与抽象类,因此整体的思路非常简单:阅读JML规格并在此基础上做填空。
基本架构:见UML类图,以第三次作业为例

要想在MyNetwork中构建了整个社交网络的图模型,需要节点与边的信息。首先可以以Network中的每个人作为图的各个节点,之后,由于Person类采用了类似领接表的方式保存了其下每个节点的相邻节点acquaintance(也即边的关系),因此边的关系不需要再重复加入MyNetwork中。通过递归地访问图中的节点,我们就能在Network中还原出整个图的结构。

维护图通常考虑维护图的节点与边,由于本单元作业不会删除或者修改图的边与节点,故维护图只需要考虑在图中加入Person和Relation。这部分的操作遵从JML的指示即可。

本单元作业的基本架构较为简单,接下来将讨论一些具体的细节问题

 

2、实现细节与部分问题的处理

(1)分析在本单元自测过程中如何利用JML规格来准备测试数据

由于JML规格包含require、assignable和ensure语句,因此我们可以根据这些语句进行测试。
针对require的限定,可以尝试在测试数据中加入不符合require要求的指令。如果这个指令无法被对应的函数发现为不满足require需要,就证明函数功能不完善,需要进行修改;

针对assignable的限定,可以组合测试指令,先调用assignable不为\nothing的函数对数据进行修改,接下来再对函数可能修改的数据进行查询。若一些不包含在assignable的数据发生了改变,证明该函数的操作存在异常行为,需要修改;

针对ensure,更多的是考虑函数处理结果的正确性。这一部分可以用大量广泛的数据进行暴力测试,或者对几个复杂度较高的操作进行针对性测试。我在正确性测试中主要采用了后者,例如验证在图的结构改变前后,图的相关计算结果的正确性等等。

 

(2)梳理本单元的架构设计,分析自己的图模型构建和维护策略

 本次作业的基本架构已在上文中提及,但是为了进一步优化结构的合理性与运算效率,引入类Block,用以表示MyNetwork关系图中的若干个最大连通子图。在Block之中,像MyNetwork一样包含了最大连通子图的所有节点(即Person),并且由于Person的领接表属性,图的边信息也自然涵盖其中。

a、Block的访问、创建与维护

在类图中可以发现,MyPerson下维护了一个类型为Block的属性,也就是说我们通过访问MyPerson的方法getBlock,就能得到含有这个Person的Block的信息。当我们在向MyNetwork中输入addPerson指令后,处理增加网络成员的函数将创建这个人并将其加入MyNetwork的函数中,与此同时也会生成一个新的Block赋给这个人(显然此时这个新Block只有这个人自己),可见这一步完成了一个Block实例的创建。

而当增加关系时,意味着点与点之间的连通属性将发生改变,此时访问这个新关系双方下的Block,有两种可能情况:

  • 若两人的Block相同,意味着这两个人已经在同一个图中,新关系不会改变这个最大连通子图与其他最大连通子图的关系,只需要正常增加关系就好; 
  • 若两人的Block不同,则两人在不同连通图中,这一步操作后两个图将连通变成一个新的连通图。我的具体处理方法是将第二个人的Block(简称为block2)作为函数的参数,调用第一个人Block(简称为block1)下的mergeBlock函数,让两个不同的Block合并。mergeBlock函数会遍历Block2下的每一个节点,将节点的参数Block设置为block1,同时再把这个节点加入到block1中。最后,mergeBlock将设置block2为不可访问,以防止其他错误的发生。

b、Block的好处

  • 引入Block后,isCircle的查询就变得很简单了:只要看看要查询的两个人是否在同一个Block内即可,如果在同一个Block内,由图的连通性,一定存在一条路径连接第一个人和第二个人,如果不在那二者也一定不连通;
  • queryBlockSum这个查询也将变得很简单,遍历MyNetwork中的每个节点,计数一共有多少个不同的Block即可。当然这个方法时间复杂度为O(n),更好的方法是给MyNetwork引入属性qbsNum,专门存放这个网络下拥有的最大连通子图数量,并且这个属性与Block一起维护,在加新人时值加一,在加关系且连通两个本不相连的子图时减一,这样查询时直接输出qbsAns的值,复杂度将直接降为O(1);
  • 由连通图的性质可知,查询最小生成树的计算显然发生在Block中,而且这个树对每一Block唯一,因此我们可以在Block没有更新(即Block内没有引入新的成员和关系)的情况下,仅进行一次计算,将值保存在Block的属性leastConnection下。之后再查询最小生成树权值和时,若该图没有更新就可以直接输出该值,避免进行多次的重复计算;
  • 在查询图的最短路径时,由于两点一定连通,可知这步计算也发生在Block中,与Network中其他节点没有联系,降低了遍历的总量。

因此Block具有的优点非常明显,处理效率更高并且对各种运算的集成度也更高。

 

(3)按照作业分析代码实现出现的性能问题和修复情况

不幸的是,上述设计是在多次bug修改与反复迭代下才逐渐形成的,在每次作业的强测中都遇到了或多或少的性能问题

在第一次作业中,由于只考虑了简单实现JML规格而没有仔细思考运行效率上的问题,导致了queryBlockSum指令在每一次输入时都需要进行繁琐的计算。进行bug修复时,还未增加Block类,只是引入了类似的思想,即MyNetwork的qbsAns属性,以对最大连通子图数快速查询;

在第二次作业中,引入了Block类进行优化,然而在最小生成树的计算上,我由于此时没有定义边,而采用了prim算法计算,其效率较低,引发超时。在bug修复时,在Block类中引入新类Edge定义边,以此为基础即可采用更高效的kruskal算法完成最小生成树的计算;

在第三次作业中,求最短路径时考虑不周到,采用了未优化的dijkstra算法,显然又超时了。最后我对dijkstra算法进行堆优化,同时采用在找到目标节点后直接退出循环的方法,对bug进行修复。

 

3、Network拓展

在营销网络中,让新增的Advertiser、Producer、Customer继承MyPerson,并且都管理属性Product产品。

  • 对于Product类,主要维护数个基本信息:产品编号id:int产品数量count:int产品单价price:int产品的销售路径path:ArrayList<Person>。假定产品销售流程中Producer、Advertiser和Customer都只经过一级,则查询path.size()可以得出产品的营销状态:size==0,产品未制造;size == 1,产品刚刚制造完毕; size == 2,产品已分发给Advertiser; size == 3,产品已被Customer购买。对于已经被消费者购买的产品来说,查询每一级的营销成员也十分简单,path.get(0)可得到生产者信息;path.get(1)可得到推销员的信息;path.get(2)则得到消费者自己的信息. 并且为Product定义一个特殊构造方法Product(Product product, int nowCount, Person person),用于在复制原本product信息的基础上,新的Product实例修改count为nowCount,且传播链条上新增成员person(需判断person的instanceof与其所对应序号是否匹配,例如person增加在一号位时需要为Advertiser,错误则抛出异常)。
  • 对于Advertiser和Customer,多增加一个属性products:ArrayList<Product>,管理自身需要推销或是已购买的产品集;
  • 对于Producer,除了与Advertiser和Customer相同的产品集,还需要对不同id的产品基本信息进行管理。需要用两个HashMap<Interger,Interger>来建立产品id分别与产品单价和产品销量的对应关系,分别为mapPricemapSold,在产品生产结束后维护两个HashMap,进行mapPrice.put(product.getId, product.getPrice)和mapSold.put(product.getId, 0)两个操作。

下面是部分营销过程在MyNetwork中的JML规格

 

(1)生产者将产品分发给广告商进行销售:

 1 /*
 2 @ public normal_behavior
 3 @ requires contains(advId) && getPerson(advId) instanceof Advertiser && contains(pduId) && getPerson(pduId) instanceof Producer;
 4 @ requires (\exists int i; 0 <= i && i < getPerson(pduId).getProducts.size(); products[i].equals(product) && products[i].getCount - toSell >= 0);
 5 @ assignable people[*], product;
 6 @ ensures getPerson(advId).getProducts.size() == \old(getPerson(advId)).getProducts.size() + 1;
 7 @ ensures (\forall int i; 0 <= i && i < \old(getPerson(advId)).getProducts.size(); 
 8     (\exist int j; 0 <= j && j < getPerson(advId).getProducts.size(); getPerson(advId).getProducts.get(j) == \old(getPerson(advId)).getProducts.get(i)));
 9 @ ensures getPerson(advId).getProducts.get(getPerson(advId).getProducts.size() - 1) == new Product(product, product.getCount - toSell, getPerson(advId));
10 @ ensures (\exists int i; 0 <= i && i < getPerson(pduId).getProducts.size(); \old(products[i]).equals(product) && products[i].getCount == \old(products[i]).getCount - toSell);
11 @ also
12 @ public exceptional_behavior
13 @ assignable \nothing;
14 @ signals (PersonIdNotFoundException e) !(contains(advId) && getPerson(advId) instanceof Advertiser);
15 @ signals (PersonIdNotFoundException e)(contains(advId) && getPerson(advId) instanceof Advertiser) && !(contains(pduId) && getPerson(pduId) instanceof Producer);
16 @ signals (ProductNotfoundException e) contains(advId) && getPerson(advId) instanceof Advertiser && contains(pduId) && getPerson(pduId) instanceof Producer && 
17                 !(\exists int i; 0 <= i && i < getPerson(pduId).getProducts.size(); products[i].equals(product))
18 @ signals (ProductSoldOutException e) contains(advId) && getPerson(advId) instanceof Advertiser && contains(pduId) && getPerson(pduId) instanceof Producer && 
19                 (\exists int i; 0 <= i && i < getPerson(pduId).getProducts.size(); products[i].equals(product) && products[i].getCount - toSell < 0);
20 */
21 public void pushToAdvertiser(int pduId, int advId, Product product, int toSell) throws PersonIdNotFoundException, ProductNotfoundException, ProductSoldOutException;

 

(2)消费者根据自身喜好(喜好的产品其id为proId)从广告商的推销处购买产品:

 1 /*
 2 @ public normal_behavior
 3 @ requires contains(cusId) && getPerson(cusId) instanceof Customer;
 4 @ requires (\exist int i; 0 <= i && i < people.length; people[i] instanceof Advertiser && 
 5         (\exist int j; 0 <= j && j < ((Advertiser)people[i]).getProducts.size(); ((Advertiser)people[i]).getProducts.get(j).getId == proId 
 6         && ((Advertiser)people[i]).getProducts.get(j).getCount - toSell >= 0));
 7 @ assignable people[*];
 8 @ ensures Product toBuy == (\exist int i; 0 <= i && i < people.length; people[i] instanceof Advertiser && 
 9         (\exist int j; 0 <= j && j < ((Advertiser)people[i]).getProducts.size(); ((Advertiser)people[i]).getProducts.get(j).getId == proId));
10 @ ensures getPerson(cusId).getProducts.size() == \old(getPerson(cusId)).getProducts.size() + 1;
11 @ ensures (\forall int i; 0 <= i && i < \old(getPerson(cusId)).getProducts.size(); 
12     (\exist int j; 0 <= j && j < getPerson(cusId).getProducts.size(); getPerson(cusId).getProducts.get(j) == \old(getPerson(cusId)).getProducts.get(i)));
13 @ ensures getPerson(cusId).getProducts.getLast() == new Product(tuBuy, toBuy.getCount - toSell, getPerson(cusId));
14 @ ensures getPerson(cusId).getProducts.getLast().getPath().get(0).getMapSold.get(proId) == \old(getPerson(cusId).getProducts.getLast().getPath().get(0)).getMapSold.get(proId) + toSell;
15 @ ensures toBuy.getCount == \old(toBuy).getCount - toSell;
16 @ also
17 @ public exceptional_behavior
18 @ assignable \nothing;
19 @ signals (PersonIdNotFoundException e) !(contains(cusId) && getPerson(csuId) instanceof Customer);
20 @ signals (ProductNotfoundException e) (contains(cusId) && getPerson(csuId) instanceof Customer) && !(\exist int i; 0 <= i && i < people.length; people[i] instanceof Advertiser && 
21         (\exist int j; 0 <= j && j < ((Advertiser)people[i]).getProducts.size(); ((Advertiser)people[i]).getProducts.get(j).getId == proId));
22 @ signals (ProductSoldOutException e) (contains(cusId) && getPerson(csuId) instanceof Customer) && (\exist int i; 0 <= i && i < people.length; people[i] instanceof Advertiser && 
23         (\exist int j; 0 <= j && j < ((Advertiser)people[i]).getProducts.size(); ((Advertiser)people[i]).getProducts.get(j).getId == proId 
24         && ((Advertiser)people[i]).getProducts.get(j).getCount - toSell < 0));
25 */
26 public void buyFromAdvertiser(int cusId, int proId, int toSell) throws PersonIdNotFoundException, ProductNotfoundException, ProductSoldOutException;

 

(3)某生产者查询自己某种产品的总销售额

 1 /*
 2 @ public normal_behavior
 3 @ requires contains(pduId) && getPerson(pduId) instanceof Producer;
 4 @ requires ((Producer)getPerson(pduId)).getMapPrice.get(proId) != null;
 5 @ assignable \nothing;
 6 @ ensures \result == ((Producer)getPerson(pduId)).getMapPrice.get(proId) * ((Producer)getPerson(pduId)).getMapSold.get(proId);
 7 @ also
 8 @ public exceptional_behavior
 9 @ assignable \nothing;
10 @ signals (PersonIdNotFoundException e) !(contains(pduId) && getPerson(pduId) instanceof Producer);
11 @ signals (ProductNotfoundException e) ((Producer)getPerson(pduId)).getMapPrice.get(proId) == null;
12 */
13 public /*@ pure @*/ int queryTotalSale(int pduId, int proId) throws PersonIdNotFoundException, ProductNotfoundException;

 

4、hack策略

本单元依旧仅仅简单参与hack,基本策略是

(1)一边更新Network的图结构一边查询,成功hack到了几名同学;

(2)可以针对低效算法,采用大数据量轰炸的方式暴力hack。

 

5、bug修复

除了之前提及的低效算法导致的bug以外,在第三次作业中还出现了某人群发红包时,他自己也收了一份红包的bug。这个bug较容易修改。

 

6、学习体会

在JML规格的指导下,撰写一个方法变得十分简单且合理严谨,这充分说明了“契约式编程”在工程上具有着逻辑严密清晰、可靠性强、可移植性好等优点,为开发人员编写大型程序提供了非常大的帮助,减少了代码撰写人的思考负担,提高了编码的效率。当然,对我来说更大的体会在于有JML规格的情况下,撰写代码并不是像机器一样将规格中的内容“复刻”出来,更关键的是理解JML规格的各种约束,并在此基础上思考效率最高的处理方法。

本单元再次带我复习了图论的有关知识,使我对其中所涉及的各种算法有了更深入的理解,也让我对图算法效率的重要性有了更深刻的认识。