OO-第三单元总结
概述
本单元的任务是根据JML描述实现要求的接口,以完成一个社交网络图的模拟,包括添加节点(Person),添加边(add relation),查询连通性和变得权值等。在几次迭代开发的过程中还会加入构建最小生成树和查询最短路径的功能。本单元的主要聚焦于JML规格的理解和一些基础图算法的编码应用。
基于JML的测试方法
JML通过严谨的形式化语言描述方法为我们需要实现的接口给出了规格定义,说明了这些方法要满足的功能和在此过程中受到的限制。这就使得可以通过JML来设计单元测试的数据。
执行方法的前置条件
JML中的requires
关键字描述了方法执行前的状况要求,同时,不同的前置条件可能使方法导向不同的行为,甚至包括报错。因此在设计测试数据时要遍历全部可能的执行前状态,不留遗漏的进行测试。我本人就曾经在写代码过程中漏看了一条异常状态的JML,但是在测试过程中被找出来了。
方法的正确性判断
这一部分是最关键的,理论上方法的正确性和次生影响都是通过ensures
判断的,但这里感觉还是要进行一定区分。对于方法的正确性测试,感觉似乎并没有一个形式化的直接判定手段,还是需要仔细阅读JML,根据自己的理解计算正确结果。当然,如果相应的JML不是特别复杂的话可以考虑直接按照JML的描述方法写一个性能很差的对拍程序。
方法的次生影响
方法的执行导致了对象状态的改变,然而在方法运行结束后还需要判定当前的改变是否在JML限定的范围内,是否造成了多余的副作用,这也需要进行检查。这一部分的检查既包括一部分ensures
字段,还有定义在类中的invariant
字段。要保证方法在执行前后不对不该做出改变的部分做出改变。
方法的性能
众所周知,JML仅仅是定义了方法应该干什么,而没有定义怎么干,因此JML中的描述往往为了严谨而性能很差。为了避免在写代码时忽略了复杂度问题,可以构造极限数据进行性能测试。
架构设计
本单元作业因为主要是实现JML规格定义的接口,因此在架构上并不复杂,基本上是给什么写什么的状态。不过为了实现一些图论算法的复用,我额外构造了一个并查集类用于查询连通块和最小生成树算法。同时我的最小生成树算法也单独抽象成一个工具类。此外,我还构建了一个用于最短路优先队列排序的DijNode数据结构。这些结构基本都很独立。
图模型构建和维护
并查集
出于复用的考虑将并查集抽象为类,以下为并查集类中的部分代码。
public class UniFinSet {
private Map<Integer,Integer> fatherMap;
public UniFinSet() {
fatherMap = new HashMap<>();
}
public void add(int id) //添加初始节点
public void union(int id1, int id2) {
int f1 = find(id1);
int f2 = find(id2);
if (f1 != f2) {
fatherMap.put(f1,f2);
}
}
public int find(int id) {
if (!fatherMap.containsKey(id)) {
add(id);
}
if (fatherMap.get(id) != id) {
fatherMap.put(id,find(fatherMap.get(id)));
}
return fatherMap.get(id);
}
//为外部调用封装的方法
public boolean isCircled(int id1, int id2)
public int getFatherSetSize()
}
最小生成树
Kruskal算法实现
public class MiniSpanTreeTool {
public int getMiniSpanTreeValue(int id) {
//getAllLinkedPersonSet;
Set<Integer> linkedPeople = getLinkedPeople(id);
//getAllEdgesThatLinkedAllPersonAbove;
Set<Edge> linkedEdges = getLinkedEdges(linkedPeople);
//FindMinGeneTreeValue
return getMiniSpanTree(linkedPeople,linkedEdges);
}
//获取与id同处一个连通块的所有人的id集合
private Set<Integer> getLinkedPeople(int id)
//获取与id同处一个连通块的所有边的集合
private Set<Edge> getLinkedEdges(Set<Integer> peopleSet)
//最小生成树Kruskal算法
private int getMiniSpanTree(Set<Integer> peopleSet, Set<Edge> edgeSet) {
UniFinSet<Integer> checkSet = new UniFinSet<>();
for (Integer personId : peopleSet) {
checkSet.add(personId);
}
int count = peopleSet.size() - 1;
int miniValue = 0;
List<Edge> edgeList = new ArrayList<>(edgeSet);
Collections.sort(edgeList);
while (count > 0) {
Edge edge = edgeList.get(0);
edgeList.remove(edge);
if (!checkSet.isCircled(edge.getPerson1().getId(),edge.getPerson2().getId())) {
checkSet.union(edge.getPerson1().getId(),edge.getPerson2().getId());
miniValue += edge.getValue();
count--;
}
}
return miniValue;
}
}
最短路
Dijkstra算法实现
private int dijkstra(int stId,int edId) {
Map<Integer,Integer> dist = new HashMap<>();
Set<Integer> vis = new HashSet<>();
Set<Integer> linkedPeople = getLinkedPeople(stId);
Queue<DijNode> pq = new PriorityQueue<>(Comparator.comparingInt(DijNode::getWeigh));//按照边权排序的优先队列
int size = linkedPeople.size();
int max = Integer.MAX_VALUE / 2 - 10;//TODO
for (Integer person : linkedPeople) {
dist.put(person,max);
}
dist.replace(stId,0);
pq.add(new DijNode(stId,0));
while (!pq.isEmpty()) {
if (vis.contains(edId)) {
break;
}
DijNode node = pq.poll();
int minId = node.getId();
if (vis.contains(minId)) {
continue;
}
vis.add(minId);
for (Person person: ((MyPerson) getPerson(minId)).getAcquaintance()) {
int pj = person.getId();
if (!vis.contains(pj)) {
dist.replace(pj,
Math.min(
dist.get(pj),
dist.get(minId) + getEdgeValue(minId,pj)
)
);
pq.add(new DijNode(pj,dist.get(pj)));
}
}
}
return dist.get(edId);
}
性能问题与修复
qgvs & qgav
qgvs:最早在实现时在Group的getValueSum()方法中调用了Person的queryValue()方法。这种调用实际隐含了一个二重循环,因此导致了在互测过程中超时。通过在Group中维护全局的valueSum属性,并在人进组时增加其值,离开组时减去,减少查询时的时间复杂度。
qgav:与上一条指令类似的错误,同样通过维护全局totalAge值来减少时间复杂度。
Dijkstra
Dijkstra需要使用堆优化。
规格扩展
假设出现了几种不同的Person
Advertiser
:持续向外发送产品广告Producer
:产品生产商,通过Advertiser
来销售产品Customer
:消费者,会关注广告并选择和自己偏好匹配的产品来购买 -- 所谓购买,就是直接通过Advertiser
给相应Producer
发一个购买消息Person
:吃瓜群众,不发广告,不买东西,不卖东西如此
Network
可以支持市场营销,并能查询某种商品的销售额和销售路径等
请讨论如何对Network
扩展,给出相关接口方法,并选择3个核心业务功能的接口方法撰写JML规格(借鉴所总结的JML规格模式)
Advertiser
, Producer
,Customer
继承Person
,并添加AdvertiseMessage
,PurchaseMessage
继承Message
。
Network
中应添加addAdvertiser
,addProducer
,addCustomer
,addAdvertiseMessage
,addProduct
,containsProduct
,sendAdvertiseMessage
,addPurchaseMessage
,sendPurchaseMessage
,querySalesValue
,querySalesPath
等方法。
public interface Network {
/*@ public instance model non_null Product[] products;
@*/
/*@ public normal_behavior
@ requires containsMessage(id) && getMessage(id).getType() == 0 &&
@ getMessage(id).getPerson1().isLinked(getMessage(id).getPerson2()) &&
@ getMessage(id).getPerson1() != getMessage(id).getPerson2();
@ assignable messages
@ assignable
@ ensures !containsMessage(id) && 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 \old(getMessage(id)).getPerson1().getSocialValue() ==
@ \old(getMessage(id).getPerson1().getSocialValue()) + \old(getMessage(id)).getSocialValue() &&
@ \old(getMessage(id)).getPerson2().getSocialValue() ==
@ \old(getMessage(id).getPerson2().getSocialValue()) + \old(getMessage(id)).getSocialValue();
@ ensures (\old(getMessage(id)) instanceof PurchaseMessage) ==>
@ (((PurchaseMessage)\old(getMessage(id))).getProduct().getSalesValue() ==
@ \old(((PurchaseMessage)getMessage(id)).getProduct().getSalesValue()) + ((Customer)\old(getMessage(id))).getSpendMoney());
@ ensures (!(\old(getMessage(id)) instanceof PurchaseMessage)) ==> (\not_assigned(products[*].salesValue));
@
@ also
@ public exceptional_behavior
@ signals (MessageIdNotFoundException e) !containsMessage(id) ||
@ containsMessage(id) && getMessage(id).getType() == 1;
@*/
public void sendPurchaseMessage(int messageId) throws MessageIdNotFoundException,;
/*@ public normal_behavior
@ requires containsProduct(productId);
@ ensures \result == getProduct(productId).getSalesValue;
@ also
@ public exceptional_behavior
@ signals (ProductIdNotFoundException e) !containsProduct(personId);
@*/
public /*@ pure @*/ int querySalesValue(int productId) throws ProductIdNotFoundException;
/*@ public normal_behavior
@ requires !(\exists int i; 0 <= i && i < people.length; people[i].equals(person)) && (containsProduct(customer.getPreferProductId()));
@ assignable people;
@ ensures people.length == \old(people.length) + 1;
@ ensures (\forall int i; 0 <= i && i < \old(people.length);
@ (\exists int j; 0 <= j && j < people.length; people[j] == (\old(people[i]))));
@ ensures (\exists int i; 0 <= i && i < people.length; people[i] == person);
@ also
@ public exceptional_behavior
@ signals (EqualPersonIdException e) (\exists int i; 0 <= i && i < people.length;
@ people[i].equals(person));
@ signals (ProductIdNotFoundException e) !containsProduct(customer.getPreferProductId())
@*/
public void addCustomer(/*@ non_null @*/Customer customer) throws EqualPersonIdException,ProductIdNotFoundException;
}
学习体会
本单元在编码上的难度相比于之前两单元简单了不少,主要的重点还是在JML的理解上。JML的规范非常严谨,然而却也多了不少看起来“冗余”而复杂的描述。因此还是需要仔细的阅读JML,划分不同段落并理解其含义。
本单元也是我第一次接触契约式编程,这次的体验让我深刻的意识到老师在学期之初说到的架构设计在先,编程在后的真正意义。面向对象开发的真正难度不在于具体的代码实现,而是如何设计好一个合理而完善的架构。就像本次作业,因为有了JML的规范,编码就完全不是有难度的一件事了。
本单元还推荐我们使用Junit自己进行单元测试。我认为应当更加着重强调这一点,将如何进行单元测试的内容添加到本单元的实验或者训练中,毕竟JML与单元测试十分般配。