301
社区成员
发帖
与我相关
我的任务
分享经历了前两个单元的学习,笔者对面向对象的基本思想有所了解。完成作业二乃至作业七后,笔者感受到在编程实践中有两个问题始终无法得到有效解决:如何判断面向对象的程序的正确性?如何设计更加高效的程序?
接下来,笔者就从这两个角度来总结这一单元。
在现实生活中,bug可谓是无处不在的。汽车故障可以算是机械的bug,电线短路可以是电路的bug,人们运动时不幸受伤可以是生物体的bug;而软件的bug则不必多加提及——即使是刚刚进入编程世界的笔者,也已经充分感受到了它的不友好。
虽然高强度的测试可以消除很多bug,但是测试无论如何都不可能让程序的逻辑彻底完备。在第三单元开始之前,笔者就尝试思考:结合面向对象将编程的数学模型分类为多个数据实体的思想,能否在编程前或编程时通过保证{程序中各个数据实体中数据和方法的正确性}来保证程序的正确性呢?
一个可能的答案很快就到来了——这就是JML($Java\ Modelling\ Language$)。
在这个单元之前,笔者想要完成一个java项目,就必须边编程,边思考 “这个方法需要完成哪些任务?”“这个方法应该属于哪一个类?”“这几个类应该是什么关系?”以解决类之间的协作关系和层次关系问题。这时,算法的实现问题总是和数据实体的分野问题混杂在一起,往往顾此失彼——算法的实现出错造成bug,而数据实体的分野出错使算法实现混乱并且易于出错、难以debug。这非常痛苦。
同学们似乎对于JML颇有微词,或许是因为JML以对读者造成的认识论难题为代价对形式化的符号施加了相当的辞义负担来保证语义的无二义性(即程序语义与程序需求、目的严格契合,或者说充分保证了程序正确性)的缘故。
可是,通过JML保证的设计与实现分离的好处却是实实在在的!实现者只需要关注数据实体(类)中的数据采用何种数据结构存储、以及类中的方法采用何种算法实现,而不必关心类之间的协作关系和层次关系,并且能够真正保证程序的正确性!
笔者在这三次作业中唯一出现的错误也是因为无意忽略了方法中的
requires前提条件导致的!
于是,笔者可以很轻松的查阅资料、寻找效率很高的算法并慢慢付诸实现,顺便还能享受JML已经提供了的完备数据实体设计的美感(一个完美的架构会让实现者觉得很舒服!),更关键的是能够保证自己编写的程序是正确的!
所以,笔者反而认为,这个单元的编程作业相当地舒心,设计与实现分离的好处在这里彰显得淋漓尽致;而且很好地解决了能否在编程前或编程时通过保证{程序中各个数据实体中数据和方法的正确性}来保证程序的正确性的问题!
JML并未规定实现的具体方法,不过其定义的规格却可以很好地用来做面向对象的测试,也就是用来测试程序中各个数据实体中数据和方法的正确性。
笔者的浅见,在面向对象程序中如果针对每一个数据实体中的数据,以及方法中的每一个步骤都进行测试,则这样的测试就是白盒测试。
可见,程序设计中不仅程序架构可以是面向对象的,就连程序的测试也可以是面向对象的。
反之,如果不考虑程序内部的结构,只是构造数据对程序的整体功能进行测试,则这样的测试就是黑盒测试。
除了第三单元中我们自己设计的Junit测试外,无论是课程组的公测强测、还是同学们自己的评测机评测,它们都应属于黑箱测试。
虽然Junit测试可以很好地测试程序中各个数据实体中数据和方法的正确性,可是其编写的确比较复杂:程序员需要自己构造测试数据,自己根据JML重新实现目标方法,最后要做数据的比较。
数据的构造
@Parameters
public void prepareParameters() {
Random r = new Random();
int id = r.nextInt();
// ...
}
如上述代码块所示,笔者对于数据的构造并无特别策略——唯一的策略就是随机!利用伪随机数随机新增Person,随机addRelation和modifyRelation...
数据的比较
equals则需要对其中元素进行逐一比较,相当复杂。根据JML重新实现目标方法
笔者认为这一部分是最简单的了——只需要完全地根据JML的规格原原本本地重新实现方法即可,而几乎无需考虑任何时间和空间复杂度问题。这样几乎总是能保证Junit的代码实现与规格是一致的。如果能够在数据的比较上做到比较地完备,那么Junit测试检验代码实现与规格的一致性效果是相当不错的——因为即使我没有对所有容器都作严格的比较,也仍然保证了自己的程序的正确性。
除了上述的黑箱测试、白箱测试,还有很多更加细化的测试:
| 名称 | 概念 | 笔者的理解 |
|---|---|---|
| 单元测试 | 对软件基本组成单元进行的测试 | 基本采用白箱测试的方法,有时也采用黑箱测试的办法 |
| 功能测试 | 据功能编写测试用例,并逐项进行测试验证,确保执行结果与预期的结果一致 | 似乎与黑箱测试的概念一样 |
| 集成测试 | 在单元测试的基础上,将所有模块按照设计要求(如根据结构图)组装成为子系统或系统,进行集成测试 | - |
| 压力测试 | 压力测试是长时间或超大负荷地运行测试软件,来测试被测系统的性能、可靠性、稳定性等 | 第7、8次作业中出现的一秒钟49位乘客乘坐电梯的数据,应该就是压力测试的一种 |
| 回归测试 | 需要重新测试前面已经测试过的内容,以确认此次修改没有引入新的错误 | 修复bug或者新增功能时总需要保证从前做过的内容不出错! |
在这三次作业中,在Network方法中要实现**查找通路isCircle、计算分支数queryBlockSum、计算三元组数目queryTripleSum和计算最短路径queryShortestPath**等——这些都是图中的经典问题。
恰如前文所述,有了JML以后,实现者只需要关注数据实体(类)中的数据采用何种数据结构存储、以及类中的方法采用何种算法实现。正是因为JML已经完全规定了每个方法要完成什么,所以我只需要思考要高效实现这些方法需要何种算法即可。于是便有了下方的架构——
MyNetwork、MyPerson、MyMessage、MyNoticeMessage、MyRedEnvelopMessage和MyEmojiMessage之间的关系是JML中已经规定的,无需赘述。RelationGraph、DisjointSet和AcquaintanceList是笔者为更高效实现算法而设计的新的类。HashMapIterator类(实现Iterator接口),用于规格化遍历和删除HashMap中的元素,但HashMapIterator类并未在图中标出。| 类 | 功能 | 算法效率 |
|---|---|---|
DisjointSet | 这是一个并查集,用于计算图中分支数,及确定节点间是否有通路(即两个节点是否在同一个分支中) | 采用按秩合并和路径压缩的启发式算法,每一次操作的时间复杂度控制在$O(1)$内 |
RelationGraph | 管理DisjointSet,并在删除节点时重建并查集;提供BFS(广度优先遍历)方法计算最短路径 | - |
AcquaintanceList | 这是一个最大堆,管理一个优先级队列,用于在$\Theta(1)$时间内获取当前Person关系最好的Person | 最大堆的构建、新增和删除的时间复杂度控制在$O(\log_{2}n)$内 |
利用最大堆、并查集,以及BFS(广度优先遍历)方法,可以保证较高的时间效率。
但是,笔者过分关注特殊数据结构和对应算法的优化,以至于忽略了下列两处简单却细节的优化:
**未动态维护Tag中valueSum**,致使每次执行qtvs(即query_tag_value_sum)指令均需要$\Theta(n)$的时间当场计算。
直接照搬JML中计算couple_sum的双重循环,致使每次执行qcs(即query_couple_sum)指令均需要$\Theta(n^2)$的时间当场计算。
编程的细节问题实在防不胜防!故笔者在第十一次作业中便仔细审视了所有方法,尽量保证所有指令执行的时间复杂度均不超过线性时间,才没有再出现性能问题。
在第二单元时,笔者承认笔者并没有足够的能力设计出高效的调度算法,...毕竟研究如何设计更好的程序架构和debug便已经花费了我大量的时间了。
但在第三单元的作业中,笔者却得以在JML的帮助下,不必再花费大力气设计更好的程序架构,而终于得以抽出时间采用高效的算法。所以设计与实现分离实在是up主倒立——太nb了!
最后,十分感谢课程组、老师们和助教们的辛勤付出!