OO第一单元总结
架构设计
本单元最终的架构设计如下
在第一次作业时,就已经大致生成了这样的架构,在第二次作业的迭代中只增加了Exp、Func类以及相关方法,第三次作业迭代只增加了Der类及相关方法。这体现了这一架构在本单元作业中的可扩展性。
接下来,我将通过介绍三次作业的迭代过程来说明架构的具体情况。
第一次作业
第一次作业要求读入一个包含加、减、乘、乘方以及括号(其中括号的深度至多为 1 层)的单变量表达式,输出恒等变形展开所有括号后的表达式。本次作业的难点就是如何对表达式进行解析,在公众号上,课程组提供了表达式解析的方法——递归下降法。我借鉴了递归下降的思想,但并没有完全按照文章中的方法编写自己的代码。另外一个难点是式子的存储,我借鉴了往年学长的博客,总结出了自己的思路。
对于输入的字符串,首先通过FirstDeal类进行预处理,先除去空白符,再处理符号,将连续的符号以及指数后的+进行处理。这里要注意的一个问题是要用两次delSymbol方法,因为这个方法只能处理两个连续的符号,由于格式规定,可能会出现三个符号连续的情况,而delSymbol只能处理两个符号连续的情况。本来一开始FirstDeal中还有除前导零的方法,但后来发现,BigInteger在构建时会自动处理前导零,所以后边又删去了这个方法。
之后预处理完的的字符串进入Parser类进行解析,parserExpr的具体实现如图,这与公众号的思路大致相似,但我没有Lexer类,直接在Parser类中完成了分析过程。在while循环中,我们获取Term并将它们解析后的结果放到ArrayList中存储,分析完所有Term后,for循环将所有Term合并得到Expr。对于Term同理。
对于Factor,在第一次作业中只存在两种情况,有括号与没有括号,有括号的是表达式,需要去括号后继续分析表达式,没有括号的为Var类型(variable,纯x的幂次或纯数字),分别交由相应的方法处理后返回Factor类型的处理结果。
表达式类型的因子又要分成有指数(removeBracketChar1),与没有指数两种(removeBracketChar2);Var类型又分成含x与纯数字两种。具体实现就不多赘述了。
这时候就出现了问题,到了最底层的parserFactor,我们该怎么分析。这时候就需要引出Item的设计。不难发现,所有的式子类型(无论是Expr,Term还是Var),最终都可以表示为coe1·xpow1+coe2·xpow2+…的形式,我们不妨构建一个Item类存储coe和pow,而Factor类中包含一个ArrayList的属性,Expr,Term,Var均继承Factor,通过这个方法实现各种类型的表示与存储。
同时,Item的存在能够让我们更好地进行运算,在parserTerm中,我们在分析得到所有的Factor后,需要将它们乘起来,这时候,我们只需要先实现两个Item的相乘,再实现两个ArrayList的相乘(两个for循环的遍历)即可,这里我将乘法运算封装到了Calculator类中。乘法得到一个新的ArrayList,我们便可以使用这个新的list来创建Term并返回。
注意:在乘法计算时一定要采用深拷贝,即结果为new出来的一个新的Item,如果直接等号赋值,可能会出现结果比实际结果大,这是因为没有采用深拷贝导致Factor内存储的Item内容发生了改变。深拷贝是本次作业中很容易出现错误的一个点,一不小心就会忽略。
前边提到了parserExpr中Term的合并,其实就是将所有Term的ArrayList简单合并成一个新的,构造Expr返回。
最后,调用经过Parser分析得到的expr的toString方法,即可实现输出。
注意:这里的toString方法并非继承自object的toString,而是需要自己编写,用到了Item中的toString。
bug情况
我第一次作业并没有好好进行测试,在完成了代码的编写后,看到通过了中测,自己编写了几组数据没有出错之后就没再理会。因此在强测时被测出了好几个问题,得分仅仅及格,被分到了C房。在互测中,当我构建数据想去hack别人时,我却先发现自己的程序的bug。
本次作业bug主要出现在两个位置,一是我对于去括号时符号的处理出现了问题,在第一次作业中,我使用while循环对要变号的字符串进行遍历,但是,这样需要对很多种情况进行考虑,我正是少考虑了一种情况导致错误。针对这一问题,我发现,通过遍历列举可能情况的方法过于繁琐同时伴随着大量的错误,因此在第二次作业前,我将changeSign方法由Calculator类移到了Expr类,变符号只需将Expr中的每个item变号即可。二是指数为0的情况时,我仅仅输出了一个1,并没有考虑前边可能存在的符号。
第二次作业
第二次作业要求读入一系列自定义函数的定义以及一个包含幂函数、指数函数、自定义函数调用的表达式,输出恒等变形展开所有括号后的表达式。实际上新增的要求有三个,括号嵌套,自定义函数与指数函数。括号的嵌套对于采用递归下降方法解决问题的同学来说是不需要关心的,因为这个方法本来就可以处理嵌套的函数。对于自定义函数与指数函数,我个人认为比较难的是前者,因为自定义函数的代入是个不小的问题,但后边实践中,指数函数反而给我带来不小的麻烦。
对于自定义函数,我先将它们以字符串的形式读入并存在ArrayList中,之后将这个list送入FirstDeal进行处理,返回一个HashMap<Character,ArrayList>,Character为函数名,ArrayList里存形参(通过“,”分隔得到),以及等号后的表达式。通过这样的处理,我们就实现了函数名与形参、表达式的对应,以用于后续的代入处理。
注意:不要忘了对函数表达式进行delBlank与delSymbol
在parserFactor中,遇到f,g,h的函数因子,便会调用Func类的funcFactor方法解析。在这个方法中,我们首先采用与FirstDeal中相似的方法获取实参,存到ArrayList中,再将函数名(遍历字符串得到)、实参表与函数表一同传入takeIn方法。takeIn主要思想是遍历函数表中对应的函数表达式,将形参换成对应的实参后返回给funFactor一个字符串。经过代入得到的字符串在funcFactor中经过parserExpr分析后再返回。通过这样一个过程实现对自定义函数类型factor的处理
对于指数函数,我们首先要在Item上对代码进行更改。由于指数函数的添加,基本式子类型现在应该表示为coe1·xpow1·exp(s1)+coe2·xpow2·exp(s2)+…的形式(指数函数后边的幂次被我乘到了括号里),所以Item中要新加一个属性Expr exprFac,用以表示指数函数括号内的值。
随着上边改变,Item的add,mult,toString方法也随之发生更改。本次作业一个棘手的问题也随之出现:总是有些数据的跑出的值会大于实际结果,我调试了很久才发现是自己mult方法实现中没有进行深拷贝,导致一些本不应该变的值发生了改变。
bug情况
本次作业强测与互测中有三个bug,一是我为了提高代码的性能,选择将coe为0的Item以及空的expr输出空串,这导致我最后的输出结果中出现一些很可笑的bug,例如1-1没有结果,exp(0)输出exp()等,将coe为0以及expr为空时输出0即可解决。第二个bug是在我的处理中,可能会出现-n0这种情况,我会输出1,没有考虑前边的符号,将这种情况在处理指数为0时特殊处理即可。最后一个bug是((((2)8)8)8)^8这种,对于括号+指数的处理,我一开始选择将指数除去,展开成多个term相乘的字符串,在进行parserExpr分析,这样会导致时间复杂度变高,最终超时。我对removeBracketChar1方法进行重构,将乘方直接变成乘法运算,执行指数次,得到的结果构建成Expr返回即可,这样降低了时间开销,第二个强测点顺利通过。
第三次作业
第三次作业添加了两个要求,一是新增求导因子,二是允许函数表达式调用已定义的函数。这两个要求看着不难,实际上真的很简单。
我的第二次作业的实现方法支持函数表达式调用已定义的函数,所以后者完全不需要考虑。
至于新增的求导因子,先增加一个Der类,用于parserFactor遇到求导因子时求导因子的构建。经过分析不难发现,dx()括号内是表达式,所以我们可以先分析内部的表达式再求导。所以,我们只需再Expr与Item类中分别增加求导方法,Expr的求导依赖于Item的求导(Expr的求导其实就是其包含的Item求导结果之和),而Item的求导严格根据求导法则编写即可(法则如下图)
第三次的作业的实现很好地说明了目前架构面对新运算时的应对:添加新型因子,为Expr,Item编写新运算实现方法。
bug情况
本次作业强测互测没有出现bug,但在我自己测试的过程中,发现了自己程序的一些不足。程序在处理指数的方面仍存在不足,即当底数的式子比较复杂,指数次数较高(四次以上)时,程序会无法得出结果,出现超时的错误。最终我也没找到错误的具体位置,没有对这个潜在的错误点进行改正。
debug策略
我三次debug的积极性是逐渐降低的,因为大家的代码逐渐完善,加之有同学提供的测评机,大家的代码写的是逐渐完备,绞尽脑汁也不见得能找出一个bug。第一次互测中,我根据题干中的标准化输入,总结出了几个可能容易出错的点,像符号,括号与幂次,先构建了一个比较复杂的数据,结果就成功hack到了三个人,之后我又分解自己的数据,找出每个人的错误的具体原因再针对构造。第二次作业时,构造的数据就经常会出现cost过大的问题,这时候更应该有针对性的构造数据,针对可能出现的错误原因,构造一个简单的数据来测试,要相信,在大数据中出现的bug最终都能通过一个小数据来检测出来。第二次我们房间就是用1-1这种简单的数据找出了两个人的bug。第三次就完全开摆了,把大家的代码下载下来但是没有发现问题,按照前边的思路构造了几个简单数据拿了base分就结束了这次互测。
代码复杂度分析
(借用一下学长的表格)
方法衡量指标
指标 | 含义 |
---|---|
CogC(Cognitive complexity) 认知复杂度 | 衡量一个方法的控制流程有多困难去理解。具有高认知复杂度的方法将难以维护。sonar要求复杂度要在15以下。计算的大致思路是统计方法中控制流程语句的个数 |
ev(G)(essential cyclomatic complexity) 方法的基本圈复杂度 | 衡量程序非结构化程度的 |
iv(G) (Design complexity) :设计复杂度 | 字面意思 |
v(G)(cyclomatic complexity) :方法的圈复杂度 | 衡量判断模块的复杂度。数值越高说明独立路径越多,测试完备的难度越大 |
类的衡量指标
指标 | 含义 |
---|---|
OCavg(Average opearation complexity) | 平均操作复杂度 |
OCmax(Maximum operation complexity) | 最大操作复杂度 |
WMC(Weighted method complexity) | 加权方法复杂度 |
第一张图中我截取了部分复杂度较高的方法,研究比较后不难发现,if、for这样的语句嵌套层数越多,方法的复杂度越高,takeIn方法最多实现了四层这样的嵌套,varFactor也有三层。所以在编写代码时要尽量减少这样的情况,将for,if尽可能地提取形成新的方法,从而降低复杂度。
第二张图时类复杂度表,同样,包含复杂方法的类复杂度也会相应的高。对类,同样要尽可能地降低复杂度,增强代码的可读性与可理解性。
心得体会
首先,必须要感谢先导课的帮助,正是有了先导课的练习,我才能在第一次作业时较为熟练地使用idea与git。同时,先导课让我基本掌握了java的语法,如果没有先导课,我第一次作业估计就像没头苍蝇一样找不到头绪,甚至可能会无法完成,假使完成,也会在后边两次作业痛苦地进行重构。有了先导课的基础,让我能在有了思路之后便能够去得心应手的编写代码与调试,在一开始便去以一种面向对象的思想来编写程序。
至于我在第一单元的作业中学到了什么,我认为我现在能够做到大部分情况下以面向对象的想法来思考问题,对于一个新增的需求,首先思考的不是如何实现需求,而是怎样实现符合面向对象的思想。另外,第一次作业时我花费了大量时间在研究递归下降法上,因此对递归下降有了深刻的理解。最后,这三次作业提高了我对测试的重视,从第一次作业卡点提交,到最后第三次作业强测互测都没出现bug,这背后是大量测试的结果,想要实现程序的完美无错,充分测试是必不可少的,充分测试要比通过样例或中测更加重要。
未来方向
深刻的理解。最后,这三次作业提高了我对测试的重视,从第一次作业卡点提交,到最后第三次作业强测互测都没出现bug,这背后是大量测试的结果,想要实现程序的完美无错,充分测试是必不可少的,充分测试要比通过样例或中测更加重要。
未来方向
简单说一点我的建议:有些需求的设计感觉有些多余,像是第三次作业中的调用已定义函数以及不支持dx的多层嵌套,前者感觉大家在第二次作业的实现中就可以实现,后者感觉以大家的思路,实现多层嵌套似乎并不是什么困难的点。