OO第三单元总结

一、架构设计

1.对于社交网络模型的理解

1.1 Person:

person是社交网络模型中最基本的元素,每一个person相当于图中的一个个结点,他们存储了各种相关的信息。person也是我们操作时的基本对象,他们之间的交互、关系的连接、互相发送消息的行为,都是我们用来构成社交网络的基本元素。

1.2 Group:

group相当于是一群person的集合,person可以被加入其中,也可以从中被删去。但Group并非简单的将一群person囊括其中,它可以为person提供新的功能。拿我们平常用的最多的微信举例:用户与用户之间只能够聊天、收发红包、点对点传输文件等,但是我们在微信群中可以发布群通知、发起群收款、共享群文件等,这是group给person带来的行为上的扩展。

对于社交网络来说,群是比person高一级的抽象存在,它由元素形成了集合,并赋予其中的元素更多的性质。另外一点值得注意的是,person可以归属于不同的group,就像我们可以同时存在于OO和OS课程群一样,二者是互不影响的。它很类似于一种性质的概括,person可以有很多不同的性质,比如一个计算机系的同学,既需要上OO课,也需要上OS课,所以他就需要同时加入这两个课程群。

1.3 Network:

Network就是一张图,类似于一个平台,在其中可以存在数量众多的people,不论他们是否属于一个或者几个group,他们都归属于这个Network。继续沿用上述微信的例子,我们定义的Network实际上就类似于微信这个平台本身,他管理着groups和people,并且能从上帝视角来观看整个社交网络本身,我们可以查询某个group中有多少对象、两个person之间究竟能不能通过现有的好友产生联系、整个网络中存在多少个连通图、整个网络的最小连通图是什么等信息。在我们构建Network的时候,实际上站在了一个网站或者平台的构建者角度,来宏观的看我们管理着的一个个元素(person)和集合(group)。

2.图模型及对应的算法分析

2.1 union-find

并查集本质上是个树形结构,但是它比一般的树优越的地方在于其查找根节点的速度要快的多。由于这个优势,它的应用场景主要是:只查询两点间的连通性而不关心两点之间的具体路径。

