OO第三单元总结
规格实现总体思路
-
首先完成所有异常类的书写,方法大同小异(注意输出格式)
-
优先整体浏览工程中待实现的每一个类的规格。JML语言十分严谨,与此同时细节也非常多,由细节开始理解程序的整体功能不符合人的认知规律也不符合自顶向下的设计原则。理解每个类的整体功能也对理解具体方法的功能有帮助,比如如果可以将
Mygroup
理解为社交网络系统中的群组,那么第三次作业中的type = 1的message(即群发功能)自然也很好理解 -
实现具体方法
-
理解规格后不要直接使用规格中的容器结构实现。如本次作业中存在大量id对应关系,故考虑采用
HashMap<K, V>
,而不是规格中查找效率超低的顺序表结构。 -
优先实现可以“望文生义”的类,先直接从类名理解方法目标,再阅读规格检查实现细节(比如如果不看规格,
addToGroup()
方法的人数1111上限就会出问题) -
优先实现耦合度低的方法
-
将耦合度高、用一个工具类/一种统一方法实现的一类方法放到最后实现
-
架构设计与性能优化
JML单元作业的架构设计可以直接基于规格来完成。
同时,基于三次作业各自的实现需求,增加自己的工具类。
以下是第三单元第三次作业的UML类图(为了使结构更清晰,异常处理类和官方接口并没有被展现在图中)
第九次作业
-
图结构中连通分支的处理
isCircle
queryBlockSum
-
拒绝了
dfs
:本身时间复杂度就高,queryBlockSum
按照规格的实现方法甚至还会调isCircle
,风险较大 -
使用了基于高度优化的并查集,工具类
UnionFind
:完美解决图结构中连通分支相关的问题,Network类直接用并查集结构来实现queryBlockSum
时间复杂度也不高 -
维护
blockSum
变量,加人时自增,加关系时若两人原本位于不同连通分量则blockSum
自减,这样查询queryBlockSum
的时间复杂度方法变成了O(1)。不过,引入不必要的变量一定程度上增加了维护不同步信息不统一的风险,也违背了面向对象设计的原则
-
-
Group类年龄相关数据计算
getValueSum
getAgeMean
getAgeVar
-
在群组增减人时维护
valueSum
ageSum
agePowerSum
(注意,value涉及的是两人之间的socialValue
,所以全局有relation更新的时候也需要遍历维护该变量) -
优点:提高性能;缺点:同样是容易出错,违反面向对象设计原则
-
第十次作业
-
增加
Message
的相关问题,sendMessage
方法实现没有什么难度但是规格巨长无比,考验规格阅读理解能力和耐心 -
图结构中最小生成树的处理
queryLeastConnection
-
因为最开始没有想过维护一个处理图相关算法的类,同时社交网络构建过程中有add relation(即增加边)的过程,故我顺从了最直观的想法——用
PriorityQueue
为一个block维护一个边集,然后使用kruskal
算法(后来发现这种方法找边需要重新遍历......比prim算法复杂度高......也许有更好的遍历方式但我没想出来) -
工具类
-
Relation
-
RelationComparator
:按照权重对边进行排序
-
-
第十一次作业
-
增加三种不同的message,需要正确理解这些message的继承关系和实现关系
-
图结构中最短路径的处理
sendIndirectMessage
-
堆优化的迪杰斯特拉算法,
PriorityQueue
实现 -
Dist
节点工具类,Graph
内含有Dijkstra
算法的具体实现
-
-
迭代器删除
稍显遗憾的是我并没有在三次作业中维护顶层图工具类(执迷不悟,一直到第三次作业时才有了这个想法),更别提在该工具类里完成连通分支、最小生成树和最短路径这三个图的问题实现,而只是每一次基于新的作业需求增加工具类结构(比如),这样三次作业完成之后代码结构看起来会非常冗杂。
测试与bug总结
测试
-
单条指令集中测试:在输入了基础信息之后,可以构造同一个查询指令很多条的数据,重点构造三次作业中图查询的相关数据,精确测试每一个方法的实现是否准确
-
数据限制的测试:比如人数上限1111
-
分支覆盖:构造规格中每一条normal behavior和异常抛出的分支
-
对拍测试
bug
-
第九次作业:维护图结构的过程中在查找两点之间是否联通的过程中出错(实现并查集合并之前忘记判断是否已经在一个连通分支上,非常严重的错误),导致强测后面五个点全部失分加互测被砍了13刀
-
第十一次作业:群发红包消息时,发消息者发给
group.size() - 1
,导致计算错误,互测被砍7刀 -
由于第三单元的代码是基于规格实现的,不易出现隐形的逻辑错误而只会出现严重而明显的错误,测试不充分的结果就是强测两行泪。
Network扩展
假设出现了几种不同的Person
Advertiser:持续向外发送产品广告
Producer:产品生产商,通过Advertiser来销售产品
Customer:消费者,会关注广告并选择和自己偏好匹配的产品来购买 -- 所谓购买,就是直接通过Advertiser给相应Producer发一个购买消息
Person:吃瓜群众,不发广告,不买东西,不卖东西
如此Network可以支持市场营销,并能查询某种商品的销售额和销售路径等 请讨论如何对Network扩展,给出相关接口方法,并选择3个核心业务功能的接口方法撰写JML规格(借鉴所总结的JML规格模式)
-
Advertiser
、Producer
、Customer
继承Person类
-
Advertisement
、BuyMessage
、ProductMessage
继承Message类
Network类中的新增属性与新增方法
//生产产品JML
/*@ public normal_behavior
@requires contains(producer.getId()) && !containsMessage(product.getId());
@assignable products;
@ensures products.length = \old(products.length) + 1;
@ensuers (\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 (PersonIdNotFoundException e) !contains(producer.getId());
@ signals (EqualMessageIdException e) containsMessage(product.getId());
@*/
void addProduct(Producer producer, ProductMessage product) throws PersonIdNotFoundException, EqualMessageIdException
//询问销售额的JML
/*@ public normal_behavior
@ requires containsProductId(product.getId());
@ ensures \result == products(product.getId()).getvalue();
@ also
@ public exceptional_behavior
@ signals (ProductNotFoundException e) !containsProduct(product.getId());
@*/
int getProductValue(ProductMessage product) throws MessageIdNotFoundException
/*@ public normal_behavior
@ requires containsMessage(prductMessageId) && (getMessage(prductMessageId) instanceof Advertisement);
@ assignable messages;
@ assignable people[*].messages;
@ ensures (\forall int i; 0 <= i && i < people.length && getMessage(prductMessageId).getPerson1().isLinked(people[i]);
@ (\forall int j; 0 <= j && j < \old(people[i].getMessages().size());
@ people[i].getMessages().get(j+1) == \old(people[i].getMessages().get(j))) &&
@ people[i].getMessages().get(0).equals(\old(getMessage(id))) &&
@ people[i].getMessages().size() == \old(people[i].getMessages().size()) + 1);
@ ensures !containsMessage(prductMessageId) && messages.length == \old(messages.length) - 1
@ ensuers (\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 < people.length && !getMessage(prductMessageId).getPerson1().isLinked(people[i]);
@ people[i].getMessages().equals(\old(people[i].getMessages()));
@ also
@ public exceptional_behavior
@ signals (MessageIdNotFoundException e) !containsMessage(prductMessageId);
@*/
void sendAdvertisement(int prductMessageId) throws MessageIdNotFoundException
JML单元学习体会
-
复习了数据结构图论的一些重要算法
-
JML通过规格实现了设计与实现的分离并且约束了方法的需求与具体实现,体现了层次化设计的重要性。
阅读规格实现代码,比起以自然语言描述的内容为起点建模,更形式化、更精确、更工程化。
-
学习了在实际代码工作中非常重要的单元测试,同样体会到了层次化设计的妙处