面向对象程序设计Unit1作业分析总结
面向对象程序设计Unit1作业分析总结
一、作业分析和整体设计思路
1.1作业目的
在本次Unit 1的三次作业中,主要的目的是将输入表达式中的非必要括号拆除,保留必要括号。同时,我们可以在这个基础上做适当的化简与合并。三次作业是层进式的,表达式的复杂程度和需要考虑的方面逐渐增多,这就需要我们对于代码整体的结构进行设计,方便在后续可以直接进行迭代而不是每次都需要重构。
1.2整体思路
在开始写第一次作业之前,我首先用了一天时间来完成课程组给出的训练小作业以及逛作业的评论区。最后基于我对于题目的理解,采用了逐层解析,构建表达式的层级结构,然后利用将表达式整体转化成同一结构的多项式来进行合并输出。
1.2.1解析
这里的解析是对输入的表达式进行解析,按照要求给出的分层的形式化定义,从顶向下将输入的字符串表达式变成一个表达式——项——因子的表达式树。
用于解析的类有Lexer
和Parse
。
Lexer
的作用是对输入的表达式字符串进行词法分析,将表达式中的每一个“词”分解出来,提供给Parse
来进行整体的分析,同时去掉表达式中对于整体解析不必须的空白符号,在我们的作业中就是空格和制表符。在表达式中,需要解析的“词”主要包括表示乘方的**,数字,加减乘号,括号,变量,函数名,三角函数名等等。其中比较重要的正负号处理,我利用addAndSub
方法将所有连续出现的“+”和“-”都合并成一个“+”或“-”。
Parse
的作用是对表达式进行语法分析,进而构建出表达式树,根据表达式的结构定义,自然的利用parseExpr
,parseTerm
,parseFactor
来分层次解析表达式,构建表达式树。
通过Expression
、Term
、Constant
、TriFunc
、Power
类来最终实现表达式树,其中Expression
、Constant
、TriFunc
、Power
都实现了Factor
接口。
1.2.2去除括号
对于将表达式展开的这一过程,由于我最后会将表达式转化成Polynomial
,这个类是一个WholeMonomial
类的数组,数组元素间是求和关系。而WholeMonomial
类是一个可以一般化的表示每一项的类,主要结构定义如下:
public class WholeMonomial {
private MonomialX monomialX; //以x为主体的因子,具有指数
private ArrayList<MonomialSin> monomialSins; //sin函数的数组,每一个分别具有指数
private ArrayList<MonomialCos> monomialCoses; //cos函数的数组,每一个分别具有指数
private BigInteger number; //这一项的整体的系数,由常数和上面所有项的系数相乘得到
private boolean symbolOfN; //系数的符号,ture为“+”,false为“-”
}
//以上所有相互关系为相乘关系,函数数组内部也为相乘关系
因此,我在之前表达式树的基础上,在表达式树的每一个层级都实现了一个toPoly
的方法,在将表达式转化为polynomial
的同时进行计算,拆开所有的非必要括号。
1.2.3化简与输出
在第一次作业中,由于结构比较简单,我进行了简单的合并,后续的两次作业中由于三角函数相互之间比较的复杂,我就没有进行过多的合并与化简。
在输出方面,我在Polynomial
类中重写了toString
方法,其中调用了WholeMonomial
的toString
方法,在这个方法中又调用了MonomialX
、MonomialSin
、MonomialCos
中的toString
方法。
输出中有几下几点需要注意:
- 最后得到的字符串如果是空串,需要补一个0,因为空串不是合法输出。
- 为了减少不必要的输出,如果系数为1,就不用输出系数。
- 记得通过符号判断在前面加上符号“-”。
二、每次作业具体分析
2.1第一次作业
2.1.1UML类图
2.1.2OO度量
2.1.3本次作业具体分析
本次作业比较简单,代码总量在550行左右,虽然比我之前写的任何一次代码都多(除了计组的那个没搭起来的cpu),但是由于细分了很多类,加上架构对我自己来说足够清晰,在我没有写注释的情况下就毫无压力的写完了这一次作业。
具体的实现在上面基本都已经讲述过了,没有什么特别需要注意的地方。
自己认为这次作业的优缺点如下:
- 优点:在之后的两次迭代中,由于这次建立的架构还算可以,没有遇到需要重构的情况,基本顺延思路就可以迭代下去。
- 缺点:由于从这里开始就采用了
ArrayList
容器来储存,多项式的合并虽然在这次作业中足够简单,但是在之后的作业中,在加入了三角函数后,由于ArrayList
的局限性,导致合并变得很麻烦(最后写了一天全是bug,就没交);方法的归类和名字并没有处理的太好,有一些方法感觉放错类了,后续也没有再更改;对于parseFactor
没有再细分,导致最后这个方法长度过长,在最后一次的bug修复中删改了很多才保证了代码风格的检测。
2.1.4bug分析
这次作业中,强测错了一个点,互测被hack了6个点,都是同一个bug。
由于我对于java的语法并不是很熟悉,在第一次作业中对于BigInteger
的使用很陌生,在使用字符串来构造一个BigInteger
的时候,我并不知道它本身就有一个构造函数可以直接以字符串为变量,而是使用了Valueof
来赋值,导致IDEA推荐我用Long作为中间变量转化,造成了如果输入数据中数字超过long的上限时会直接报错。
这个bug一是因为我对于java的语法了解太少,二是我在测试的时候并没有使用很大的数去测试自己的代码,属于是一个很大的疏忽了。
2.2第二次作业
2.2.1UML类图
2.2.2OO度量
2.2.3本次作业具体分析
整体情况
本次作业的复杂度比上次提高很多,代码量提高到了1050多行。
具体的要求变更有下面几个点:
- 加入了sin和cos两个三角函数。
- 加入了自定义函数。
- 加入了求和函数。
首先,我认为这次的迭代我并没有做好,很多方法都写的很复杂,parseFactor
没有及时切分,同时结构的构建感觉有些冗余,在第三次作业后我学习了几个同学的代码,发现他们的代码结构要更加简单。
三角函数处理
首先我建立了TriFunc
类来表示三角函数,继承Factor
接口,同时根据三角函数这次的特点,其内部的结构选择了Factor
。然后对应的在polynomial包里面建立了对应的类(后来我发现这个类的目的本来是为了更好的合并,但是因为采用了ArrayList
,加上WholeMonomial
的复杂,导致最后的合并变得很麻烦)来处理。
自定义函数处理
我建立SelfFunc
类来接收所有输入的自定义函数,并且作为Parse
的一个属性。
具体的函数带入方法采取了字符串替换的方法,这里有几个需要注意的点:
- 在最开始储存输入数据的时候,将式子中的x替换成了t,防止在替换是反复替换x出错。
- 所有的变量替换的时候都必须加括号,否则可能会出各种各样的bug。
- 在替换完成后得到的字符串也必须加上括号才能返回给
Parse
继续处理。
sum函数处理
这里也是采用字符串替换的方式处理,其中的begin和end都采用BigInteger
类型,防止出现越界情况。
这里需要注意的几点:
- 因为sin中有i,所以在替换变量i的时候会造成错误,我的解决方法是在最开始就将变量i换成j,后面的替换都用j来做。
- 替换的时候要记住需要加括号防止未知bug。
2.2.4bug分析
这次作业在互测和公测中都没有被测出bug
2.3第三次作业
2.3.1UML图
2.3.2OO度量
这次的情况和上次差不多,parseFactor
的复杂度依旧很高,还有toString
虽然我已经根据结构做了分层,但是可能分的还不够细,所以依旧复杂度高。
2.3.3本次作业具体分析
整体情况
本次作业与上次作业之间变化不大,具体的有以下几点:
- sin和cos中的因子由幂函数和常数变为表达式因子。
- 括号可以嵌套。
- sum中可以调用sum函数等。
在这次作业中,我只调整了TriFunc
中的变量的类型,由Factor
调成了Expression
,并且微调了各个类中的toString
函数来适配新的输出,针对括号的嵌套,我在需要对括号作出判断的地方新增了对于括号的识别,防止提前跳出导致错误,其他地方基本都没有进行修改。
结构分析
作为这三次迭代作业的最后一次,我在这里写一下我对于我代码的分析。
1000多行的代码,共有16个类和一个接口,根据写代码的实际体验,这些类之间的相互关系还算分的比较清楚,修改一个类的时候大多数时候并不会带动其他类的修改,除了在第三次作业中修改了Polynomial
的成员导致的大修改。
由于在最后修复bug的时候并没有多考虑结构问题,只是想堵上漏洞,导致出现了对于allPow
这个属性的利用不符合我一开始对它的定义,因此感觉后面这个代码逐渐变得比较混乱,如果继续迭代可能面临更多未知的风险。
同时,这次我本来以为会对我化简有用的多项式设计并没有发挥出应有的功能,而且如果三角函数种类继续增加,可能会导致用这种模式的化简越来越困难,尤其是利用ArrayList
储存的情况下。
2.3.4bug分析
在本次作业中,公测没有测出bug,但在互测中,被hack了九次,合计两个bug。
第一个bug是我没有考虑到常数会有指数的情况,例如2**3
这样的情况,所以当在sum函数中出现这种情况的时候,前期的字符串替换和后面的解析都会出现问题。
第二个bug是没有考虑0**0
的情况。
第一个bug显然是因为第二次向第三次迭代的时候我的考虑不够周全引起的,第二个bug是因为我一直认为这个式子无意义,不会出现在测试数据中。
在这次bug修复中,我遇到了由于修改后方法超过了60行导致代码风格不能通过,后来在助教的建议下切分了parseFactor才解决问题。这也让我认识到了结构更多方法简化的思想。
三.有关互测的感想
这三次互测中,我只有在第一次互测的时候成功hack了其他人4次,互测的时候感觉看别人代码分析bug是一件很困难的事情,看不出来别人的bug就不想提交数据,导致我后面两次没有收获。
在第四周的研讨课上,通过同学的分享,我知道了平时大家的测评程序都是怎么写的。从去年计组开始,我就一直没有自己手动写过测评数据生成的程序,测评数据基本是根据自己理解来思考什么地方最容易出问题,然后有针对性的写几条数据来测评。这样测评很容易造成由于自己在写代码的时候就没有考虑到的问题在自己测试阶段也没有发现,测试效率不高。
我计划在接下来的学习中向他人学习一下怎么自己写测评数据(如果课程组能有针对性的教一下就更好了)。
四.心得体会
- 这次作业是我第一次迭代设计,由于我在第一次作业前仔细思考了结构,所以这次确实在三次作业中没有重构的成功完成了,但是由于自己的设计经验严重缺乏,所以这次的迭代并不能说是成功的,不仅在测试中确实出现了bug,而且继续迭代的可能性不太大,方法也变得比较臃肿,缺少怎么切分方法的经验。
- 但上述那些不足也同样是我所获得的收获,通过水群、课程群、助教、讨论区等等的渠道,我看到了很多人更加优秀的设计思路。在第一次研讨课上的讨论也通过大家的分享让我确立了后续设计的一个思路,同时通过集思广益一起避免了很多可能会出现的bug,让我充分认识到了大家讨论的重要性。在学校中的作业虽然是我们自己完成的代码,但是在未来必然需要和他人合作,相互学习也是很重要的。
- 我学习到了层次化方法,递归下降的思路。这个思路是在train中第一次见到,当时对于Parse这个类大为震撼,相信这种方法在之后也会频繁使用。