[BUAA OO Unit 3 HW12] 第三单元总结

写在前面

面向对象课程第三单元结束了。

这个单元和前两个单元体验有些不同:

前两个单元更偏向于整体架构的合理设计,好的架构可以说成功了大半;

而这一单元整体的架构已经给出,我们虽然可以自己增添一些设计,但整体不会有很大改变,反而是将重点转移到了对于具体方法的书写中。话虽如此,选择什么数据结构、用什么算法实现所需要的方法仍然需要我们好好地考虑。

总的来说,这一单元做的事情就是读JML规格,写代码。以下是我这单元的学习心得。

一、测试过程

1.1 黑箱测试与白箱测试

在这一单元的理论课学习中老师向我们讲述了这两个概念。我通过复习PPT和上网查阅资料,结合自己本单元作业的经验,梳理出以下对这两个概念的解释:
黑箱测试
这里的“黑箱”指的是我们的程序,“黑”是指我们看不到里面,具体来说就是我们不知道程序里的各种各样的数据结构和方法是如何实现的。而“黑箱测试”就是我们不需要知道程序的具体实现方法,而是只需要知道这个程序做什么,也就是输入什么,输出什么,功能是什么,然后进行覆盖性的测试看是否和其所承诺的功能相符合就行了。
白箱测试
类比上述解释,“白箱”就是我们的程序,“白”是指我们可以将程序的内部看得一清二楚。在这个测试中,我们要对代码所用语言、内部逻辑、数据结构和算法等细节都要有清晰的理解,然后通过分类讨论的推理等方式推测出可能会在某方面有功能上的漏洞,以及对每个功能单元设计有针对性的数据进行单独的检验等。

简单来说,“黑箱测试”中我们只得到了输入和输出两个“端口”,而“白箱测试”中我们得到了整个代码的各种细节。

拿我们在OO课上的互测环节举例:我们下好所有人的代码然后导出jar包进行数据生成批量测试时就是“黑箱测试”;而当我们这样试不出错时就要耐下心来阅读其他人的代码,来通过了解其实现细节构造有针对性的数据进行hack,这就是“白箱测试”。

1.2 单元测试、功能测试、集成测试、压力测试与回归测试

我认为这是对测试的另一个基于不同视角的划分,“黑箱测试”和“白箱测试”是基于程序内部是否可见,而这四个测试则可以认为是基于测试的策略:

单元测试
对于每个功能实现的单元模块如某一个方法等进行输入输出的正确性检验。

功能测试
通过输入输出来检查整个程序的功能实现是否成功。

集成测试
将通过了单元测试的单元模块一点一点地组合起来构成程序,期间通过测试发现与模块接口有关的问题。

压力测试
这是OO课程强测环节中经常出现的。是对程序不断施加越来越大的负载,来检查性能和稳定性

回归测试
在代码进行修改(不管是对性能进行优化还是修复bug等)或者功能迭代之后重新测试之前的测试用例,以保证修改的正确性。

总的来看,我认为这一系列测试环节是层层递进、相辅相成的:
首先我们要进行单元测试来检查每个“部件”是否正确(单元测试),然后再看其整体功能是否正常(功能测试)和组合过程中的模块接口设计是否正确(集成测试)。之后对其正确性的测试暂告一段落,我们在保证了其正确性的基础上把视角放在其性能和稳定性上(压力测试)。这个过程难免涉及bug修复和性能优化,我们再检查每次的修改是否正确(回归测试)。

其中,功能测试一般在黑箱测试中应用,而单元测试一般在白箱测试中应用。

1.3 测试工具和数据构造策略

本单元我主要采用的是自动数据生成程序+对拍器+Junit的测试工具。

通过手动画一些图,然后用指令描述这个图,然后检查各个指令的功能是否正确。但这样只适用于规模较小的,简单的功能检查。(功能测试)

对于比较重要的指令(qts, qbs, qci, qlm等)进行大规模的测试,主要是用ap和ar来生成稠密图和稀疏图,紧接着大量的qlm等指令进行测试。为了让生成的图更有效一些,我们可以先把3000个点左右连成一条线,再进行大量随机的ar。(压力测试)

