OO第一单元总结
前言
本次OO第一单元总结将从如下几个部分展开:
1.三次作业迭代开发思路
2.整体架构分析
3.自动化生成数据及自动化测评实现思路
4.自我程序bug分析及测试手段
5.他人程序bug分析
6.hack别人程序bug策略
7.心得体会
8.鸣谢
复杂度分析利用IDEA的MetricsReloaded插件,代码规格统计利用IDEA的static插件
第一次作业
HW1基本思路
虽然笔者在寒假较为充分地预习了JAVA,但在面对第一次作业时仍然感到有一点无从下手,感到难度巨大,尤其是在面对一种叫做“递归下降”的前所未闻的方法时,笔者一度认为笔者过不了中测。可是事实证明课程组对作业的安排和设置是合理的,在潜下心来研究递归下降法后,笔者发现这种方法并没有想象中的这么难,只是一种基于文法的递归调用。当要解析一个表达式的时候,可以递归下降解析表达式中的每一个项;当要解析项的时候,可以递归下降每一个因子。解析每一个因子的时候,根据当前的token解析分类解析每一个因子(幂函数,常数,表达式因子)。这样一来,利用递归下降便可以很自然地处理括号嵌套的情况。因此,从第一次作业到第三次作业,笔者都能狗处理嵌套括号的情况(自己有空玩玩(1+(1+x)**2)**2也挺爽的)
在理解了这一点后,笔者又遇到了一个难点,应该采取什么数据结构来表示每个对象的状态?由于当时笔者仍然是面向过程思维(现在似乎也没有改善多少oao),简单地采取了采用HashMap<Integer, BigInteger>来保存每个x的指数和系数,虽然对于第一次作业这是完全没有问题的,但是对于第二次作业便捉襟见肘,不得不重构了。
UML类图
PowFunc、Expr、ConstantNum实现Factor接口便于实现多态进行递归调用,parser类依赖Lexer进行文法解析
代码规模
一共写了474行代码,似乎并不算多?
复杂度分析
方法复杂度
先解释一下ev(G)、 iv(G)、 v(G)的含义
ev(G)是基本复杂度,是用来衡量程序非结构化程度的,非结构成分降低了程序的质量,增加了代码的维护难度,使程序难于理解。因此,基本复杂度高意味着非结构化程度高,难以模块化和维护。实际上,消除了一个错误有时会引起其他的错误。
Iv(G)是模块设计复杂度,是用来衡量模块判定结构,即模块和其他模块的调用关系。软件模块设计复杂度高意味模块耦合度高,这将导致模块难于隔离、维护和复用。模块设计复杂度是从模块流程图中移去那些不包含调用子模块的判定和循环结构后得出的圈复杂度,因此模块设计复杂度不能大于圈复杂度,通常是远小于圈复杂度。
v(G)是圈复杂度,用来衡量一个模块判定结构的复杂程度,数量上表现为独立路径的条数,即合理的预防错误所需测试的最少路径条数,圈复杂度大说明程序代码可能质量低且难于测试和维护,经验表明,程序的可能错误和高的圈复杂度有着很大关系。
from: https://blog.csdn.net/weixin_30346033/article/details/97099084
| Method | CogC | ev(G) | iv(G) | v(G) |
|---|---|---|---|---|
| ConstantNum.ConstantNum(String) | 0 | 1 | 1 | 1 |
| ConstantNum.changeForNeg() | 3 | 1 | 3 | 3 |
| ConstantNum.getPowAndCoe() | 0 | 1 | 1 | 1 |
| ConstantNum.setNegative(boolean) | 0 | 1 | 1 | 1 |
| Expr.Expr() | 0 | 1 | 1 | 1 |
| Expr.addTerm(Term, boolean) | 0 | 1 | 1 | 1 |
| Expr.changeForNeg() | 3 | 1 | 3 | 3 |
| Expr.getPowAndCoe() | 0 | 1 | 1 | 1 |
| Expr.getStrFunc(int, BigInteger) | 11 | 3 | 7 | 8 |
| Expr.merge() | 3 | 1 | 3 | 3 |
| Expr.mergeByPow(int) | 7 | 1 | 5 | 5 |
| Expr.setNegative(boolean) | 0 | 1 | 1 | 1 |
| Expr.toString() | 8 | 1 | 5 | 5 |
| Lexer.Lexer(String) | 0 | 1 | 1 | 1 |
| Lexer.getNum() | 2 | 1 | 3 | 3 |
| Lexer.getPowX() | 5 | 1 | 4 | 4 |
| Lexer.getSignedNum() | 0 | 1 | 1 | 1 |
| Lexer.isPow() | 1 | 1 | 2 | 2 |
| Lexer.isSignedNum() | 1 | 1 | 3 | 3 |
| Lexer.moveBlank() | 3 | 2 | 2 | 4 |
| Lexer.next() | 8 | 2 | 6 | 7 |
| Lexer.peek() | 0 | 1 | 1 | 1 |
| Main.main(String[]) | 3 | 3 | 2 | 3 |
| Parser.Parser(Lexer) | 0 | 1 | 1 | 1 |
| Parser.parseExpr() | 8 | 1 | 6 | 6 |
| Parser.parseFactor() | 11 | 4 | 6 | 6 |
| Parser.parseTerm() | 4 | 1 | 4 | 4 |
| PowFunc.PowFunc(String) | 2 | 1 | 2 | 2 |
| PowFunc.changeForNeg() | 3 | 1 | 3 | 3 |
| PowFunc.getPowAndCoe() | 0 | 1 | 1 | 1 |
| PowFunc.setNegative(boolean) | 0 | 1 | 1 | 1 |
| Term.Term() | 0 | 1 | 1 | 1 |
| Term.addFactor(Factor, boolean) | 0 | 1 | 1 | 1 |
| Term.getPowAndCoe() | 0 | 1 | 1 | 1 |
| Term.merge() | 14 | 1 | 7 | 7 |
| Term.setNegetive(boolean) | 0 | 1 | 1 | 1 |
| Total | 100.0 | 45.0 | 93.0 | 98.0 |
| Average | 2.78 | 1.25 | 2.58 | 2.72 |
分析
可以看到,整体的复杂度并不高。但可以发现其中Parser类的方法复杂度较高,这是因为parser类作为递归下降的分析器,大量调用了Expr,Term,Factor的方法(将某一项设置为负数)。这一点是当初写代码的时候考虑得不够周全的地方,应该把每一项设置为负数和parser类解耦。
类复杂度
同样先解释一下OCavg、OCmax、 WMC的含义
OCavg是类平均圈复杂度, 继承类不计入
OCmax应该是类最大圈复杂度
WMC是类总圈复杂度
| Class | OCavg | OCmax | WMC |
|---|---|---|---|
| ConstantNum | 1.5 | 3 | 6 |
| Expr | 3 | 7 | 27 |
| Lexer | 2.33 | 7 | 21 |
| Main | 3 | 3 | 3 |
| Parser | 4 | 6 | 16 |
| PowFunc | 1.75 | 3 | 7 |
| Term | 2.2 | 7 | 11 |
| Total | 91.0 | ||
| Average | 2.53 | 5.1 | 13.0 |
分析
同样可以看到,parser类的复杂度比较高,理由同上。
优化策略
第一次作业由于只包含多项式,因此优化较为简单,笔者采用HashMap<Integer,Biginteger>作为每个因子,项,表达式的基本项,保存指数和系数。
化简时根据HashMap的指数和系数来进行合并同类项,由于HashMap存的是基本类型,因此也就不存在深拷贝和浅拷贝的问题。
可以做的优化有x**2->x*x,把正项提到前面。
但由于笔者太懒了,没有把第一项前面的"+"去掉,导致答案为1的时候会输出+1,因此强测碰到这个点性能分一下子就全没了= =
第二次作业
HW2基本思路
第二次作业在第一次作业的基础上增加了三角函数因子,自定义函数,求和函数等内容。
这样一来采用HashMap<Integer,Biginteger>作为每个因子,项,表达式的基本项的思路就完全行不通了,只能进行重构,笔者本来想用一个四元组作为基本项,每一个表达式,项,因子保存形如a*x**b*cos(x)**c*sin(x)**d的基本项,但这样一看就很不OO,并且在仔细阅读了指导书后,笔者发现还会有类似a*x**b*cos(x)**c*sin(x)**d*cos(x**e)**f*sin(x**g)**h这样的项,因此基本项的长度是不定的,所以这个方案也被否决了。为了更好地从第一次作业迭代到第二次作业,笔者最后决定新建一个类BasicTerm作为每个表达式,项,因子的基本项。
其中每个基本项有三个列表,保存幂函数,cos,sin因子,在BasicTerm中实现“BasicTerm相加运算”、“BasicTerm相乘运算”最后便可以类似作业一中使用HashMap<BasicTerm, BigInteger>化简(其中BigInter为每一个基本项的系数)。
对于文法解析器Parser和Lexer,由于第一次作业到第二次作业文法基本没有变化,所以不需要重构,只需要在Lexer增加对新增加的因子的解析,以及在Parser中实现对新增因子的递归下降即可。
对于新增的自定义函数,笔者的做法是字符串替换,即将对应的实参添加括号后替换到函数定义式中得到待解析的表达式,利用Parser和Lexer解析表达式,最后将解析完成的表达式返回。
对于新增的求和函数,笔者的做法也是字符串替换,先设置一个总表达式,然后将每个求和表达式中的i换成当前进行运算的数字,然后将"+替换后的表达式"加入到总表达式中,最后类似自定义函数对总表达式进行解析(现在看来似乎可以边代入边解析,而不必转化成一个总表达式再进行解析),然后将解析完成的表达式返回。
可以看到,在笔者的实现中,自定义函数和求和函数这两个因子其实最后在输入表达式中的地位就是一个表达式因子,只不过解析的方法与表达式因子有所不同。同时,要实现笔者对自定义函数和求和函数的解析方法,要求程序能支持对嵌套括号的处理,而对于递归下降法,这一点也就是自然而然的了。
最后,关于结果的输出方法,笔者是在顶层Expr的toString递归调用每个Term的toString,每个Term递归调用因子的toString这样递归输出
UML类图
其中Expr, Term, ConstantNum, PowFunc, Cos, Sin实现了Factor接口,由于自定义函数和求和函数只是返回的是表达式因子,而本质上只是一种“解析方法”,故不作为Factor接口的实现。
同时Term依赖Simplification类的静态方法进行化简。
代码规模
对比于第一次作业的474行代码,第二次作业总共为1251行代码,几乎是第一次作业的三倍,可以看出HW1到HW2是一个巨大的飞跃。
如果说第一次作业是从0到1,那第二次作业就是从1到n
| BasicTerm.java | 215 | 199 | 0.9255813953488372 | 1 | 0.004651162790697674 | 15 | 0.06976744186046512 |
|---|
在所有类中BasicTerm类的行数最多,共有215行,原因是它作为基本项,需要实现乘、加、相等这些基本运算,因此代码行数较高
复杂度分析
方法复杂度
| Method | CogC | ev(G) | iv(G) | v(G) |
|---|---|---|---|---|
| BasicTerm.BasicTerm(PowFunc, Cos, Sin) | 3 | 1 | 4 | 4 |
| BasicTerm.deepClone() | 0 | 1 | 1 | 1 |
| BasicTerm.equals(Object) | 4 | 3 | 4 | 6 |
| BasicTerm.equalsOfCosList(ArrayList |
10 | 6 | 4 | 7 |
| BasicTerm.equalsOfPowerList(ArrayList |
10 | 6 | 4 | 7 |
| BasicTerm.equalsOfSinList(ArrayList |
10 | 6 | 4 | 7 |
| BasicTerm.getCosxList() | 0 | 1 | 1 | 1 |
| BasicTerm.getPowerList() | 0 | 1 | 1 | 1 |
| BasicTerm.getSinxList() | 0 | 1 | 1 | 1 |
| BasicTerm.hashCode() | 0 | 1 | 1 | 1 |
| BasicTerm.mul(BasicTerm) | 0 | 1 | 1 | 1 |
| BasicTerm.mulOfCos(ArrayList |
15 | 2 | 6 | 7 |
| BasicTerm.mulOfPower(ArrayList |
15 | 2 | 6 | 7 |
| BasicTerm.mulOfSin(ArrayList |
15 | 2 | 6 | 7 |
| ConstantNum.ConstantNum(String) | 0 | 1 | 1 | 1 |
| ConstantNum.equals(Object) | 3 | 3 | 2 | 4 |
| ConstantNum.getBasicTerm() | 0 | 1 | 1 | 1 |
| ConstantNum.getNum() | 0 | 1 | 1 | 1 |
| ConstantNum.hashCode() | 0 | 1 | 1 | 1 |
| ConstantNum.mulOfNegOne() | 0 | 1 | 1 | 1 |
| ConstantNum.toString() | 0 | 1 | 1 | 1 |
| Cos.Cos(int, Factor) | 4 | 1 | 4 | 4 |
| Cos.addPow(int) | 0 | 1 | 1 | 1 |
| Cos.equals(Object) | 4 | 3 | 3 | 5 |
| Cos.getBasicTerm() | 0 | 1 | 1 | 1 |
| Cos.getFactor() | 0 | 1 | 1 | 1 |
| Cos.getPow() | 0 | 1 | 1 | 1 |
| Cos.hashCode() | 0 | 1 | 1 | 1 |
| Cos.toString() | 3 | 3 | 2 | 3 |
| DefinedFunc.DefinedFunc() | 0 | 1 | 1 | 1 |
| DefinedFunc.addDefinedFunc(String) | 1 | 1 | 2 | 2 |
| DefinedFunc.useDefinedFunc(String) | 1 | 1 | 2 | 2 |
| Expr.Expr() | 0 | 1 | 1 | 1 |
| Expr.addTerm(Term, boolean) | 0 | 1 | 1 | 1 |
| Expr.cosPowerIsZero(ArrayList |
3 | 3 | 2 | 3 |
| Expr.equals(Object) | 3 | 3 | 2 | 4 |
| Expr.getBasicTerm() | 0 | 1 | 1 | 1 |
| Expr.getStrFunc(BasicTerm, BigInteger) | 20 | 3 | 13 | 13 |
| Expr.hashCode() | 0 | 1 | 1 | 1 |
| Expr.merge() | 3 | 1 | 3 | 3 |
| Expr.mergeByPow(int) | 7 | 1 | 5 | 5 |
| Expr.powFuncPowerIsZero(ArrayList |
3 | 3 | 2 | 3 |
| Expr.sinPowerIsZero(ArrayList |
3 | 3 | 2 | 3 |
| Expr.toString() | 17 | 1 | 6 | 7 |
| Lexer.Lexer(String) | 0 | 1 | 1 | 1 |
| Lexer.getDefinedFunc() | 5 | 1 | 2 | 4 |
| Lexer.getNum() | 2 | 1 | 3 | 3 |
| Lexer.getPowX() | 5 | 1 | 4 | 4 |
| Lexer.getSignedNum() | 0 | 1 | 1 | 1 |
| Lexer.getSum() | 5 | 1 | 2 | 4 |
| Lexer.isCos() | 1 | 1 | 2 | 2 |
| Lexer.isDefinedFunc() | 1 | 1 | 2 | 2 |
| Lexer.isPow() | 1 | 1 | 2 | 2 |
| Lexer.isSignedNum() | 1 | 1 | 3 | 3 |
| Lexer.isSin() | 1 | 1 | 2 | 2 |
| Lexer.isSum() | 1 | 1 | 2 | 2 |
| Lexer.moveBlank() | 3 | 2 | 2 | 4 |
| Lexer.next() | 12 | 2 | 10 | 11 |
| Lexer.peek() | 0 | 1 | 1 | 1 |
| Main.main(String[]) | 1 | 1 | 2 | 2 |
| Parser.Parser(Lexer) | 0 | 1 | 1 | 1 |
| Parser.addDefinedFunc(DefinedFunc) | 0 | 1 | 1 | 1 |
| Parser.parseCosFactor() | 3 | 1 | 3 | 3 |
| Parser.parseExpr() | 8 | 1 | 6 | 6 |
| Parser.parseExprFactor() | 3 | 1 | 3 | 3 |
| Parser.parseFactor() | 10 | 8 | 8 | 8 |
| Parser.parseSinFactor() | 3 | 1 | 3 | 3 |
| Parser.parseTerm() | 4 | 1 | 4 | 4 |
| PowFunc.PowFunc(String) | 2 | 1 | 2 | 2 |
| PowFunc.addPow(int) | 0 | 1 | 1 | 1 |
| PowFunc.equals(Object) | 4 | 3 | 3 | 5 |
| PowFunc.getBasicTerm() | 0 | 1 | 1 | 1 |
| PowFunc.getPow() | 0 | 1 | 1 | 1 |
| PowFunc.getVarType() | 0 | 1 | 1 | 1 |
| PowFunc.hashCode() | 0 | 1 | 1 | 1 |
| PowFunc.toString() | 3 | 3 | 2 | 3 |
| Simplification.simplyPow(HashMap<BasicTerm, BigInteger>) | 1 | 1 | 2 | 2 |
| Sin.Sin(int, Factor) | 7 | 1 | 5 | 5 |
| Sin.addPow(int) | 0 | 1 | 1 | 1 |
| Sin.equals(Object) | 4 | 3 | 3 | 5 |
| Sin.getBasicTerm() | 0 | 1 | 1 | 1 |
| Sin.getFactor() | 0 | 1 | 1 | 1 |
| Sin.getPow() | 0 | 1 | 1 | 1 |
| Sin.hashCode() | 0 | 1 | 1 | 1 |
| Sin.toString() | 3 | 3 | 2 | 3 |
| SumFunc.SumFunc() | 0 | 1 | 1 | 1 |
| SumFunc.sumOfFunc(String) | 2 | 2 | 2 | 3 |
| Term.Term() | 0 | 1 | 1 | 1 |
| Term.addFactor(Factor) | 0 | 1 | 1 | 1 |
| Term.cloneMap(HashMap<BasicTerm, BigInteger>) | 1 | 1 | 2 | 2 |
| Term.equals(Object) | 4 | 3 | 3 | 5 |
| Term.getBasicTerm() | 0 | 1 | 1 | 1 |
| Term.hashCode() | 0 | 1 | 1 | 1 |
| Term.merge() | 14 | 1 | 7 | 7 |
| Term.setNegative(boolean) | 0 | 1 | 1 | 1 |
| Total | 272.0 | 151.0 | 229.0 | 270.0 |
| Average | 2.863157894736842 | 1.5894736842105264 | 2.4105263157894736 | 2.8421052631578947 |
分析
对比第一次作业的
| Total | 100.0 | 45.0 | 93.0 | 98.0 |
|---|---|---|---|---|
| Average | 2.78 | 1.25 | 2.58 | 2.72 |
可以看到第二次作业的Iv(G)模块设计复杂度相对于第一次作业有所下降,但基本复杂度和圈复杂度都有所增加。
笔者认为原因在于第二次作业增加了新的因子,Parser和Lexer需要实现对新因子的解析,正是这两个类的对因子的解析造成了复杂度的提高。
类复杂度
| Class | OCavg | OCmax | WMC |
|---|---|---|---|
| BasicTerm | 3.71 | 7 | 52 |
| ConstantNum | 1.29 | 3 | 9 |
| Cos | 1.88 | 4 | 15 |
| DefinedFunc | 1.67 | 2 | 5 |
| Expr | 3.5 | 11 | 42 |
| Lexer | 2.47 | 11 | 37 |
| Main | 2 | 2 | 2 |
| Parser | 3.5 | 8 | 28 |
| PowFunc | 1.62 | 3 | 13 |
| Simplification | 2 | 2 | 2 |
| Sin | 2 | 5 | 16 |
| SumFunc | 2 | 3 | 4 |
| Term | 2.12 | 7 | 17 |
| Total | 242.0 | ||
| Average | 2.5473684210526315 | 5.230769230769231 | 18.615384615384617 |
分析
Expr、Lexer、Parser类的复杂度较高。其中Lexer、Parser类的复杂度原因上文已经分析,现在分析一下Expr的复杂度。
在重新看了看代码后,笔者发现笔者在Expr类里实现的toString方法需要判断基本项中的每个列表中的因子指数是否为0,因此与cos、sin、powFunc三个类的耦合较为紧密。
笔者感觉应该可以再新建一个类,专门实现这些基本方法的判断,与Expr类解耦,降低复杂度。
优化策略
由于第二次作业新增了三角因子,因此可以做的优化变得很多。
其中较难实现的是三角函数的平方和cos**2+sin**2=1优化。
笔者也尝试做过平方和的优化,可惜能力有限并未成功,每次在评测机上跑一两百组数据就会出错,于是只能把平方和优化注释掉了。。。但除了平方和以外,能做的小优化还是很多的,除了第一次作业的优化以外,笔者还实现了比如cos(0)=1,sin(0)=0,sin(-1)=sin(1),cos(-1)=cos(1)等优化。
第三次作业
HW3基本思路
第三次作业可以称得上是OO第一单元最简单的一次作业了。
相比与第二次作业,第三次作业增加的内容有:
1.嵌套括号
2.三角函数因子嵌套
3.自定义函数嵌套调用自定义函数
4.求和函数的求和表达式因子种类增加
可以看出,第三次作业相比与第二次作业其实没有增加什么东西。与前两次作业最大的不同就是第三次作业增加了对嵌套因子,嵌套括号的处理,而这也是笔者觉得第三次作业主要想考察的地方。然鹅,对于递归下降的解析方法来说,处理嵌套因子,嵌套括号本来就是自然而然的事情,因此从第二次作业到第三次作业,笔者几乎不用改动,这也是笔者第一次体会到好的架构设计带来的好处。
当然,几乎不用改动并不是不需要改动。第三次作业由于需要实现自定义函数嵌套调用自定义函数,笔者在第二次作业简单的字符串替换变无法处理了。但解决的办法也很简单,对于自定义函数的调用复用递归下降的办法,对每一个调用因子进行解析,再把解析完成的表达式代入函数表达式中,最后替换完因子后得到一个新的表达式,再对这个表达式进行解析。除此之外,没有需要大改的地方了。
UML类图
可以看到,第三次作业的UML图与第二次作业的UML图几乎没有变化,新增的DefinedLexer类继承了Lexer类,重写了next方法,实现对自定义函数的文法解析
代码规模
对比于第二次作业的1251行代码,第二次作业总共为1529行代码,主要是增加了DefinedLexer类。其实这次实际的代码只有1287行,为什么呢?因为一大堆失败的优化代码被笔者注释掉了(0.0)
复杂度分析
方法复杂度
| Method | CogC | ev(G) | iv(G) | v(G) |
|---|---|---|---|---|
| BasicTerm.BasicTerm(PowFunc, Cos, Sin) | 3 | 1 | 4 | 4 |
| BasicTerm.deepClone() | 0 | 1 | 1 | 1 |
| BasicTerm.equals(Object) | 4 | 3 | 4 | 6 |
| BasicTerm.equalsOfCosList(ArrayList<Cos>) | 10 | 6 | 4 | 7 |
| BasicTerm.equalsOfPowerList(ArrayList<PowFunc>) | 10 | 6 | 4 | 7 |
| BasicTerm.equalsOfSinList(ArrayList<Sin>) | 10 | 6 | 4 | 7 |
| BasicTerm.getCosxList() | 0 | 1 | 1 | 1 |
| BasicTerm.getPowerList() | 0 | 1 | 1 | 1 |
| BasicTerm.getSinxList() | 0 | 1 | 1 | 1 |
| BasicTerm.hashCode() | 0 | 1 | 1 | 1 |
| BasicTerm.mul(BasicTerm) | 0 | 1 | 1 | 1 |
| BasicTerm.mulOfCos(ArrayList<Cos>) | 15 | 2 | 6 | 7 |
| BasicTerm.mulOfPower(ArrayList<PowFunc>) | 15 | 2 | 6 | 7 |
| BasicTerm.mulOfSin(ArrayList<Sin>) | 15 | 2 | 6 | 7 |
| ConstantNum.ConstantNum(String) | 0 | 1 | 1 | 1 |
| ConstantNum.equals(Object) | 3 | 3 | 2 | 4 |
| ConstantNum.getBasicTerm() | 0 | 1 | 1 | 1 |
| ConstantNum.getNum() | 0 | 1 | 1 | 1 |
| ConstantNum.hashCode() | 0 | 1 | 1 | 1 |
| ConstantNum.toString() | 1 | 2 | 2 | 2 |
| Cos.Cos(int, Factor) | 0 | 1 | 1 | 1 |
| Cos.addPow(int) | 0 | 1 | 1 | 1 |
| Cos.equals(Object) | 5 | 3 | 4 | 6 |
| Cos.getBasicTerm() | 0 | 1 | 1 | 1 |
| Cos.getFactor() | 0 | 1 | 1 | 1 |
| Cos.getPow() | 0 | 1 | 1 | 1 |
| Cos.hashCode() | 0 | 1 | 1 | 1 |
| Cos.simply() | 6 | 2 | 5 | 5 |
| Cos.toString() | 21 | 8 | 13 | 14 |
| DefinedFunc.DefinedFunc() | 0 | 1 | 1 | 1 |
| DefinedFunc.addDefinedFunc(String) | 1 | 1 | 2 | 2 |
| DefinedFunc.getBasicTerm() | 0 | 1 | 1 | 1 |
| DefinedFunc.getDefinedArgument(String) | 1 | 1 | 2 | 2 |
| DefinedFunc.useDefinedFunc(String) | 1 | 1 | 2 | 2 |
| DefinedLexer.DefinedLexer(String) | 0 | 1 | 1 | 1 |
| DefinedLexer.next() | 13 | 2 | 11 | 12 |
| Expr.Expr() | 0 | 1 | 1 | 1 |
| Expr.addTerm(Term, boolean) | 0 | 1 | 1 | 1 |
| Expr.cosPowerIsZero(ArrayList<Cos>) | 3 | 3 | 2 | 3 |
| Expr.equals(Object) | 3 | 3 | 2 | 4 |
| Expr.getBasicTerm() | 0 | 1 | 1 | 1 |
| Expr.getStrFunc(BasicTerm, BigInteger) | 20 | 3 | 13 | 13 |
| Expr.hashCode() | 0 | 1 | 1 | 1 |
| Expr.merge() | 3 | 1 | 3 | 3 |
| Expr.mergeByPow(int) | 7 | 1 | 5 | 5 |
| Expr.powFuncPowerIsZero(ArrayList<PowFunc>) | 3 | 3 | 2 | 3 |
| Expr.sinPowerIsZero(ArrayList<Sin>) | 3 | 3 | 2 | 3 |
| Expr.toString() | 17 | 1 | 6 | 7 |
| Lexer.Lexer(String) | 0 | 1 | 1 | 1 |
| Lexer.getDefinedFunc() | 5 | 1 | 2 | 4 |
| Lexer.getInput() | 0 | 1 | 1 | 1 |
| Lexer.getNum() | 2 | 1 | 3 | 3 |
| Lexer.getPos() | 0 | 1 | 1 | 1 |
| Lexer.getPowX() | 5 | 1 | 4 | 4 |
| Lexer.getSignedNum() | 0 | 1 | 1 | 1 |
| Lexer.getSum() | 5 | 1 | 2 | 4 |
| Lexer.isCos() | 1 | 1 | 2 | 2 |
| Lexer.isDefinedFunc() | 1 | 1 | 2 | 2 |
| Lexer.isPow() | 1 | 1 | 2 | 2 |
| Lexer.isSignedNum() | 1 | 1 | 3 | 3 |
| Lexer.isSin() | 1 | 1 | 2 | 2 |
| Lexer.isSum() | 1 | 1 | 2 | 2 |
| Lexer.moveBlank() | 3 | 2 | 2 | 4 |
| Lexer.next() | 12 | 2 | 10 | 11 |
| Lexer.peek() | 0 | 1 | 1 | 1 |
| Lexer.setPos(int) | 0 | 1 | 1 | 1 |
| Lexer.setToken(String) | 0 | 1 | 1 | 1 |
| Main.main(String[]) | 1 | 1 | 2 | 2 |
| Parser.Parser(Lexer) | 0 | 1 | 1 | 1 |
| Parser.addDefinedFunc(DefinedFunc) | 0 | 1 | 1 | 1 |
| Parser.parseCosFactor() | 3 | 1 | 3 | 3 |
| Parser.parseExpr() | 8 | 1 | 6 | 6 |
| Parser.parseExprFactor() | 3 | 1 | 3 | 3 |
| Parser.parseFactor() | 10 | 8 | 8 | 8 |
| Parser.parseSinFactor() | 3 | 1 | 3 | 3 |
| Parser.parseTerm() | 4 | 1 | 4 | 4 |
| PowFunc.PowFunc(String) | 2 | 1 | 2 | 2 |
| PowFunc.addPow(int) | 0 | 1 | 1 | 1 |
| PowFunc.equals(Object) | 4 | 3 | 3 | 5 |
| PowFunc.getBasicTerm() | 0 | 1 | 1 | 1 |
| PowFunc.getPow() | 0 | 1 | 1 | 1 |
| PowFunc.getVarType() | 0 | 1 | 1 | 1 |
| PowFunc.hashCode() | 0 | 1 | 1 | 1 |
| PowFunc.toString() | 6 | 3 | 3 | 4 |
| Simplification.simplyPow(HashMap<BasicTerm, BigInteger>) | 1 | 1 | 2 | 2 |
| Sin.Sin(int, Factor) | 0 | 1 | 1 | 1 |
| Sin.addPow(int) | 0 | 1 | 1 | 1 |
| Sin.equals(Object) | 5 | 3 | 4 | 6 |
| Sin.getBasicTerm() | 0 | 1 | 1 | 1 |
| Sin.getFactor() | 0 | 1 | 1 | 1 |
| Sin.getPow() | 0 | 1 | 1 | 1 |
| Sin.hashCode() | 0 | 1 | 1 | 1 |
| Sin.simpliy() | 14 | 2 | 7 | 7 |
| Sin.toString() | 18 | 6 | 10 | 11 |
| SumFunc.SumFunc() | 0 | 1 | 1 | 1 |
| SumFunc.getBasicTerm() | 0 | 1 | 1 | 1 |
| SumFunc.sumOfFunc(String) | 2 | 2 | 2 | 3 |
| Term.Term() | 0 | 1 | 1 | 1 |
| Term.addFactor(Factor) | 0 | 1 | 1 | 1 |
| Term.cloneMap(HashMap<BasicTerm, BigInteger>) | 1 | 1 | 2 | 2 |
| Term.equals(Object) | 4 | 3 | 3 | 5 |
| Term.getBasicTerm() | 0 | 1 | 1 | 1 |
| Term.hashCode() | 0 | 1 | 1 | 1 |
| Term.merge() | 14 | 1 | 7 | 7 |
| Term.setNegative(boolean) | 0 | 1 | 1 | 1 |
| Total | 334.0 | 173.0 | 276.0 | 318.0 |
| Average | 3.1809523809523808 | 1.6476190476190475 | 2.6285714285714286 | 3.0285714285714285 |
分析
这一次作业的复杂度相较于前两次均有所增加,从上表可以看到方法复杂度较高的有Sin.simpliy(),Sin.toString(),Cos.toString(),BasicTerm.equalsOfPowerList(ArrayList<PowFunc>),Expr.getStrFunc(BasicTerm, BigInteger),Expr.toString()等。
观察上述方法,可以发现这些方法大多与输出有关,其中cos和sin的toString方法更是iv(G)异常的大。
在重新阅读笔者的代码后,笔者发现这是因为笔者在sin和cos的toString方法中大量使用了if-else来进行输出的化简,比如利用正则表达式匹配x**2,替换成x*x。
现在看来,似乎可以这些化简处理抽象成一个类,专门交给这个类去处理,与cos和sin解耦。
类复杂度
| Class | OCavg | OCmax | WMC |
|---|---|---|---|
| BasicTerm | 3.71 | 7 | 52 |
| ConstantNum | 1.5 | 3 | 9 |
| Cos | 2.78 | 11 | 25 |
| DefinedFunc | 1.6 | 2 | 8 |
| DefinedLexer | 6.5 | 12 | 13 |
| Expr | 3.5 | 11 | 42 |
| Lexer | 2.16 | 11 | 41 |
| Main | 2 | 2 | 2 |
| Parser | 3.5 | 8 | 28 |
| PowFunc | 1.75 | 4 | 14 |
| Simplification | 2 | 2 | 2 |
| Sin | 2.78 | 9 | 25 |
| SumFunc | 1.67 | 3 | 5 |
| Term | 2.12 | 7 | 17 |
| Total | 283.0 | ||
| Average | 2.6952380952380954 | 6.571428571428571 | 20.214285714285715 |
分析
如果你仔细地看了上面的表格,相信聪明的你不难发现有一个类的复杂度特别的大,就是DefinedFunc
| DefinedLexer | 6.51.6 | 122 | 138 |
|---|
这是为什么呢?因为这个类是继承了Lexer类用于解析自定义函数因子,自然需要用到Lexter类中的pos,token等成员变量。
但课程组要求我们每一个类的成员变量应该是private的,连protected都不能用,于是我只能为Lexter类实现了getpos,setpos,gettoken等方法。这看起来似乎不是一个好办法,将Lexer的成员暴露了,Lexter本应该只对外部暴露他的状态。只可惜我没想到太好的办法,只能出此下策。
同时BasicTerm,Expr,Term等类的复杂度也较高,原因在HW2已经阐述,这里就不再赘述了。
优化策略
相比与第二次作业,这次作业支持了三角函数里面添加表达式因子嵌套,因此在此基础上可以做二倍角优化2*cos(x)*sin(x)=sin((2*x)),但经过上一次优化失败的教训后笔者也不敢再优化了,实在是太容易出bug了,于是也没做二倍角优化。这次作业的优化和HW2相同。
听说有人做了二倍角和平方和优化,并通过比较不同优化的长度,搜索到最长的输出,如果超时就及时熔断。但笔者觉得除非特别厉害,否则这样就是在刀尖上起舞(事实证明的确是这样,详见下文)。
整体架构分析
从第一次作业到第三次作业,我的程序紧紧围绕着一个基本项展开,在第一次作业中他是保存有指数和系数的HashMap<Integer, BigInteger>;第二、三次作业中,他是一个类BasicTerm。但这样的设计我觉得并不好,因为并不利于迭代开发。一个基本项显然只能涵盖较为简单的情况,比如三角函数、幂函数这样的,一旦表达式中的函数因子多了起来,这个基本项也会变得极为庞大和复杂,显然与OO的精神违背。
同时我的化简运算merge()也是各自在term类和Expr类中实现的,term中是乘,Expr中是加。这样我也感觉并不好,因为term现在只有乘的运算,但如果有取模和除法运算呢?想必我就要修改原来的term类了。而这种运算并不是term类应该具有的能力,这种运算应该交给“运算类”专门去做,而不是在term类中实现。
现在想来,一个好的设计应该是表达式中每一种因子,每一种运算,每一项,都应该成为一个类,即使需求变了,也只需要增添新的运算类和因子类即可,无需或者很少需要改动原来已经设计好的代码。
数据生成及自动化评测
在OO第一单元的每次作业中,笔者都使用Python及其xeger库写了数据生成器,并在第二次作业和第三次作业实现了自动化评测机
第一次作业
数据生成
根据指导书中的设定的形式化表述
- 表达式 → 空白项 [加减 空白项] 项 空白项 | 表达式 加减 空白项 项 空白项
- 项 → [加减 空白项] 因子 | 项 空白项 * 空白项 因子
- 因子 → 变量因子 | 常数因子 | 表达式因子
- 变量因子 → 幂函数
- 常数因子 → 带符号的整数
我们可以自底向上构造测试用例,首先构造因子中的变量因子和常数因子,通过这两个因子构造没有表达式因子的项,再通过没有表达式因子的项,构造没有括号的表达式,通过把没有括号的表达式加上括号,我们就得到了表达式因子,再用三种因子构造含有表达式因子的项,最后用含有表达式因子的项生成表达式
其中,变量因子和常数因子的构造可以使用Python的xeger库(根据正则表达式随机生成字符串)
再通过调节不同因子的生成概率和程度,可以得到不同强度的数据
缺点:对于边界数据难以生成,可以考虑添加边界数据常量池,在生成表达式时随机添加
代码示例
展开代码
from xeger import Xeger
import random
powFun = 'x( ){0}(\*\*( ){0}\+0{0}[0-4])?'
const = '(\+|-)0{0}[1-9]{1}'
x = Xeger(limit=100)
x._cases['any'] = lambda x: '.'
x._alphabets['whitespace'] = ' '
def generTermNotExp(x,notpre):
n = random.randint(1,1)
m = random.randint(1,3)
s = ""
if not notpre:
if m==1:
s+="+"
elif m==2:
s+='-'
for i in range(n):
if random.random()>0.5:
s+=x.xeger(powFun)
else:
s+=x.xeger(const)
if i < n - 1:
s+="*"
return s
def generExpNotBrac(x,notpre):
n = random.randint(1,1)
m = random.randint(1,3)
s = ""
if not notpre:
if m==1:
s+="+"
elif m==2:
s+='-'
s+=generTermNotExp(x, False)
for i in range(n):
if random.random()>0.5:
s+="+"
else:
s+="-"
s+=generTermNotExp(x,True)
return s
def generTermWithBracket(x, notpre):
n = random.randint(2,3)
m = random.randint(1,3)
s = ""
if notpre:
if m==1:
s+="+"
elif m==2:
s+='-'
for i in range(n):
tmp = random.random()
if tmp > 0.80:
s+=x.xeger(powFun)
elif 0.60 < tmp <= 0.80:
s+=x.xeger(const)
elif tmp<=0.60:
s+="("+generExpNotBrac(x,False)+")"
s+="**"+"+"+str(random.randint(1,2))
if i < n - 1:
s+="*"
return s
def generExpWithBracket(x):
n = random.randint(2,2)
m = random.randint(1,3)
s = ""
if m==1:
s+="+"
elif m==2:
s+='-'
for i in range(n):
s+=generTermWithBracket(x,True)
if i < n - 1:
if random.random()>0.9:
s+="+"
else:
s+="-"
return s
for i in range(100):
s = generExpWithBracket(x)
print(s)
自动化评测
本次作业未实现自动化评测