BUAA OO 第一单元总结
前言
OO课的第一单元为多项式求导。这是我第一次接触面向对象的设计方法和规范。过去3周的3次作业难度逐渐上升,对架构和思维的考验也逐步提高,为了完成这几次作业,我花费了大量的时间,同时也收获了很多东西,让我对面向对象的设计理念有了一定的了解。
第一次作业
第一次作业为简单多项式导函数的求解。
本次作业的难度较低,涉及到的函数只有幂函数,需要注意的点不多,因此我的解决方法也是偏向于面向过程的。在解析表达式时,我使用的方法是正则表达式匹配,每次匹配一个项,对里面的常数和幂进行解析,并存储到容器中,求导按照求导公式进行即可。这一方法虽然很容易想到,但是可扩展性不高,同时也没有偏向于面向对象思想,这为后来的重构埋下了伏笔。
度量分析
类的度量分析
方法度量分析
从图中可以看出,部分方法的复杂度较高,结构较为复杂,仍是偏向于面向过程式的设计,类的度量中后两个类也较为复杂,模块化程度不高。在实际运行程序时,也没有体现相互之间的逻辑关系,只是简单地将几个具有较强关联性的方法封装在一起,没有体现面向对象的继承,多态,接口等特点。同时由于架构的问题,很难拓展到后面的作业。
类图
本次作业的类之间的关系较为简单,有浓烈的面向过程味道。
Main类为主类,掌管全局,各类中解析或处理的数据最终都被汇总于此,进行全局性的操作。
Polyhandle类为输入表达式后对表达式进行操作的第一个类,它由Main类创建,存储处理后的字符串,并有方法获得该字符串。它的主要作用是将空白符号删除,并且合并表达式中的多个加减号为一个,以便进行后续的操作。
Polysave类主要用于解析表达式,它由Main类创建,用一个Map存储解析后的字符串,并有方法获得该表。它通过正则表达式读取的方式,每次读一个项,求出它的系数和指数,并存储到一个Map中,同时在存储的时候进行合并同类项。其中由于getnum()方法需要进行循环以得到数字,因此复杂度较高。
Polyprint类主要用于求导和打印。由于第一次作业的求导较为简单,因此将它们合并进行。求导完成后,直接将该项按照输出格式打印。
程序bug与互测
本次作业中我的程序bug仅有一个,就是因正则表达式考虑不周导致的读取问题,在强测和互测中被hack的点均是因为这个bug。在检查正则表达式时,我使用这个网站进行在线正则表达式测试,以发现并修复bug。
在互测环节,太菜了未发现其他人的bug,被hack的bug均为上述原因造成的。
第二次作业
第二次作业为包含简单幂函数和简单正余弦函数及其嵌套组合函数的导函数的求解。
与第一次作业相比,第二次作业的难度可以说是突飞猛进,不仅新增了三角函数,而且还需要考虑嵌套的因素,复杂度陡然上升,需要一个更加合理的架构,同时嵌套也使得单单使用正则表达式很难将作业完成,这为我作业翻车埋下了伏笔。在这次作业里,我一开始还没有意识到这一点,仍旧尝试使用正则表达式来解析字符串,没有充分思考就直接开始写程序,结果花费大量时间仍然没有完成。这时我才想起递归下降法对处理这类问题的优势。但时间已经不多了,我在没有充分了解该方法的情况下匆忙上手,写出一个“四不像”,最终只通过了弱测,同时由于数据存储的结构存在很大缺陷,已无可能进行bug修复。
度量分析
类的度量分析
方法度量分析
从程序度量分析中可以看出当时的匆忙。由于很多的实现细节没有想清楚,就只能采用“土法上马”,其结果就是写出了极为复杂难懂的代码,并且由于选择架构上的失误,最终这个程序也只是通过了弱测,同时在bug修复中已不存在修复可能。
类图
Main类为主类,主要进行全局性的操作。
Term类是存储解析后表达式的类,起初设想的是用来存储每一个项,其中sin,cos,x分别存储该因子的指数,coe存储项的系数,exp则用于存储项中的表达式嵌套。每一个变量都配有访问它们的方法。但是问题就出在这了,我没有考虑到每个项中的表达式嵌套是可以有多个的,这个类中只能存放一个表达式,而我接下来的解析表达式方式都是从只有一个嵌套的情况考虑的。
Polyhandle类与第一次作业类似,用于简化处理表达式。
Polybreak类为解析表达式类,从上面的程序度量可以看出,里面的很多方法极为复杂臃肿,这也是由于我对递归下降法没有完全理解就匆忙上手的代价,并且这个类里解析表达式的方法完全是围绕Term类的数据结构进行的,缺乏改进空间。
Polycombine类主要是做一些完成解析后的善后工作,比如删除常数为0的项等。
Derivative类用于求导,我采用的遍历方法为递归遍历,逐一遍历,将求导后的结果存入一个新的容器中,传递到输出类里。
Print类为输出类。当时没有使用重写toString方法,只是使用递归的方式遍历整个容器,并将结果输出。
程序bug与互测
这个程序中存储表达式的数据结构完全是错误的,当时写的时候没有考虑到这一点,导致最后我只是侥幸过了弱测,中测WA了一个点,强测则惨不忍睹。
由于解析表达式与求导两个类都是围绕Term类进行的,若想修复bug,就需要对这两个主要的类进行大改,这几乎已经是重构了,因此我最终放弃了修复,转而潜心研究递归下降法。
互测环节不存在的
第三次作业
第三次作业为包含简单幂函数和简单正余弦函数及其嵌套组合函数的导函数的求解。
本次作业据说是本学期难度最高的一次作业。这次作业使用递归下降法来做比较容易。在有了上次的教训后,我在开始作业前首先认真学习了递归下降法,并用该方法重构了第一次作业中的解析表达式部分作为练习,除此之外我重新规划了存储表达式的数据结构,使其更加合理。然后才开始上手第三次作业。经过我的努力以及同学们的帮助,最终顺利完成了这次作业。
度量分析
类的度量分析
方法度量分析
从程序度量分析可以看出此次作业已经有了一些面向对象的意思了,虽然个别方法还是比较复杂,但是我通过一些解耦手段尽量降低复杂性了。这次比较复杂的地方主要是解析表达式中的getfactor()方法和Term类的deri()方法,具体分析见下文。
类图
这次的类图较为复杂。
我的存储结构为定义一个顶层的Factor类,其中三角因子,幂函数因子,常数因子,表达式因子都是它的子类。Factor类有抽象方法deri(),主要用于求导,返回求导后的表达式字符串,将在子类中根据因子种类分别实现。Factor类的sign变量为符号,只取1或-1。Factor的5个子类还分别重写了toString方法,用于后续的输出,并且几个子类也根据自己的因子类型增加了一些变量。
Term类储存表达式的项,它的主体为一个ArrayList<Factor>容器,用于表示因子间的乘法关系,并且这个类也拥有deri()函数用于求导。由于项的求导公式较为繁杂,存在着很多循环与嵌套语句,因此它的求导方法的复杂度也较高。
Expr类储存整个表达式,它的主体同样为ArrayList<Term>容器,用于表达项之间的加法关系,它也拥有deri()函数用于求导。
程序中共有3个方法用于对表达式字符串进行处理。本次作业新增了判断输入是否合法的环节,因此使用了这几个类。Handle1和Handle2两个类用于预处理和部分判断。其中Handle1类用于判断与空格有关的错误类型,我主要是通过暴力枚举的方法进行判断的,并在判断完成后删除全部空格,合并符号。由于这个类中使用了较多的if语句,因此在程序度量中显示它较为复杂。Handle2类则用于对一些其它错误进行判断。Handle3类主要是用于在输出前检查一遍将要输出的字符串是否有缺少括号的情况,并补足括号。
Polysave类用于解析表达式,它使用递归下降法进行解析,方法从获取表达式的getexp()到获取数字的getnum()都有。其中getfactor()方法复杂度最高,因为它需要对当前因子进行类型和正确性判断,存在较多if语句,所以复杂度较高。
最终的输出我是通过直接调用顶层Expr的求导方法来进行的,十分方便,具有一定的面向对象的特征。
但是它也存在一些问题,最主要的就是多项式的求导方法与求导结果的字符串化完全耦合了,这也是求导方法过于复杂的主要原因,虽然这个写法在最初构建代码时较为方便,但是不利于后续的debug,也没有达到低耦合的原则。
程序bug与互测
刚开始写好时程序是有亿点点bug的,包括没有检测出所有非法输入,嵌套的表达式没有加括号,判断语句书写不全等等。而且有一些bug藏得很深人太菜没找出来,提交了数次都没有通过。后来多亏了一位好心的同学帮我跑了测试,找出了隐藏的bug,我也因此改进了解析表达式的算法,最终顺利通过了弱测和中测。
强测与互测阶段,我重点关照了屋内其他人的输入格式检查上。我总共找出屋内其他人共3个bug,都是与判断输入格式有关的,这也说明了输入格式检测是一道相当繁琐易错的工序。我也被hack了3次,都是因为一个致命的bug没有找出来。在刚开始写三角因子求导方法时,我一时疏忽将求导公式记错了,导致部分三角函数求导结果是错误的,这一bug在中测中没有检测出来,直到强测才暴露了出来,但幸好修复很简单,这也体现了程序解耦的重要性。其它几个bug都是出在合法性判断上的用暴力枚举的方法果然有些不靠谱。
重构经历
这一单元每次作业都是重新从零开始写的,爬了。由于架构问题,作业与作业之间缺乏升级迭代空间,并且第二次作业的架构完全就是错误的,导致每一周都只能花费大量时间重新编写。好在第三次作业终于走上正道,稍微带有一些面向对象的色彩了,在修复bug时各功能间的解耦也让我能够更为方便地定位与修改。
心得体会
本单元作业据说是这个学期难度最高的一单元作业,作业间的难度增幅也很大。回望这一单元,我也有很多心得体会。
1.面向对象思想在解决很多问题的时候都是优于面向过程思想的,例如这一单元作业,第二次作业我更多的是采用了面向过程的思维来解决的,导致写起来非常痛苦,而且由于架构错误,花费了大量时间也未能通过中测,到了第三次作业,我开始学会运用继承,重写等方式来编写程序,力求解耦,让编程更加容易了。
2.开始动手前的思考比直接蛮干更重要。在上手写作业前,应当仔细阅读题目要求,思考合适的架构和算法,这样能使编写更为简单,也避免了反复重构带来的麻烦与痛苦。如果不经思考直接开干,很容易绕进死胡同里无法自拔。遇到困难时也应该多向老师,助教,同学请教,避免单打独斗。
3.写程序要仔细认真,避免因粗心大意造成的bug,比如第三次作业中由于粗心大意写错的求导方法。这样是相当可惜的。
这次作业也让我看到了与其它大佬们的差距,当很多人都在思考如何优化程序时,我还在纠结于程序的正确性,以至于后面通过了正确性测试后,已经完全没有时间进行优化了,这也提醒我学习是没有止境的,我仍需努力。