对于OKTest方法,主要的方式是手动构造数据,对于每个异常情况进行有针对性的构造,然后再构造几个正常数据。

利用Junit测试框架,对于JML规格里的每一种情况进行检查,争取覆盖到所有可能的情况。这部分内容让我联想到了OS课程中用于测试的check函数,其思路也是通过构造数据,传入写好的函数,利用assert方法来进行检查。在这里变成了创建对象,传入数据,然后进行检查,但十分类似。(单元测试)

总的来看,以上我主要采用了单元测试、功能测试和压力测试三个测试策略。

二、架构设计

本单元在架构上的设计大部分已经给定,也给了我们一个很好的示范:功能迭代最好只是不断的进行新的继承(新的子类)而不要在原来的基础上进行改动。

课程组对原有类的改动只涉及了方法和数据的添加,并没有对之前的方法和数据进行改动,此外不断创建新的子类进行功能的迭代。

这满足了理论课上讲到的一个替换原则:将程序中所有父类改成子类,仍应该可以正常运行

2.1 架构设计

架构设计
除去runner等直接给出的部分和自定义异常部分的实现,关键部分的架构设计大体如上。

迭代的步骤主要有两种方式:

1.在原来的类里面加新的数据结构和新的方法,但原来的数据结构和方法不改变,原来的不变式和约束也不变。

2.创建新的子类,继承原来的类然后加入新的部分,但要注意与父类相比,父类不变式和约束式应当仍满足。

由于较少涉及方法的重写,但课上重点提到了,所以进行复习:在满足2.的条件下还要满足:原来父类该方法的输入范围不能缩小、满足父类满足父类输入条件的副作用不能扩大、满足父类输入条件的后置条件(ensures)不能更宽松。这些条件不满足的话编译仍可以正常通过,但违反了面向对象设计准则。简单来讲还是那句话:将程序中所有父类改成子类,仍应该可以正常运行。当重写的方法不能满足以上要求时,哪怕相似度再高,我们都要另写一个新的方法而不是重写。

图中让我觉得有趣的设计是message方法的设计。message是最高抽象,它总结了所有消息的共同点。下面衍生出几类接口(红包、通知和表情等),每个接口对于自己独特的特性新添加新的方法进行实现。整个过程让我更加体会到了面向对象的魅力。虽然这门课一开始就早已讲到这一点,但前面的作业主要是自己来设计,没有好的参考,而这一单元给出了范例,让我学到了很多。当然也跟我没做过什么项目,经验不足有关(苦笑)。

2.2 图模型构建和维护策略

我采用hashmap来存储节点(key为id,value为对应的person或者group或者message),在每个person里面用hashmap来存边值,而用treemap来存人,权值就是其value,这样使得查询最有价值的朋友时直接返回堆顶元素即可。

采用并查集处理联通块的问题。在删除边时遍历其之前所在连通块内的所有人,进行并查集的重建。

三元环和连通块个数采用动态维护的方式:

每新加一个人,连通块个数+1;每新加一个边,看这两个点原来是否联通,不联通则连通块个数-1,再看有无其他点同时和这两个点分别通过一条边相连,每有一个这样的点,在加边后三元环个数都要+1。

在删边时,并查集重建后如果删的边对应的两个点不再联通,那么连通块个数+1;再看有无其他点同时和这两个点分别通过一条边相连,每有一个这样的点,在删边后三元环个数都要-1。

最小环方面:

首先采用了堆优化的最短路径算法。

也就是首先将原点放入小顶堆中,在每次查询有无可以新加的节点时从该小顶堆中取堆顶元素,然后将所有与原点距离能变得更小的点放入或重新放入堆中进行维护。当堆空时便意味着最短路径建好了。由于堆的维护时间代价为log级别,因此时间效率比原来的遍历高很多。这一思想与每个人查询自己最有价值的人的思路类似,以后当我们遇到类似的题目,要我们取最大最小值时我们就可以用这种办法。

