BUAA OO 第一单元总结
第一次作业
-
总体架构
一句话概括就是:经典的面向过程做法。。。
-
Main类负责表达式的读入、预处理和求导处理,由于第一次作业中只有常数项和未知数,所以笔者的思路是在Main类中先通过预处理将所有的项都转化成常数*未知数**指数的形式(通过补1或者0),然后将常数和指数存进Expre类中。
-
Expre类只负责接受从主类输入的常数项和指数的大小,然后用单一的求导方法得到结果,并进行化简(即化简指数为1或0的形式等等),最后输出结果。
-
-
基于度量的结构分析
上表是对每一个方法所取的度量分析,可见依然是C语言数据结构的写法,相当于是写了一个主函数和一个结构体,然后用了一些无关紧要的小方法来分担CheckStyle的行数要求。
-
优缺点分析
-
优点
结构简单清晰,易于找到bug和进行分析。。。
-
缺点
内聚性差,难以进行相关拓展,意味着只要题目稍有改动就需要进行重构。
在递归算法中有许多无意义的遍历循环,导致时间复杂度过高,可能会在后续实验中面临超时的问题,第二次果然超时了。
-
-
hack经历
由于笔者在忙于第二次的重构,所以只是无脑交给了评测机,结果同组同学都写的很棒,并没有bug被找出来。
-
总结
总的来说,第一次作业虽然通过一系列复杂的算法,使得在正确性和化简方面的结果和得分还可以接受,也没有被其他同学hack出漏洞。但代码像是仍在学习数据结构时的写法,与面向对象课程的相关性不大,也并没有考虑到后续作业在此基础上的拓展,果然,重构是必须的。
第二次作业
-
总体架构
-
第二次的架构在第一次的基础上进行了改动,Main类是函数的主类,负责表达式的读入和结果的化简,值得大家引以为戒的是化简用到了粗暴的replace,愚蠢的笔者妄图在不产生bug的情况下将所有的化简情况都列出来。
-
PreExpression类负责对读入的表达式进行简单化简(如:将“**”符号替换为“^”,将多余的正负号和空格去掉)和拆项处理。但因为第二次作业项中含有带括号的表达式因子,笔者(太菜了)担心将各种运算符和括号化简时会产生过多的bug,所以在化简时只考虑括号外的内容,而括号内的式子则在递归函数中进行化简;还有个失误就是在构思的时候没有考虑清楚,只想着把求导和拆因子分开,结果一不小心就把负责求导的函数也写在了该类中,导致该类爆炸。
-
Expression类主要负责将PreExpression类中分出来的项进行拆因子处理,然后将因子传给求导函数,在求导算法中自动判断因子类型和产生相应的结果。
-
-
基于度量的结构分析
相比于第一次的作业,第二次中将主要过程分散到了几个核心算法中,没有出现像第一次作业中两个函数占绝大多数任务的情况。但总体上来说仍然有少数算法复杂度过高的问题,且各个算法的循环复杂度(v(G))都不是特别理想。
-
优缺点分析
-
优点
在结构设计上采用了类似递归下降的算法,将表达式一项项地拆分,最终拆成最小单位-因子时进行求导处理,不仅方便debug,也给后续可能的除法和带表达式因子的三角函数预留了空间。
-
缺点
仍然没有很好地体现出面向对象的特点,感觉每一个类仍是一堆方法的合集,为后续的修改和添加造成了麻烦。
化简时没有想到很好的合并表达式中每一项的方法,导致用到了大量的replace来进行化简(类似:把"(+"给替换成"("),给自己构思过程制造麻烦的同时带来了大量的bug,下面会进行详细地分析。
-
-
bug分析
由于用replace来进行粗暴化简实在是个毒瘤,所有笔者不光反复提到,还要把它放在bug分析的最前面。
如上图所示,由于算法化简时采用了大量的replace语句,效率奇低的同时bug还多。。。导致了在互测时被疯狂hack,主要的错误点在于把“*1+”换成了“+”这条语句,原因是因为笔者在处理过程中把“**”符号替换成了“^”符号,所以并没有考虑到指数若为带正号的整数时也满足替换条件,而笔者又愚蠢地先将“^”符号替换回了“**”符号,所以导致了bug的产生。
另一个小bug则是笔者忘记了将递归函数产生的结果及时储存起来,导致在后续的化简判断中,只要用到结果便会再一次递归,最终引起了超时问题。值得一提的是,该类型的bug容易在调试时被发现,笔者是在调试过程中发现有一段算法在很诡异的地方被反复调用,从而发现该bug。
-
小小的重构经历
重构虽然看似是要写一份新的代码,但笔者的重构实际上只是将上一份代码进行更好地分析和规整:调整可以留下的部分之间的顺序和关系、将不满意的部分进行重写、将多余的部分删除。
其实在第一次作业的时候笔者就想着重构(因为第一次的结构过于面向过程,根本没有拓展和完善的可能性),但无奈在第一次的ddl之前没有改完重构代码的bug,便只能在第二次作业中提交。。。
由于在第一次作业中,深受给长代码debug的折磨,在调试过程中只要出现了思路断裂,就完全不知道代码进行到了什么地方,只能从头再来。所以在重构时首要的考虑便是把总算法的任务细分,让每一部分的算法只执行一小部分的工作,即增加方法的个数同时降低每一个方法的复杂度,但很可惜效果不是太好,算是失败了。。。
第二个在重构时与第一次作业有较大变化的便是对表达式求导和化简顺序的调整:由于第一次作业中只有常数项和未知数项,所以第一次作业笔者的思路是先将输入的表达式调整成一种标准形式,然后合并同类项,最后再进行求导和输出即可;但第二次作业中出现了表达式因子和三角函数,使得笔者想将输入表达式化成标准形式的思路完全作废,只能考虑先进行求导操作后再合并和化简。
除了上述的两个方面后,唯一的不同便是在求导函数中给三角函数求导的部分预先留下了给表达式求导的位置,减轻了在第三次作业中的工作量;以及猜测第三次作业中会出现除法式子而预留的除法求导算法(可惜第三次作业并没有)
;其他的部分和第一次作业的算法基本一致。 -
hack经历
与第一次hack时无脑跑评测机的做法有些许的差别,由于笔者认为自己并没有很好地理解面向对象的精髓,加上对JAVA语句算法的不熟悉,所以本次互测时基本上都有好好地阅读同房间同学的代码,不过没想到的是结果变成了在一边感叹大佬们太强的时候,一边丢给了评测机。。。
虽然通过阅读代码找到了一些明显的bug,但笔者认为在互测中更大的意义是在阅读他人代码的同时进行学习和提高自身。
-
总结
代码风格和架构方面:第二次作业在经历了小重构之后,代码结构和写法上面有了一些进步,虽然仍旧很面向过程,但已有了向面向对象发展的趋势。在正确性方面:由于超时和强制replace留下的漏洞,被hack了多次。但总的来说:代码的质量相比第一次有了提高,在可拓展方面也有了进步,在正确性和性能分上虽不如第一次,但也差强人意。
第三次作业
-
总体架构
-
第三次作业有关求导的类在设计上与第二次的基本一致,只在一些细节上进行了调整;不过由于第三次作业中加入了格式检查,所以新建了其他的类来进行类似空格和符号的格式检查。
-
Main类仍旧是函数的主类,与前两次不同的是,主类中只有读入操作和对结果在输出前进行简单的化简处理,其余的操作分散到了其他的类中。
-
DealInput类是用来接受输入、进行简单的空格格式检查(如空格位于sin和cos之间、空格位于符号与带符号的整数之间等等)和去除表达式中的前导零的工作,如果表达式符合规范便将表达式传输到Expression类中进行进一步处理。
-
Expression类是用来接受预判断后的字符串,由于预处理类中检查了空格的相关问题,便在本类中将空格去除后进行拆项的工作,即把表达式分成一个个的项,然后依次把每一个项传到下一个类。
-
Term类接受传输进来的项,然后对其进行符号的判断(如:符号的个数和位置问题等),合规后将每一项中多余的符号全部化简,然后进行拆因子的工作,即把项拆分成为一个个的因子,然后调用下一个类中的求导函数,并返回一个表示结果的字符串。
-
Derivation类负责将传输来的因子进行求导,同时检测到因子的类型后进行相应的格式检查(如:检测到是三角函数类后会检查三角函数括号内是否为表达式因子等),是本次作业的核心内容。
-
-
基于度量的结构分析
可以看出本次作业算法的复杂度明显上升。除了主要的几个算法外,其他部分的复杂度在一个可以接受和便于维护的范围之内;但主要算法的复杂度说明代码的维护难度仍旧很大,仍有很重的面向过程的影子在代码中。
-
优缺点分析
-
优点
沿用了第二次作业中类似递归下降的算法,从表达式-项-因子的顺序进行分析和处理,使得在格式判断、复杂求导和最终结果化简问题上都可以较为清晰和容易地解决,代码的拓展性也有了很大的提升。
-
缺点
缺点也非常明显,代码中部分算法还是过于复杂,维护和调试的难度大;虽然有些进步,但代码仍旧没有很好地掌握面向对象的思维方式,即代码的可拓展性只是相对于类似的表达式求导而言,如果题目稍稍改变内容(如加入类似于第三次格式检查的要求后),仍然只有重构一条路走。
-
-
bug分析
第三次作业整体上产生的bug数目较少(可能的原因是严格了hack条件?)
,主要有两个较为严重的bug:-
第一个bug是在判断空格是否在sin和cos中间时,笔者愚蠢地忽视了cos的结尾也有一个s字符,所以写出了如下的代码,从而使许多正确格式的例子输出”Wrong Format“而导致出错。在预处理中将“cos”替换成“coS”后避免了此问题。
-
第二个bug是因为在原先的代码中,检查空格和删除空格的算法并没有写在一起,笔者在检查空格的算法中使得程序不检查括号内的内容,想通过递归调用来检查,却在删除空格的算法中将空格全部去除,从而使得括号内的空格格式问题无法检查,导致产生bug。
-
-
重构经历
面对本次的表达式格式检查,第二次代码需要改动的地方过于复杂,所以便下定决心重构。
在本次重构中,核心内容仍然是有关于整体的架构,笔者想尽可能地把任务细化,使得每一个类进行的任务复杂度大体上相同,结果最后写完发现,只是把总任务按照过程细分,使得每一个类负责一个过程,导致了DealInput类、Term类和Derivation类的复杂度远超于其他的类,在这方面可以说本次重构并不成功。
但在代码思路上来说,本次的思路相比于前两次有了质的飞跃,笔者在调试本次作业的bug时明显感觉比前两次要轻松,就算在过程中思维断裂也可以很轻易地知道代码运行到了哪一环节,在这一方面来看,可以说重构还是有价值的。
-
总结
第三次作业进一步加强了对于面向对象编程思想的认识,在编写代码的过程中也开始考虑如何从面向过程向面向对象进行转变,尽管仍然没有很好地掌握。。。加上下定决心进行重构时有点晚,导致程序留有很多bug的时候便到了ddl,所以正确性和性能分有些许下降。总的来说,虽然得分相比前两次有降低,但在代码质量上有了明显的提高。
心得体会
本次作业是面向对象课程的第一单元作业,在经历了从什么都不知道,到逐渐可以写出一些符合题目规范的代码,虽然仍有很重的面向过程的影子,但我已经逐渐认识到了面向对象思维方式的重要性,也开始理解JAVA中一些诸如子类父类,继承多态,抽象类,接口类的概念和好处。总结下来就是:学习的过程虽然很痛苦,但是坚持下来之后会发现自己的收获真的很多。
另外本次作业中最有感触的就是重构了。重构虽然费时费力,而且重构有时候还不能解决原代码中的所有问题,但是,重构可以轻松解决原代码中的绝大多数问题,还能使自己的代码产生质的飞跃,不仅是代码框架思路,从代码理解、维护和拓展方面来看都是。如果把重构比作是在翻新房屋,笔者认为不管是一丁点不改变原有代码,直接在原有的架构上反反复复地加补丁,最终形成补丁套补丁的场景;还是完完全全地抛弃原有代码,直接从地基开始新建都是不可取的做法。重构应该是对原有代码基础上的整合、调整与升华,即保留其可以用到的部分,调整其有不足地方的部分(具体算法内容、和其他算法的关系或是在总程序中的顺序等等),重写或是直接放弃其中我们认为无用的部分。这样的话,既让重构所消耗的时间精力最小化,也使得我们完成重构的目的。总而言之一句话:重构几个小时就可以解决原来代码中的绝大多数问题,何乐而不为呢(反正原来的代码也不能用)。
最后的最后,希望在下一单元的作业中自己可以写出更加面向对象、更加优美的程序。