一、题目分析
本单元的三次作业均为解决表达式解析类的问题及进行增量开发。最初需要实现的需求是解决表达式化简问题,在之后的两次作业则是增量开发了三角函数、自定义函数和求导的功能。
在学习数据结构时我们曾用逆波兰表达式的形式解决过相对简单的表达式计算问题,但当时并没有学习过OOpre或OO课程,所以并没有面向对象的意识。在上学期进行了OOpre的学习后,可以说已经对面向对象有了初步的理解,因而在实现本单元的题目时,经过与老师、助教、同学的交流,选择了之前学习过的递归下降法解决本单元的问题。
二、程序结构分析
下述数据均为实现基本功能的代码部分分析数据,复杂度较高的运算结果合并部分的代码(shi山)未计入。
度量类和方法
类名 | 属性数 | 方法数 |
Expr | 1 | 10 |
Term | 2 | 4 |
Factor | 4 | 11 |
Parser | 1 | 4 |
Lexer | 2 | 8 |
Mainclass | 0 | 1 |
Poly | 1 | 2 |
Operation | 0 | 6 |
下述为利用Calculate Metrics插件进行的代码分析得到相关数据
Class | OCavg | OCmax | WMC |
Expr | 2.7 | 5.0 | 27.0 |
Factor | 2.4545454545454546 | 13.0 | 27.0 |
Lexer | 1.875 | 3.0 | 15.0 |
MainClass | 1.0 | 1.0 | 1.0 |
Operation | 4.666666666666667 | 7.0 | 14.0 |
Parser | 2.5 | 4.0 | 10.0 |
Poly | 1.3333333333333333 | 2.0 | 4.0 |
Term | 2.2 | 6.0 | 11.0 |
Total | 109.0 | ||
Average | 2.422222222222222 | 5.125 | 13.625 |
Method | CogC | ev(G) | iv(G) | v(G) |
Term.Term() | 0.0 | 1.0 | 1.0 | 1.0 |
Term.getFactors() | 0.0 | 1.0 | 1.0 | 1.0 |
Term.changeSign() | 2.0 | 1.0 | 1.0 | 2.0 |
Term.calculate() | 7.0 | 1.0 | 6.0 | 6.0 |
Term.addFactor(Factor) | 0.0 | 1.0 | 1.0 | 1.0 |
Poly.toString() | 1.0 | 1.0 | 2.0 | 2.0 |
Poly.getPoly() | 0.0 | 1.0 | 1.0 | 1.0 |
Poly.addPoly(Factor) | 0.0 | 1.0 | 1.0 | 1.0 |
Parser.parseTerm() | 2.0 | 1.0 | 3.0 | 3.0 |
Parser.Parser(Lexer) | 0.0 | 1.0 | 1.0 | 1.0 |
Parser.parseFactor() | 6.0 | 2.0 | 4.0 | 4.0 |
Parser.parseExpr() | 5.0 | 1.0 | 6.0 | 6.0 |
Operation.judgeSign(String) | 5.0 | 6.0 | 4.0 | 6.0 |
Operation.cutSign(String) | 7.0 | 1.0 | 7.0 | 7.0 |
Operation.cut(String) | 0.0 | 1.0 | 1.0 | 1.0 |
MainClass.main(String[]) | 0.0 | 1.0 | 1.0 | 1.0 |
Lexer.move() | 0.0 | 1.0 | 1.0 | 1.0 |
Lexer.Lexer(String) | 0.0 | 1.0 | 1.0 | 1.0 |
Lexer.judgePow() | 2.0 | 2.0 | 2.0 | 3.0 |
Lexer.judgeExprPow() | 5.0 | 3.0 | 4.0 | 5.0 |
Lexer.judgeENd() | 1.0 | 2.0 | 1.0 | 2.0 |
Lexer.getnow() | 0.0 | 1.0 | 1.0 | 1.0 |
Lexer.getNext() | 2.0 | 1.0 | 3.0 | 3.0 |
Lexer.dealSign(Term) | 2.0 | 1.0 | 2.0 | 3.0 |
Factor.toString() | 28.0 | 2.0 | 13.0 | 13.0 |
Factor.setNum(BigInteger) | 0.0 | 1.0 | 1.0 | 1.0 |
Factor.setFactor(BigInteger, int, int, int) | 0.0 | 1.0 | 1.0 | 1.0 |
Factor.pow(int) | 1.0 | 1.0 | 2.0 | 2.0 |
Factor.multFactor(Factor) | 0.0 | 1.0 | 1.0 | 1.0 |
Factor.getZindex() | 0.0 | 1.0 | 1.0 | 1.0 |
Factor.getYindex() | 0.0 | 1.0 | 1.0 | 1.0 |
Factor.getXindex() | 0.0 | 1.0 | 1.0 | 1.0 |
Factor.getNum() | 0.0 | 1.0 | 1.0 | 1.0 |
Factor.getin(String) | 4.0 | 1.0 | 3.0 | 4.0 |
Factor.copy(Factor) | 0.0 | 1.0 | 1.0 | 1.0 |
Expr.toString() | 1.0 | 1.0 | 2.0 | 2.0 |
Expr.sum() | 6.0 | 1.0 | 4.0 | 4.0 |
Expr.powExpr(int) | 6.0 | 1.0 | 4.0 | 4.0 |
Expr.multFactor(Factor) | 1.0 | 1.0 | 2.0 | 2.0 |
Expr.multExpr(Expr) | 9.0 | 1.0 | 5.0 | 5.0 |
Expr.mergeTerm(Term, Term) | 2.0 | 3.0 | 1.0 | 3.0 |
Expr.getTerms() | 0.0 | 1.0 | 1.0 | 1.0 |
Expr.Expr() | 0.0 | 1.0 | 1.0 | 1.0 |
Expr.calculate() | 6.0 | 1.0 | 4.0 | 4.0 |
Expr.addTerm(Term) | 0.0 | 1.0 | 1.0 | 1.0 |
Total | 111.0 | 58.0 | 107.0 | 117.0 |
Average | 2.466666666666667 | 1.288888888888889 | 2.3777777777777778 | 2.6 |
类的内聚和耦合分析
1、对于容器类Expr、Term、Factor、Poly类,Expr和Poly继承了Factor类,与其他类之间互相干扰较小,一定程度实现了高内聚。
2、对于复杂度最高的Operation类,为了解决函数、求导的链式法则等问题, 其中包含了较多需要嵌套调用的方法,因而复杂度变高。
类图
架构设计
在HW1中,需要实现的功能是对给出的表达式进行拆括号化简。选择采用递归下降的方法对表达式解析和计算。
对此,我设计了Expr、Term、Factor三个容器类,其中Expr类继承Factor类,表示一个表达式可以为表达式因子。
在解析表达式的过程中,将整个表达式的解析结果存为一个由若干项Term构成的Expr,而Term中储存若干因子,这里的因子可以是Factor也可以是Expr,对于表达式因子Expr又是由Term组成。其中Expr储存的Term间的关系是+,Term中储存的Factor间的关系是*。这样就形成了一个初步的递归下降结构。
//简易表示
class Expr extends Factor {
private Arraylist<Term> terms;
}
class Term {
private Arraylist<Factor> factors;
}
class Factor {
private BigInteger;
private int xindex;
private int yindex;
private int zindex;
}
此外我设计了Poly类用于最后对得到了最顶层Expr进行合并同类项等计算化简,其同样继承了Factor类。
再加以Parser、Lexer等抽象类,就可以完成HW1的表达式化简。
在HW2中,需要实现的三个功能分别是括号嵌套、三角函数、自定义函数。
其中括号嵌套功能由于在HW1中采用的是递归下降法,已经很自然的实现了这个功能。
对于三角函数功能,首先需要在lexer中增加解析三角函数的功能,并且在Expr中增加一个起到标记作用的int类型变量,用以区分该Expr是表达式还是sin内的Expr或cos内的Expr,当三角函数作为因子储存在Expr->Term内时,可将其视作三角表达式因子,符合之前的设计中Factor可为普通因子也可为表达式因子的原则。此外还要在Expr中增加int类型变量index,用以代表三角函数的指数。
(但这里可以很直接的发现,将Expr类扩展成为一个可以额外满足三角函数需求的类,并不符合高内聚低耦合的原则,我的这种设计并不是非常好的设计)
对于自定义函数功能,在设计结构时我首先考虑的是处理形参与实参间的关系,对此我采用的是简单的字符串替换方法,首先将函数定义时的f(y,x)=y**2+x转为f(paraX,paraY)=paraX**2+paraY。之后在表达式中调用该函数时,将实参传入,返回一个Term,其中储存着自定义函数生成的表达式因子、三角表达式因子、普通因子等。
此外在改写toString方法时,我也再次意识到了我的这种结构的不足,造成的结果是增加了较多的分支,复杂度也上升,同时还需要考虑很多条件语句中是否需要判断三角表达式等额外的思考,比较繁琐。
在HW3中需要实现的功能是增加求导因子和增加函数之间的相互调用(不能调用未定义的函数与自身)。
对于后者,可以直接按照HW2的思路去实现定义新函数时调用已定义过的函数,实现这个功能并不复杂。
对于求导,首先要意识到的是Term求导得到Expr,Factor求导得到Term。要用的法则为链式法则和乘法法则。
[f(g(x))]′=f′(g(x))g′(x)
[f(x)g(x)]′=f′(x)g(x)+f(x)g′(x)
我采用的思路是先解析再求导,实现起来并不复杂。
自我分析Bug
在设计代码的阶段,我会根据题目的要求去设置一个存在较多细节点的数据,并根据这个数据进行细节构思,这样可以提前注意到很多可能遗漏的分支,从而避免bug的产生。
在提交作业之后,如果有错误点,可以根据错误点的对应数据去进行调试,这是最简单的发现bug的方式。但如果该数据点没有提供数据,那么就需要自己去不断尝试构造或者通过在交流群中和其他同学交流来找到可能存在的bug。
提前进行特殊数据的测试,避免自己因为没考虑到一些极端数据而造成输出错误或运行错误。
还可以采用测评机的方式进行bug测试(感谢讨论区里共享测评机的同学)。
发现别人Bug策略
发现别人bug的最主要方式还是设置一些根据文法定义的特殊数据点去进行hack,以及一些复杂度较高的数据点。
此外,阅读同学的代码也是一种hack方式,在这个过程中也确实看到了其他同学非常优秀的思路和架构。
(也可以用测评机多跑跑大家的代码,确实也能发现bug)
心得体会
在本单元之初,由于缓考等许多事情占据了不少的事情,作业的完成并不是非常顺利,但好在上学期进行了OOpre的学习,有了一定的基础,因此也能够勉强完成作业。
在整个过程中,我能够明显的感受到自己的代码从面向过程的角度逐渐朝着面向对象的角度转变,尤其是在两次实验课之后,我仔细分析了两次实验课上课程组提供的代码,心中真的会感叹一句“浑然天成”,读完整个代码会感觉没有一行代码是重复的、没有一个方法是与另外的方法功能大幅重合的。这两次实验课的代码给了我很大的启发,甚至我的第一次作业中的架构就有一些对实验课代码的借鉴。同时,这也给我立下了学习的方向和目标,在之后的学习中要尽力写出这样的代码。
在本单元的两次研讨课上,我的同学们提到了很多新颖的思路和结构,给了我很大的启发。有一位同学在分享中这样说:“好的代码需要好的架构,好的架构写出的代码也需要大量的测试。”经过本单元三次作业的迭代,我能非常明显的感受到好的架构带来的便利。同时,在不断调试、找bug的过程中,也印证了充分测试的必要性。
经过本单元的学习,我收获良多,我十分感谢在作业的完成过程中给我鼓励和帮助的助教和同学。在对这三次作业投入了大量的时间之后,我也在实现了作业需求后得到了极大的满足感与成就感。对于之后三个单元的学习,我尚无法断言自己能否在每一次作业都很好的去发挥,但我相信我能在这个过程中不断提升自己的水平,最后也能够写出“面向对象”的代码。
冲!