然后采用分类讨论的办法,首先我们要意识到某人所在的最短环中,每一个边两侧的点到原点应该是两条最短路径(否则还可以更短)。然后采用穷举边的办法,在最短路径图中先把与原点相连接的边去掉,然后分为两种情况,两个点在不同连通块中,和其中一个点在最短路径图中不直接与原点相连而另一个就是原点本身。取最小值即为答案,或者找不到而抛出异常。其中在最短路径树中我们仍可以采用并查集的方法来更高效地判断连通性,只要保存好每个点与原点的距离就行,因为原题中不要求输出最小环的组成而只需要输出长度,因此我们可以这样做。

这单元让我印象比较深刻的是两个堆的采用:一个是每个人维护自己最有价值的朋友,一个是最短路径的堆优化。让我初步体会到了算法设计的魅力。

三、代码性能问题、修复情况以及对规格与实现分离的理解

3.1 代码性能问题及修复情况

本单元的性能问题主要是时间复杂度上的问题,我们可以采用缓存机制(改动时才重新计算,否则直接输出原来保存的值)、并查集、动态维护、堆优化等方式解决。具体方式在维护策略上已说明。这一部分是我在本单元收获最大的地方。

3.2 规格与实现分离

JML是本单元的重点之一,让我体会到了契约式编程的魅力。

规格侧重于描述功能,其会引入一些中间变量和\forall和\exists等进行描述,其更重要的是将功能尽可能严谨全面地描述清楚。

具体实现侧重于实现功能,其没有必要照搬(有时也无法照搬,因为规格并不是完全按照编程思路描述的)规格内容,而是采用自己的办法来实现规格所描述的功能,以实现更佳的性能。

以上就是我对规格与实现分离的思考。以本单元为例,规格主要使用数组(或者说线性表)这一数据结构来描述功能,但我们在实际中可以用hashmap来实现,这样查询和修改值只需要用containsKey和compute方法,时间复杂度也更低。

综上所述,规格和实现是实际工业化流程中两个不同的重要步骤,我们都要重视而且明白其不同的侧重点,才能更好地领会这门课交给我们的内容。

四、对OK测试的思考和建议

4.1 思考

OK测试的意义
基于规格与实现分离的思想,我们在代码实现中是对规格进行了推理改编的,我们希望我们与规格不同的实现能符合规格。但实际中大家再严谨认真也难免百密一疏,这时我们需要用OK测试来尽可能证明我们的实现是符合规格的。

这也让我们更好地理解契约式编程,客户用规格来描述需求,程序员来进行实现,这避免了程序员误解客户用自然语言描述的需求等麻烦。

OK测试的步骤
基于上述OK测试的意义阐述,我们不难得出结论:我们需要尽可能一板一眼的按照规格所描述的条件来进行检验而不能做出任何推理改造

我们要做的事情可以概括为:跟据beforedata满足的前置条件来定位beforedata需要满足的副作用和后置条件,抑或是抛出的异常。然后进行一一对比检查。

一些注意点
本单元对OKTest做出了简化,比如传入的是hashmap但规格里是ArrayList等,这使得满足本OK测试是满足规格的必要条件但不是充分条件,这在指导书中也以说明过。

4.2 建议

就本单元而言,我认为可以用ArrayList来装输入和输出,这样更符合规格的实现,能更全面地检查错误。其实我认为这里也可能已经暗示了实际实现中采用hashmap是比较高效的,但是与规格不能很好的匹配。如果真的有人用了ArrayList来实现,转化成hashmap传入后原来在顺序上的错误也会消失(尽管可能并不影响功能,但却不符合规格)。

五、学习体会

本单元的学习收获颇丰,与之前相比更具趣味性和成就感:我学到了JML规格和契约式编程的思想,结合理论课和作业的框架代码更深入地领会了SOLID原则,也学到了treemap等java封装好的数据结构,也认识了实际工业生产中的步骤:需求、规格、实现、建模测试等等。这些都是源头活水,让我对编程有了更深入的理解,也增加了我的兴趣。

这一单元让我重新认识了java这门语言的强大以及面向对象思想的精妙,也让我意识到之前的学习有多么粗浅。同样的,现在的我在将来看来也会是幼稚而无知的,我要深入学习的显然还有很多,因此我会吸取经验,继续努力。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值