我的实现方法是先构建一个抽象类,其中定义了并查集的所有方法(findunionisSame

public abstract class UnionFind {
   private HashMap<Integer, Integer> parents = new HashMap<>(); // key id value parents

   public HashMap<Integer, Integer> getParents() {
       return parents;
  }

   public boolean isSame(int v1, int v2) {
       return find(v1) == find(v2);
  }
//判断
   public abstract int find(int v);
   //查
   public abstract void union(int v1, int v2);
   //并
}

然后再建一个类,继承抽象类UnionFind,并且根据选择的算法实现并查集。

在算法选择这个问题上,我一开始使用了quickfind,也就是加边的时候,自动将其所有子节点的父节点全部设为自己的父节点,这种方法看似简单且查找复杂度为O(1),但是实际执行起来Union的复杂度太高,而且有些不需要查找的结点,根本没必要压缩路径,所以后来我舍弃了这中方法,转而采用路径压缩算法:

public class UnionFindQF extends UnionFind {

   @Override
   public int find(int v) {
       if (v != getParents().get(v)) {
           getParents().put(v, find(getParents().get(v)));
      }
       return getParents().get(v);
  }

   @Override
   public void union(int v1, int v2) {
       int p1 = find(v1);
       int p2 = find(v2);
       if (p1 == p2) {
           return;
      }
       getParents().put(p1, p2);
  }
}

在并操作的时候,只是执行正常的加边,也就是把自己的父节点的父节点设为要并入的结点的父节点;在查操作的时候,再通过路径压缩递归地找到自己的根节点,并将其设为父节点。

2.2 Kruskal

Kruskal算法就是很常规的算法,查找一个最小生成树。在具体实现queryLeastConnection的过程中,看那个JML感觉是最难的,知道了它要干什么之后反倒难度就不剩下多少了。我的实现方法是先遍历结点,得到存储了与id关联的所有vertex的ArrayList,然后再读取所有的vertex之间对应的边,放入ArrayList里,传递给用来实现算法的函数。

private int kruskal(ArrayList<Integer> vertex, ArrayList<Edge> edges) {             
   UnionFindQF unionFindQF = new UnionFindQF();
   int result = 0;
   int count = 0;
   for (Integer integer : vertex) {
       unionFindQF.getParents().put(integer, integer);
  }
   Collections.sort(edges);
   for (int i = 0; i < edges.size() && count < vertex.size() - 1; i++) {
       int id1 = edges.get(i).getId1();
       int id2 = edges.get(i).getId2();
       if (!unionFindQF.isSame(id1, id2)) {
           unionFindQF.union(id1, id2);
           result += edges.get(i).getValue();
           count++;
      }
  }
   return result;
}

由于上面实现了并查集,所以在实现kruskal的时候很自然就使用了并查集。kruskal里最复杂的其实就是判断加边后是否会产生圈,而产生圈的条件就是,这两个点在一个连通图中,这就很容易想到要用并查集来判断。向isSame方法传入边的两顶点id,如果说返回true,那么说明加了边会产生圈,因此这条边不能加,反之则可以。

2.3 Dijkstra

这里由于当时完成作业是在仓促,我觉得自己写的方法并不怎么样,是自己看着算法描述随手写的,最后强测的两个点被卡超时1点几秒,可以说是是在不太好。有同学提出可以用tarjan来优化,但是我还没来得及深入研究,之后会尝试去做一下优化。具体代码:

private int dijkstra(int id1, int id2) {
   HashMap<Integer, Integer> distance = new HashMap<>();
   HashMap<Integer, Boolean> isDetermined = new HashMap<>();
   distance.put(id1, 0);
   isDetermined.put(id1, true);
   HashMap<Integer, Integer> value = ((MyPerson)getPerson(id1)).getValue();
   ArrayList<Integer> remain = new ArrayList<>();
   for (Integer id : people.keySet()) {
       if (id != id1) {
           remain.add(id);
      }
  }
   int curPersonId = id1;
   int min = 0;
   while (isDetermined.get(id2) == null) {
       for (int id : remain) {
           if (value.get(id) != null) {
               if (distance.get(id) == null ||
                       distance.get(id) > value.get(id) + distance.get(curPersonId)) {
                   distance.put(id, value.get(id) + distance.get(curPersonId));
              }
          }
      }
       min = 0;
       for (int id : remain) {
           if (distance.get(id) != null) {
               if (min == 0) {
                   min = distance.get(id);
                   curPersonId = id;
              } else if (distance.get(id) < min) {
                   min = distance.get(id);
                   curPersonId = id;
              }
          }
      }
       remain.remove((Integer) curPersonId);
       isDetermined.put(curPersonId, true);
       value = ((MyPerson)getPerson(curPersonId)).getValue();
  }
   return distance.get(id2);
}

二、遇到的问题与修复情况

1.图算法相关问题

1.1 union-find

第一次作业的bug出在union-find上,因为是后来改用的路径压缩算法,最后写的有点仓促,在合并操作的时候出了bug。

int p1 = find(v1);
int p2 = find(v2);
if (p1 == p2) {
   return;
}
getParents().put(p1, p2);

应该是先用find操作找到v1,v2的根结点,然后把其中一个挂到另一个结点上面。但是我当时手残写成了:

getParents().put(v1,v2);

1.2 dijkstra

本次的bug是算法的效率问题,强测有两个点超时了,算法本身应该没出bug。

2.关于JML理解上出现的问题

2.1 getAgeMean & getAgeVar

对于getAgeMean的JML

    /*@ ensures \result == (people.length == 0? 0:
    @         ((\sum int i; 0 <= i && i < people.length; people[i].getAge()) / people.length));
    @*/

漏看了一个括号,我一开始的理解是把每一个person的age先除length,然后再求和。

如果按照这样理解的话,JML规格就变成了:

(\sum int i; 0 <= i && i < people.length; people[i].getAge() / people.length)

而getAgeMean的错误也就导致了getAgeVar的连锁错误。

3.修复情况

3.1 union-find

由于bug很简单,所以看了一下样例后,简单分析了一下,很快就修复了。

3.2 dijkstra

未修复这两个点,暂时没去研究更好的算法,尝试了在本身的基础上改了改,过了几个点,但是最后两个始终过不去。

3.3 getAgeMean & getAgeVar

修复的过程中,我一开始以为是精度的问题,就用了math等等方法,但是始终不行,到最后才想起来看JML,然后发现是自己对于规格的理解出了问题。这次悲催的debug经历也告诉了我规格的重要性,一旦出锅,只要看看JML,再对照JML看自己的设计,只要都正确,那就不会出问题,自己盯着自己的代码死想是很难分析出问题的。JML相当于帮我们翻译了题意,只有按照题意来设计,才能够得到符合要求的程序。

三、测试思路

我的基本测试思路是大量随机数据加少量构造的边界数据。

通过编写程序自动生成随机测试数据,只要代码量够大,指令覆盖够全,对于程序测试就有价值。一般的bug,比如对JML的理解有问题导致程序行为和正确的不符,这样的bug在大量测试数据下是很难逃过去的。正确行为的判断,我选择用对拍的方法,多找几个同学,大家的代码一起跑一跑测试,然后用程序进行对比,如果出现不同结果,就要小心检查对应部分的代码了,肯定是有人有bug的。

构造测试数据针对的是JML规定的边界情况以及题目中预设的一些边界条件。

比如Group中计算平均年龄的时候,出现了Group中没有人的情况。再比如Group添加超过规定了人数上限的情况。

四、额外架构扩展

1. 要求

假设出现了几种不同的Person

  • Advertiser:持续向外发送产品广告

  • Producer:产品生产商,通过Advertiser来销售产品

  • Customer:消费者,会关注广告并选择和自己偏好匹配的产品来购买 -- 所谓购买,就是直接通过Advertiser给相应Producer发一个购买消息

  • Person:吃瓜群众,不发广告,不买东西,不卖东西

如此Network可以支持市场营销,并能查询某种商品的销售额和销售路径等 请讨论如何对Network扩展,给出相关接口方法,并选择3个核心业务功能的接口方法撰写JML规格(借鉴所总结的JML规格模式)

2. JML设计

发广告: Advertiser->Customer 传递的对象是包含产品信息的广告

生产产品:Producer->product 每个生产者产出产品

消费产品:Customer->Advertiser->product 消费者经由Advertiser购买product

每个Producer都拥有一个List,其中存放的元素是Advertiser,表示为其打广告的Advertiser。

相关接口方法

//添加销售员
void addAdvertiser(Person advertiser) throws EqualPersonIdException;

//添加生产商
void addProducer(Person producer) throws EqualPersonIdException;

//添加消费者
void addCustomer(Person customer) throws EqualPersonIdException;

//Advertiser为产品打广告
void advertise(int cusId, int proId) throws PersonIdNotFoundException;

//顾客从Advertiser地方购买产品
void purchase(int cusId, int adId, int toSell) throws PersonIdNotFoundException;

//查询生产商的产品对应的销售额
int querySalesOfProduct(int proId) throws PersonIdNotFoundException;

//查询某生产商的销售路径
List<Advertiser> querySellingPath(int proId) throws PersonIdNotFoundException;

核心业务功能

//Advertiser为产品打广告
/*@public normal_behavior
   @requires contains(cusId) && getPerson(cusId) instanceof Advertiser && containsMessage(proId) && getMessage(proId) instanceof AdvertiseMessage;
   @assignable messages;
   @assinable getPerson(cusId).subscribers[].preference;
   @ensures \old(getPerson(cusId).subscribers) == getPerson(cusId).subscribers;
   @ensures !containsMessage(proId) && messages.length == \old(messages.length) - 1 &&    
   @         (\forall int i; 0 <= i && i < \old(messages.length) && \old(messages[i].getId()) != id;
   @         (\exists int j; 0 <= j && j < messages.length; messages[j].equals(\old(messages[i]))));  
   @ensures (\forall int i; 0 <= i && i < \old(getPerson(cusId).subscriber.length);
   @getPerson(cusId).subscriber[i].getPreference.length == \old(getPerson(cusId).subscriber[i].getPreference.length) + 1);
   @also
   @public exceptional_behavior
   @signals (MessageIdNotFoundException e) !containsMessage(id);
   @signals (PersonIdNotFoundException e) !contains(id1);
   */
public void advertise(int cusId, int proId) throws MessageIdNotFoundException, PersonIdNotFoundException

//顾客向销售员发送购买消息
/*@ public normal_behavior
 @ requires contains(cusId) && getPerson(cusId) instanceof Customer;
 @ requires (\exist int i; 0 <= i && i < people.length; people[i] instanceof Advertiser &&
          (\exist int j; 0 <= j && j < ((Advertiser)people[i]).getProducts.size(); ((Advertiser)people[i]).getProducts.get(j).getId == proId
        && ((Advertiser)people[i]).getProducts.get(j).getCount - toSell >= 0));
 @ assignable people[*];
 @ ensures Product toBuy == (\exist int i; 0 <= i && i < people.length; people[i] instanceof Advertiser &&
        (\exist int j; 0 <= j && j < ((Advertiser)people[i]).getProducts.size(); ((Advertiser)people[i]).getProducts.get(j).getId == proId));
 @ ensures getPerson(cusId).getProducts.size() == \old(getPerson(cusId)).getProducts.size() + 1;
 @ ensures (\forall int i; 0 <= i && i < \old(getPerson(cusId)).getProducts.size();
    (\exist int j; 0 <= j && j < getPerson(cusId).getProducts.size(); getPerson(cusId).getProducts.get(j)   == \old(getPerson(cusId)).getProducts.get(i)));
 @ ensures getPerson(cusId).getProducts.getLast() == new Product(tuBuy, toBuy.getCount - toSell, getPerson(cusId));
 @ ensures getPerson(cusId).getProducts.getLast().getPath().get(0).getMapSold.get(proId) == \old(getPerson(cusId).getProducts.getLast().getPath().get(0)).getMapSold.get(proId) + toSell;
 @ ensures toBuy.getCount == \old(toBuy).getCount - toSell;
 @ also
 @ public exceptional_behavior
 @ assignable \nothing;
 @ signals (PersonIdNotFoundException e) !(contains(cusId) && getPerson(csuId) instanceof Customer);
 @ signals (ProductNotfoundException e) (contains(cusId) && getPerson(csuId) instanceof Customer) && !(\exist int i; 0 <= i && i < people.length; people[i] instanceof Advertiser &&
        (\exist int j; 0 <= j && j < ((Advertiser)people[i]).getProducts.size(); ((Advertiser)people[i]).getProducts.get(j).getId == proId));
 @ signals (ProductSoldOutException e) (contains(cusId) && getPerson(csuId) instanceof Customer) && (\exist int i; 0 <= i && i < people.length; people[i] instanceof Advertiser &&
      (\exist int j; 0 <= j && j < ((Advertiser)people[i]).getProducts.size(); ((Advertiser)people[i]).getProducts.get(j).getId == proId
       && ((Advertiser)people[i]).getProducts.get(j).getCount - toSell < 0));
*/
public void purchase(int cusId, int proId, int toSell) throws PersonIdNotFoundException;

//查询销售路径
/*@ public normal_behavior
 @ requires contains(proId) && getPerson(proId) instanceof Producer;
 @ ensures (\result).containsAll(producer.advertisers) &&
            (\result).length == producer.advertisers.length;
 @ also
 @ public exceptional_behavior
 @ signals (PersonIdNotFoundException e) !contains(producer.getId());
 @*/
List<Advertiser> /*@ pure @*/ querySellingPath(int proId) throws PersonIdNotFoundException;

五、本单元学习体会

本单元通过对JML的学习,掌握了如何对程序进行规格化设计。在对问题进行分析与设计阶段,我学会了用规格化设计来对我们的方案进行描述,然后通过设计实现我们的规格,来保证最后程序的正确性。不得不说,JML对于OOP程序设计是大有裨益的,尤其是代码量较大的工程,因为其本身的复杂性,阅读代码来找bug会为我们带来许多的困难。但是如果有了JML,我们就可以通过阅读它来分析我们的程序在设计上是否有问题,它实际上就是我们对问题的剖析以及对具体代码的抽象,是更符合我们人的思考习惯的。

另外,这单元的作业以社交网络为背景,让我们实现了一些与图论相关的算法,也算是对之前的知识进行了一次复习,还学到了并查集这样好用的数据结构,也算是额外收获。

posted @ 2022-06-06 15:48  KKbecomesbald  阅读(14)  评论(0编辑  收藏  举报