OO-第一单元总结-2021

OO-第一单元总结-2021

写在前面:

​ 作为一个去年疫情在家上OO只得了六十来分的巨菜大三生,今年选择重修OO来刷分,其实还是有诸多顾虑的;周围许多人听说我要重修时第一反应也都是难以置信,毕竟OO这门课实在太硬了orz。同时,据我去年的观察,每一年的OO都会有比较大的改动;而只要有改动,往往就意味着要重写。的确,第一单元的三次作业里,除了第一次作业我因为自己生活学习上的原因实在没有时间完成而侥幸提交了去年第二次作业之外,后两次作业,我是根据今年讨论课上反复提到的、编译里用过的递归下降,从零开始进行的代码搭建和扩展。

​ 我重修OO的原因其实不止成绩上的原因。我深知,OO这门课包含的内容是极多的;去年的我错过了其中许多。这些涉及的内容对于提升一个计算机系学生的系统代码能力是极有裨益的,而这也正是我所欠缺的。所以我相信,重上一遍OO绝不会浪费时间,正相反,我有许多亟待解决的问题和即将收获的知识。刚刚度过的第一单元就证实了我的想法。

一 程序结构分析

​ 由于前面提到的原因,第一次作业就不多说了。我主要将从第二次作业开始,整体对后两次作业进行分析。

架构分析(也许也算是重构分析?)

​ 我从第二次作业时首次采用了递归下降来读取表达式。递归下降的好处就是完全依赖于文法,一个字符一个字符的去读取和分析表达式串,而不用做任何整体上的解读和分析(如果能消除左递归也不用任何的回退或重入)。这避免了大量(且易错的)正则表达式的出现,同时对于第三次作业中的错误处理的扩展也是极其便捷的和自然的:只需在每次getChar后加入判断,如果不是文法中预期的字符则直接调用hasError方法进行错误输出并结束程序。

​ 我主要使用一个ExpParser类来进行递归下降式的表达式读取。而后利用ExpressionTermFactor及其子类进行表达式各部分的存储及后续求导和输出。两次作业中的几个类间关系如下UML图所示。

image-20210328151652291

​ 可以看出,这样的架构使得功能扩展较为容易:第三次作业对三角函数内层由x至因子的只需增加SinFacCosFacFactor间的聚合关系,在求导和输出时只需进一步递归调用Factor类,而不必做更多的修改。

复杂度分析

​ 复杂度上,由于使用了递归下降方法以及之前架构的较好扩展性,后两次作业并无明显差异,以下度量分析以第三次作业为例。

  • 类复杂度

image-20210327142212726

​ 复杂度主要集中在ExpParser类中,这是比较合理且可以接受的:因为ExpParser负责全部读入以及创建表达式、项和因子及其子类,里面包含有较多的判断,各个读入方法间也存在相互调用。

​ 其余各类中,Expression、Term、Factor三个类复杂度比较平均,说明这部分的整体设计是比较合理的。

  • 方法复杂度(只截取复杂度最高的部分)
image-20210328153336927

​ 高复杂度方法仍然集中在ExpParser类中,如之前所述的原因,我个人认为是比较正常、且在现有架构下是不太好避免的。

ExpParser类之外,具有较高圈复杂度的方法时是Factor类的toString()。

​ 究其原因,是我将除了Expression类之外Factor全部子类的toString方法都统一集中在其中(在子类中直接使用super.toString()),这使得里面有大量用instance of判断具体子类类型以获得不同的结果。这确实是一个较为集中且糟糕的设计。应该对其做类似于工厂模式的改进:在Factor父类中进行判断,而后分别进入子类中进行实际的toString操作。

