OO第三单元总结
OO第三单元总结
一、测试
本单元基于JML进行规格化设计,并且针对实现的代码进行测试。由于每个要实现的功能和方法的规格和作用都已明确给出,所以在具体实现方面着重考虑数据结构和方法复杂度。众所周知,中测就是闹着玩的,真测试还得靠自己。
黑箱、白箱测试
-
黑箱测试,着重测试软件的功能需求,是在程序接口上进行的测试。对于每次作业,就是评测机通过大量测试数据力求达到高覆盖性的测试。由于只能看到输入与输出,其内部运行结构我们并不关心,所以是黑箱测试。
-
白箱测试,逻辑测试,穷举路径测试。
- 针对模块中所有的方法路径都需要进行测试
- 所有逻辑值true和flase都需要进行测试
- 数据结构的测试
- 测试数据在合法范围内都需要覆盖测试
比如我们三次作业中都有针对特定方法的OKTest,在验证OKTest的正确性时,对于每一个逻辑值都要测试true和flase,基于此生成的 测试数据就是白盒测试。
- 此外单元测试是指对软件中最小的可测试单元的测试,在作业中OKTest就是单元测试;将多个模块耦合进行集成测试,例如作业中类之间方法调用测试;针对性能进行压力测试,在作业中CPU时间要求在15s内;当优化或修改代码后,要进行回归测试保证修改没有带来新的错误。
测试数据构造
- 通过缩小id范围,增加数据数量来保证每个异常都能被触发
- 对边界数据的测试,比如测试group的人数
- 对于涉及到动态维护且复杂度较大的指令,加强测试。比如mr操作可能涉及到并查集的更改,可以先ap500条,然后10000条mr,测试CPU运行时间。
二、架构设计
类图(以第三次作业为例)
classDiagram
class MyPerson{
-int id
-String name
-int age
-ArrayList<Message> messages
-HashMap<Integer, Integer> value
-Integer bestAcqId
-Integer bestAcq
-int money
-int socialValue
+MyPerson()
+int getId()
+String getName()
+int getAge()
+boolean equals()
+boolean isLinked()
+int queryValue()
+void setValue()
+void addSocialValue()
+int getSocialValue()
+List<Message> getMessages()
+List<Message>getReceivedMessages()
+void clerNotices()
+void removeAcq()
+void addMessage()
+Integer getBestAcqId()
+int compareTo()
+void addMoney()
+int getMoney()
-void updateBest()
}
class MyNetwork{
-HashMap<Integer, Person> people
-HashMap<Integer, Group> groups
-HashMap<Integer, Message> messages
-HashMap<Integer, Integer> emojis
-Graph graph
+MyNetwork()
+boolean contains()
+Person getPerson()
+void addPerson()
+void addRelation()
+void modifyRelation()
+void modifyGroupRelation()
+void removeRelation()
+int queryValue()
+boolean isCircle()
+int queryBlockSum()
+int queryTripleSum()
+void addGroup()
+Group getGroup()
+void addToGroup()
+int queryGroupValueSum()
+int queryGroupAgeVar()
+void delFromGroup()
+boolean containsMessage()
+void addMessage()
+Message getMessage()
+void sendMessage()
+int querySocialValue()
+List<Message> queryReceiveMessages()
+int queryBestAcquaintance()
+int queryCoupleSum()
+void storeEmojiId()
+boolean containsEmojiId()
+int queryMoney()
+int queryPopularity()
+void deleteColdEmoji()
+void clearNotices()
+int queryLeastMoments()
+int deleteColdEmojiOKTest()
}
class MyMessage{
-int id
-int socialValue
-int type
-Person person1
-Person person2
-Group group
+MyMessage()
+int getType()
+int getSocialValue()
+int getId()
+Person getPerson1()
+Person getPerson2()
+Group getGroup()
+boolean equals()
}
class MyGroup{
-int id
-int valueSum
-int ageSum
-ArrayList<Person> people
+MyGroup()
+int getId()
+void addAllSocialValue()
+void addAllMoney()
+boolean equals)()
+void addPerson()
+void addValue()
+boolean hasPerson()
+int getValueSum()
+void modifyRelation()
+int getAgeMean()
+int getAgeVar()
+void delPerson()
+int getSize()
}
class Graph{
-HashMap<Integer, HashSet<Integer>> uniComponent
-HashMap<Integer, HashMap<Integer, Integer>> triples
-HashMap<Integer, HashMap<Integer, Boolean>> isUsefuls
-int blockSum
+void addPerson()
+void addRelation()
+void modifyRelation()
+void addTri()
+void merge()
+boolean isCircle()
+int queryTriSum()
+int queryBlockSum()
+void removeRel()
+int queryLeastMoments()
}
class MyEmojiMessage{
-int emojiId
+MyEmojiMessage()
}
class MyRedEnvelopeMessage{
-int money
+MyRedEnvlopeMessage()
}
MyNetwork --> MyPerson : contains
MyNetwork --> MyMessage : contains
MyNetwork --> MyGroup : contains
MyNetwork --> Graph : contains
MyMessage --> MyEmojiMessage : extends
MyMessage --> MyRedEnvelopeMessage : extends
图模型数据结构
本单元涉及较难实现的部分有最大联通块的数量、三元环的数量、循环路径数量,其中最大联通块的数量动态维护。
并查集实现动态维护最大联通块的数量
此部分我没有使用建树的方法,维护策略如下
graph TB
A1(操作)
A1 --加关系--> B1
A1 --删关系--> C1
subgraph 加关系
B1 --- B2
B1 --- B3
B2 --> B4
B3 --将所在的两个连通块融合--> B5
B1{判断两人是否已在同一连通块}
B2[在]
B3[不在]
B4[将此关系标记为无用关系]
B5[blockSum--]
end
subgraph 删关系
C1 --- C2
C2 --> C3
C1 --> C4
C4 --> C5
C5 --> C6
C6 --> C7
C7 --> B1
C1{判断此关系类型}
C2[无用关系]
C3[直接删去]
C4[有用关系]
C5[记录此联通块的所有边]
C6[将联通块的所有边变为孤立点]
C7[重新加边]
end
有向图查询三元环数量
将所有关系记为id较小的人指向id大的人的出边,所有三元环都有如下结构:
graph LR
id1 --e12--> id2
id2 --e23--> id3
id1 --e13--> id3
id1((p1))
id2((p2))
id3((p3))
算法如下:
1 |
|
Dijkstra记录最短路径和次短路径维护循环路径
借鉴评论区大佬的算法,感觉很巧妙,只需要一次Dijkstra就可以得到答案。
三、性能问题
并查集性能
由于我使用HashSet存储一个联通块中的所有人的id,当merge联通块的时候需要操作很多次,远不如建树方便。最初删关系的时候由于直接将所有边(除删去的边)重新加入。在本地对拍的时候,我的运行时间常常是同学的10倍。解决方法:在加关系的时候标记边的类型
- 两个点已经联通,删去此边没有影响,所以标记为无用边
- 没有联通,将次边记为有用边
所以只有有用边才需要重新加边操作,无用边可直接删去。优化完运行时间变为建树的两倍
循环路径
第三次作业qlm强测CTLE,主要是mr性能一般,并且Dijkstra没有用优先队列优化。
规格与实现分离
在第一次作业实现过程中,我总是被JML的规格所束缚。但在后续过程中我逐渐认识到,JML只是一个约束条件,在满足规格的条件下,具体实现越简洁越好。比如在MyPerson中JML维护了Persons和Values两个数组分别存放熟人和熟人的亲密度,但其实一个HashMap就够了。所以总结下来,JML就像是规则,在不违背规则的情况下,任何实现方法都是正确的。除此之外,JML就像是项目的维护手册,具体实现千差万别,理解起来很难,但阅读JML能很清晰的理解这部分功能是什么,使用了什么数据结构,以及在后续优化维护中要遵守哪些规则。
在具体实现的过程中,首先将JML翻译为自然语言,然后选择合适的数据结构和算法去实现。
四、OK测试
对于OK测试,我认为是在复杂方法中验证正确性的办法,对于我们作业中的OKTest也只能保证调用方法前后保证约束条件。但是对于invariant和constraint等约束,就会变得相当复杂。而且就第二次、第三次作业的OKTest实现起来并不比测试的方法好实现多少。以至于还要对OKTest再进行测试。但不可否认,OKTest可以保证方法严格符合JML的规格。
对于这方面的建议,我觉得可以真正让OKTest去测试我们实现的方法,每次调用该方法时都会自动进行OKTest,达到自检的效果。
五、体会
本单元中最困难的部分不是根据JML规格去实现方法,而是对于数据结构和算法的选择,所谓1000个读者心中有1000个哈莫雷特。很多时候好不容易实现自己的算法,一跑起来发现好慢,又懒得不想重构,只能小修小改结果杯水车薪。其次对于JML,有些方面它能很精确精准的去约束,但有些时候却比自然语言繁琐很多很多,明明一句话就能讲清楚的事情,却要花上很大篇幅的JML去实现。可能在最新的JML中有更高级更简洁的表达吧。总体上来说通过这三次作业,对于如何根据规格进行实现有了很好的理解,掌握了一些算法。但是毕竟不是站在架构师的角度去想,所以在如何在宏观上去实现规格,进行设计还有很长的路要走。有的时候我们不能只闷头实现,也要抬头去想想如何设计吧。