第一组作业博客总结
17231164 丁元杰
第一次作业
第一次作业要求实现一个仅由整次幂函数线性组合成的函数的求导程序。
程序结构度量
第一次作业的类图如下:
代码度量如下:
可以看出,由于第一次作业的结构设计非常简洁,各种复杂度度量都在合理的范围。
自己程序的bug
在公测与互测阶段均未发现程序的bug。
发现bug的策略
本次作业的互测阶段,我采用了手工+人工检验的黑盒测试+白盒定位的方式。
手工测试
构造了一系列具有代表性的数据,放进脚本逐一测试:
- 非法字符
v
、汉字等 - 纯空白字符串
- 500个
+x
组成的字符串(爆栈检验) - 大数测试(指数超过int范围)
其中,非法字符成功找出了几个人的bug,经过查看代码,确认问题出在正则表达式使用了s
指代空白字符,或在读入之后使用trim()
方法直接忽略了非法字符。
500个+x
成功使用单一正则表达式进行模式匹配的程序爆栈,其原因是Java自带的正则表达式引擎在普通模式下递归层数过多。
自动测试
使用递归下降的方法生成随机合法的表达式,再在合法表达式中随机插入空格、随机删除字符获得可能非法的数据,对所有程序进行大规模测试。
此法成功找出部分程序的bug,其原因大多为正则表达式的编写存在问题,结合代码阅读可以排除同质bug。
黑白盒
由于一个room内的总代码量依然很大,采用阅读代码的方式定位bug费时费力,故本次作业主要采用黑盒测试。在使用黑盒测试成功找出可以造成错误的数据后,则需要阅读代码,确定产生这个bug的位置,以及多组数据是否暴露了同质的bug。于是黑盒测试还需要结合白盒检验。
Applying Creation Pattern
第一次作业并没有使用设计模式,所有对象的创建都是使用的构造器。于是对象的内部结构需要完全暴露给创造对象的方法,一定程度上破坏了封装性。
重构时,不仅需要更改有关对象创建的部分,因为不同类之间的耦合已经十分显著。因此可以先将整体重构至第三次作业,再考虑设计模式的问题。
第二次作业
程序结构度量
类图如下:
代码度量:
可以看出,第二次作业是第一次作业的延申,因此代码复杂度依然可控。唯独function.Parser的圈复杂度偏高,这是因为我将所有递归下降的函数全部放入了一个类中。这个结果也符和预期。
自己程序的bug
在公测阶段,没有检出bug;但在互测阶段,收到了两次hack,都指向了一个同质bug:在某些条件下,化简过程会超时。
本次作业的化简方法采用了DFS的搜索,这种依赖递归的暴力算法将会尝试所有的化简方法,并从中选取最优。理论上,DFS可以在充足的时间内找到全局的最优解。但是实际上,DFS搜寻的时间可能非常之长。考虑到实现,在DFS过程中引入全局运行时间是一个折衷的解决方案:尽可能地搜索,保证不会超时。
在初版程序中,这个时间阈值设为了800ms,并在java11环境下通过了不全面的压力测试。然而,在实测的java8环境下,运行特定数据超出了1s的时间限制。经过分析是java虚拟机在启动阶段、结束阶段还要进行若干程序不可知晓的操作,占用了许多时间。
最后将阈值调整为100ms后顺利通过。
发现bug的策略
第二次作业采用了和第一次作业完全一致的策略,只是更改了相关数据生成器。
Applying Creation Pattern
第二次作业的设计只是第一次作业基础上的更改,类之间的耦合度还是非常高。问题在第三次作业得到初步的解决。
第三次作业
程序结构度量
类图:
代码度量:
第三次作业的代码复杂度非常可观,观察可以发现是用于化简的逻辑将各个类绑定在了一起。这是因为在一开始设计表达式树的时候,并没有考虑到化简的逻辑,因此在后期添加化简功能时,不得不将所有类互相捆绑在一起。
自己程序的bug
本次作业在公测和互测中没有发现bug。
发现bug的策略
第三次作业采用了和第二次作业一致的策略,只不过关闭了数据生成器随机产生扰动的功能。
Applying Creation Pattern
本次作业的关于表达式树结点的创建尝试了工厂方法,即,将创建结点的逻辑抽出到工厂中完成。这一切本来计划顺利,但是后来发现自己理解错了Java多态时隐式参数匹配机制,导致这个工厂徒有其表。再加上后来化简功能的加入,这个工厂完全没有起到作用。
重构代码的话,应当考虑每一个类应该对外保留什么构造器,再把所有的构造逻辑全部丢给工厂。
三次作业以来的感受
面向对象真是一门给自己挖坑的课程,每一周偷的懒都会在下一周原样奉还。如此这般,OO占据了比我预期中更多的时间。
虽然课程组的本意是:如果结构设计得当,每一次不需要进行代码重构。但是我在设计类的过程中发现,一个类很难在设计时保证它对所有的功能开放扩展。也就是说,如果三次作业的设计是:求导、算术运算、积分,那么很有可能保留最精简的表达式存储结构,抽象表达式的计算逻辑会获得最佳的体验;反过来,三次作业的顺序是:幂函数的线性组合、幂函数和三角函数的幂的线性组合、支持某些嵌套的基本初等函数,使得最好的结构必定是抽象表达式树、保留求导和化简为接口。
即便如此,作业与作业之间保留的只可能是结构模块,输入输出、化简模块在作业之间不可能有共用的部分,也就导致了每次重写的代码量并不会因为结构设计的优秀而减少太多。
诚然,课程组在教学过程中也会遇到诸多困难,我非常感激助教和老师的辛勤付出。今年的OO体验较往年还是有十分显著的提升的,这其中就包括规则的明确和评测机等技术的应用。不过不出所料,OO还是难逃被“吐槽”的命运。
我对于命题、判分等问题没有特别独特的感受,唯有一点让我非常不满。第三次作业的指导书内包含了关于WF的描述,而最后互测阶段不允许提交相关数据;第三次作业因为收交情况延时。我理解老师们的用心,但是真心觉得在规则公布之后频繁更改规则对于细心雕琢WF,合理安排时间完成作业的同学是一种莫大的打击。我不想张口闭口谈公平,因为我也不知道什么样才叫公平,但是如果课程组的让步使得同学们(包括我)越来越懒,潜心钻空子而非潜心设计代码,那么必定会背离课程组的初衷。