二 遇到的bug及解决

  • 强测和互测中遇到的主要问题集中在两点:

    • toString()hashCode()方法的递归调用导致的RTLE

      我在互测阶段因为这一点被hack了60余次,也是非常印象深刻了(苦笑。

      • 由于toString()递归调用导致的RTLE其实在讨论区中已经有许多优解了。主要解决方式就是减少程序中对toString的出现次数,在每个方法内,对第一次toString的结果进行记忆存储,后面直接调用变量即可。

      • 关于hashCode的问题着实花费了我比较长的时间。最终通过分步调试,我终于定位到出错原因是ExpressionTerm类hashCode方法复写以及滥用HashMap的问题。每次向HashMap中添加元素都要调用hashCod方法,而我的写法下对每个ExpressionTerm对象都要进一步递归调用Factor的hashCode进行求解。于是,一个嵌套了20层括号的因子就会因为反复递归调用hashCode而导致超时。最终,我将此处的HashMap直接替换为ArrayList,彻底地避免了对hashCode方法的调用。

    • ExpParser读取项类的parseTerm方法中,读负号-后没有正确处理空格,导致后续出错。

      • 这二点出现的原因主要是由于依照文法编写递归下降程序时的疏忽。修改上是很简单的,只需添加空格读入。
  • bug总结

    • 测试!!!!!!!!!!!!

      测试真的太重要了。实际上,我遇到的bug都属于很好测出的问题,比如((...(((x)))...))有可能出现的RTLE 应当是在阅读题面时就能想到的容易出问题的边界数据。但很显然,我在前期题面解读和架构时惰于测试样例的编写,导致后期强测、互测的大量失分,也浪费了后期大量的时间进行bug定位和debug。

三 互测策略

  • 目前,我的互测策略较为基础,只是将自己在中测时出现的、且中测中似乎没有检查的错误记录下来并在互测中对同组同学的代码依次下载并进行检查
  • 我知道目前的策略过于简单,我也没有能成功发现许多别人的bug。
  • 在讨论区和自己这两天的反思学习中,我知道了可以利用python的sympy进行对拍程序的搭建(好像知道的有点晚...还是太懒惰了...)、利用脚本批量对互测同学的代码进行测试、以及利用测试模块进行分结构的针对性测试。我将在以后的单元中进行尝试。

四 心得体会和展望

一些反思

​ 尽管在第二部分已经提过,但我还想再说说关于测试的问题。

​ 事实上,目前惰于测试样例编写的我几乎完全依靠指导书的样例和中测来测试程序的正确性。这太不够了。而只要我在中测里出现一个未公开的数据样例的WA,我就会开始非常慌张、担心自己作业挂掉,并开始像没头苍蝇一样乱撞数据点、期盼能复现WA。经过反思,我知道这太草率也太无厘头了;这也实际上是我去年几次作业挂掉的罪魁祸首。人的惰性啊,唉。

​ 解决办法其实早就很清楚了。一是趁早编写测试程序,并随着每次作业的迭代对测试程序进行迭代更新。这也许在开始会花费比较长的时间,但后面一定收效显著。二是在阅读题面时,从题目要求/文法入手尽可能对边界数据进行覆盖性构造,并在程序架构时就充分考虑,不要盲目编写。三是充分利用各种工具,包括性能分析工具(如JProfiler,MetricsReloaded)以及测试模块,提前对自己的程序进行分析测试,而不要依赖评测机。这样,或许在下一个单元中,我能有一次AC中测的经历,而不是依赖中测数据反复提交、反复修改。

一些进步

​ 第二次上OO,第二次经历一个被OO作业完全支配的学期。我好像也没有比那次之前轻松多少,也因为自己学习生活的事情导致写作业时间不够,作业得分仍然没有很理想。我知道以我的程度,即便我学第二次,我还是比不上许多优秀的同学们;但是我必须比之前的我要更好。

​ 事实上我也确实地感受到了自己的一些进步:比如我终于清楚地明白了面向对象中类继承和接口实现的原理、以及诸如工厂模式的设计模式的设计思路,能够很确定地在作业中进行使用(而不是每用一次就要打开浏览器去搜索样例和框架);比如我知道了实验中给出的、以及互测同学的代码中一些优秀设计的重要性,从中汲取收获到了许多;再比如我总算能够清晰地理解理论课上老师所讲述内容的原因和应用价值,终于能解释那些原来囫囵吞枣地在作业中用过的东西的原理。

一些展望

​ 下一单元就是多线程了。这是我去年最怕的也最难过的一部分。我已经开始对多线程进行预fu习,并期待自己能够抛弃自己的惰性,充分实践上面提出的问题解决办法,以较为顺利的通过第二单元。

​ 嘿,电梯,我又要来了。祝我们这回能相处愉快。

posted @ 2021-03-28 17:33  aucu1608  阅读(121)  评论(1)    收藏  举报