BUAA-OO第一单元总结
BUAA-OO第一单元总结
1. 简介
第一单元的作业内容是表达式化简,通过对表达式结构进行建模,逐步完成对包含常数、变量、函数(三角函数、自定义函数、求和函数)的表达式的括号展开与化简,体会层次化设计的思想。
2. 第一次作业
2.1 设计理念
第一次作业的表达式中只包含常数和变量,且只有单层括号。我的代码可以分为四部分:
- 预处理。我先对输入的表达式进行预处理:去除表达式中所有的空白字符;把所有连续的正负号都替换为单独的一个符号;把所有的乘方形式转换为乘法形式。
- 解析。再通过递归下降解析预处理后的表达式,在解析时给常数和幂函数都带一个符号,最终得到原表达式的后缀表达式。
- 计算。对后缀表达式以
[+-]a*x**b
为单元进行计算,用 HashMap 存储,key 为指数 b,value 为系数 a,计算时合并同类项。 - 输出。先输出正项,把
x**2
替换为x*x
。
2.2 UML类图
MainClass 中包含预处理部分。Lexer、Parser 类解析字符串得到后缀表达式,在解析时通过 parseExpr、parseTerm、parseFactor 传递参数 sign 来决定常数和幂函数的符号以及表达式因子内部是否需要变号,最终可以得到只含有加法和乘法的后缀表达式。Calculate 类实现加法和乘法以及同类项的合并。Poly中实现输出简化。
2.3 度量分析程序结构
class | OCavg | OCmax | WMC |
---|---|---|---|
Calculate | 4.00 | 9 | 16 |
Expr | 1.33 | 2 | 4 |
Lexer | 2.00 | 4 | 6 |
MainClass | 2.33 | 4 | 7 |
Num | 1.00 | 1 | 2 |
Parser | 4.25 | 7 | 17 |
Poly | 6.33 | 28 | 38 |
Term | 1.33 | 2 | 4 |
Var | 1.50 | 2 | 3 |
Total | 97 | ||
Average | 3.23 | 6.56 | 10.78 |
method | CogC | ev(G) | iv(G) | v(G) |
---|---|---|---|---|
Calculate.cal() | 5 | 1 | 5 | 5 |
Calculate.Calculate(String) | 0 | 1 | 1 | 1 |
Calculate.getPolys() | 0 | 1 | 1 | 1 |
Calculate.merge(String) | 21 | 1 | 9 | 9 |
Expr.addTerm(Term) | 0 | 1 | 1 | 1 |
Expr.Expr() | 0 | 1 | 1 | 1 |
Expr.toString() | 1 | 1 | 2 | 2 |
Lexer.Lexer(String) | 0 | 1 | 1 | 1 |
Lexer.next() | 4 | 2 | 3 | 4 |
Lexer.peek() | 0 | 1 | 1 | 1 |
MainClass.main(String[]) | 3 | 1 | 3 | 3 |
MainClass.mul() | 5 | 3 | 4 | 4 |
MainClass.simpleSign(String) | 0 | 1 | 1 | 1 |
Num.Num(String, String) | 0 | 1 | 1 | 1 |
Num.toString() | 0 | 1 | 1 | 1 |
Parser.parseExpr(String) | 15 | 7 | 8 | 8 |
Parser.parseFactor(String) | 3 | 3 | 3 | 3 |
Parser.Parser(Lexer) | 0 | 1 | 1 | 1 |
Parser.parseTerm(String) | 7 | 1 | 6 | 6 |
Poly.add(int, BigInteger) | 2 | 1 | 2 | 2 |
Poly.getVars() | 0 | 1 | 1 | 1 |
Poly.mul(int, BigInteger) | 3 | 1 | 3 | 3 |
Poly.Poly() | 0 | 1 | 1 | 1 |
Poly.remove0() | 4 | 1 | 4 | 4 |
Poly.toString() | 55 | 4 | 27 | 28 |
Term.addFactor(Factor) | 0 | 1 | 1 | 1 |
Term.Term() | 0 | 1 | 1 | 1 |
Term.toString() | 1 | 1 | 2 | 2 |
Var.toString() | 0 | 1 | 1 | 1 |
Var.Var(String, String) | 2 | 1 | 2 | 2 |
Total | 131 | 44 | 98 | 100 |
Average | 4.37 | 1.47 | 3.27 | 3.33 |
从代码复杂度分析数据可以看出,大部分方法的圈复杂度在合理范围内,但是 Poly 类的 toString 方法、 Parser 类的 parseExpr方法、 Calculate 类的 merge 方法的复杂度较高,经分析这是因为这些方法中使用了较多的 if-else 语句,考虑以后改进。
2.4 bug分析
第一次作业从零开始,遇到了很多细节方面的 bug:
-
公测和本地测试时有很多问题都出在 Poly 类的 toString 方法,由于考虑先输出正项以及含有 1 、0 的化简,在输出时有很多情况考虑不周。toString 方法中的 if-else 语句过多,更容易出错。把 toString 方法拆分成几个小的方法应该会降低出错的几率。
-
在替换连续的正负号时,应该先替换连续的三个符号,再替换连续的两个符号。
-
我在预处理中用正则表达式把所有的乘方都替换成了乘法的形式,错误的正则表达式如下:
String mi = "(?<expr>\\(.*\\))\\*\\*[+]?(?<mi>[\\d]+)";
这样写会匹配 (A) + (B)**3 形式的第一个左括号和第二个右括号,在第一次作业中稍微修改正则表达式即可解决:
String mi = "(?<expr>\\([^()]*\\))\\*\\*[+]?(?<mi>[\\d]+)";
但是这样修改只能应对作业1,后续作业加入函数,就不能通过正则表达式替换了。可见字符串替换并不是最佳方法,很容易出错,且不易迭代,正则表达式更适合应用在模式变化少的场合,像表达式化简这样的场合使用正则表达式很容易出错。如果使用,必须充分测试以确保考虑周全。
2.5 心得体会
第一周由于时间原因没能在写代码前仔细分析如何设计,并且过多地依赖 training 提供的思路,种种原因导致写代码的过程并不顺利,还经历了几次局部重构。在写 Parser 类时,起初因为对递归的过程了解不深入,在传递参数时写出了很多 bug,但这个过程最终让我清晰地明白了递归下降的过程,对后续作业的迭代有很大帮助。
第一次作业是整个单元的基础,应该做好充分测试。
3. 第二次作业
3.1 设计理念
第二次作业加入了三角函数、求和函数和自定义函数。我进行了重构,在这次作业中,我把 a*x**b*sin(c*x**d)**e*...*cos(f*x**g)**h
的形式作为表达式的基本单元 element,丢弃了作业1中的 factor、term、expr 类, 大框架仍然是使用递归下降,不同之处在于我在递归下降的同时以 element 为单位进行计算合并。
3.2 UML类图
Element 是表达式中的基本单元,包含一个系数、一个 x 的指数,两个存放三角函数的 HashSet。Element 类中重写了 equals 方法,第二次作业中只有在加减法中用到了 Element 的比较,此时只需要 x 的指数、sin 项 和 cos 项 完全相等即可认为两个 Element 相等,即可以合并(新的 Element的系数为两者系数之和 )。
Trigo 类表示三角函数中的内容,在第二次作业中它只能是一个常数或者一个幂函数。它有两种判定相等的机制,在加减法中,因为比较的是 Element,所以 Trigo 必须完全相等才可认为相等,在三角函数的乘法中,只要三角函数中的系数和 x 的指数相等,即可认为 Trigo 相等,即可以合并(新的 Trigo 的指数为两者指数之和)。
PrePocess类实现预处理,包括去除空白、替换连续正负号(其实在 parser 中 factorValue 方法默认返回 0 ,因此这个处理可以省略)。处理自定义函数:函数调用是用逗号分隔实参的,使用栈来判断顶层函数,遇到左括号压栈,遇到右括号弹栈,只有遇到逗号,并且栈中只有一个左括号时,才表示此实参为顶层函数的一个实参。在将形参替换为实参时,为了避免 x、i 等符号的冲突,可以先用一些特殊符号替换形参。因为递归下降可以解析括号嵌套,所以处理函数时可以给函数暴力加括号以免出错。
Parser 类在添加 Element 时进行部分优化,比如把 sin(0) 替换成 0,把 cos(0) 替换成 1,把零次幂的式子都替换成 1。这样在后续计算比较元素相等时,可以避免因为一些无关项的存在而把原本相等的 Element 误判为不相等。每个 Parse 函数都返回一个存有计算合并后的Element 的 HashSet。
Calculate 类中实现两个 HashSet<Element> 的加法、减法、乘法,并返回计算结果。
3.3 度量分析程序结构
class | OCavg | OCmax | WMC |
---|---|---|---|
Calculate | 3.40 | 5 | 17 |
Element | 3.27 | 17 | 36 |
Lexer | 3.00 | 7 | 9 |
MainClass | 7.00 | 7 | 7 |
Parser | 4.14 | 10 | 29 |
PreProcess | 6.20 | 12 | 31 |
Trigo | 1.89 | 5 | 17 |
Total | 146 | ||
Average | 3.56 | 9 | 20.86 |
method | CogC | ev(G) | iv(G) | v(G) |
---|---|---|---|---|
Calculate.add() | 7 | 3 | 4 | 4 |
Calculate.Calculate(HashSet, HashSet) | 0 | 1 | 1 | 1 |
Calculate.mul() | 3 | 1 | 3 | 3 |
Calculate.sub() | 7 | 3 | 4 | 4 |
Calculate.triMul(HashSet, HashSet) | 8 | 3 | 5 | 5 |
Element.Element(BigInteger, int, HashSet, HashSet) | 0 | 1 | 1 | 1 |
Element.equals(Object) | 4 | 3 | 4 | 6 |
Element.getCoe() | 0 | 1 | 1 | 1 |
Element.getCoss() | 0 | 1 | 1 | 1 |
Element.getExp() | 0 | 1 | 1 | 1 |
Element.getSins() | 0 | 1 | 1 | 1 |
Element.hashCode() | 0 | 1 | 1 | 1 |
Element.setCoe(BigInteger) | 0 | 1 | 1 | 1 |
Element.toString() | 38 | 2 | 16 | 17 |
Element.tri(String) | 6 | 1 | 4 | 4 |
Element.triFirst(String, boolean) | 7 | 1 | 5 | 5 |
Lexer.Lexer(String) | 0 | 1 | 1 | 1 |
Lexer.next() | 7 | 2 | 6 | 7 |
Lexer.peek() | 0 | 1 | 1 | 1 |
MainClass.main(String[]) | 9 | 1 | 8 | 8 |
Parser.exprValue() | 9 | 3 | 5 | 5 |
Parser.factorValue() | 19 | 1 | 10 | 11 |
Parser.getTrigos(BigInteger, int, int, String) | 8 | 6 | 4 | 6 |
Parser.getVar(String) | 2 | 2 | 2 | 2 |
Parser.make(String) | 0 | 1 | 1 | 1 |
Parser.Parser(Lexer) | 0 | 1 | 1 | 1 |
Parser.termValue() | 13 | 3 | 6 | 6 |
PreProcess.addFunc(String) | 21 | 1 | 12 | 12 |
PreProcess.function(String) | 21 | 1 | 10 | 11 |
PreProcess.PreProcess() | 0 | 1 | 1 | 1 |
PreProcess.simple(String) | 0 | 1 | 1 | 1 |
PreProcess.sum(String) | 13 | 1 | 6 | 7 |
Trigo.equals(Object) | 4 | 3 | 4 | 6 |
Trigo.equalsWithoutExpt(Object) | 4 | 3 | 3 | 5 |
Trigo.getCoe() | 0 | 1 | 1 | 1 |
Trigo.getExpt() | 0 | 1 | 1 | 1 |
Trigo.getExpx() | 0 | 1 | 1 | 1 |
Trigo.hashCode() | 0 | 1 | 1 | 1 |
Trigo.setExpt(int) | 0 | 1 | 1 | 1 |
Trigo.toString() | 7 | 1 | 5 | 5 |
Trigo.Trigo(BigInteger, int, int) | 0 | 1 | 1 | 1 |
Total | 217 | 65 | 146 | 159 |
Average | 5.29 | 1.59 | 3.56 | 3.88 |
从代码复杂度分析数据可以看出第二次作业的代码比第一次作业更复杂,主要是因为输出的 toString 方法更复杂了,计算时 Element 中包含的三角函数项数目不确定,使用的 for 循环和 if-else语句更多了。
3.4 bug分析
第一次作业已经为递归下降的大框架打好了基础,我的 bug 主要出现在以下几方面:
- 使用 while(xxx.find()) 查找时在循环中改变了待匹配的字符串,此时忘记更新 matcher 中的字符串,结果是只能处理一次待处理函数或者出现死循环。
- 浅拷贝问题。计算时我需要拷贝 Element 容器,如果直接用 = 赋值,其实只是拷贝了引用,修改新容器里的对象时,原有容器的内容也会改变。为避免干扰,应该用深拷贝,new 一个新对象之后再完全拷贝原来对象的属性,保证两个容器内的对象引用指向不同的对象。
- 解析表达式遇到 sin(-1) 等时,我粗心地把 Element 的系数直接设为 -1,sin 容器加 sin(1),这样忽略了 sin 项带指数的情况,应该把系数设为 (-1)**pow。
- 在计算合并时,加减法对三角函数的比较和乘除法对三角函数的比较采用的标准不同,应该写两套 equals 方法,否则会因为误判两个 Element 相等导致出现 bug。
3.5 重构体会
在迭代的过程中,重构是一个重要工具,不能因为繁琐而放弃让自己的代码变得更好的重构机会。在重构的过程中应该认真反思之前版本的不足、如何进行优化。重构后要与重构前的代码做对比测试,确保原有功能不受影响。
4. 第三次作业
4.1 设计理念
第三次作业表达式中的自定义函数可以嵌套,括号可以嵌套,三角函数里可以是表达式因子,输出时也必须给表达式因子加括号。本次作业直接在作业2 的基础上添加,改动很小。主要是三角函数 Trigo 类中的成员变成了 Element 容器和指数,以及对输出做了部分优化。
4.2 UML类图
Trigo 类中包含一个 Element 容器以支持三角函数里可以有表达式。增加了 isExpr 方法以判断输出时三角函数是否需要增加括号。
Element 类里也实现两种不同标准的 equals 方法,一种要求完全相等,用于作为 Trigo 的因子时的比较,另一种只要求除了系数完全相等,用于加减法时对 Element 的比较。
4.3 度量分析程序结构
class | OCavg | OCmax | WMC |
---|---|---|---|
Calculate | 3.40 | 5 | 17 |
Element | 3.25 | 17 | 39 |
Lexer | 3.00 | 7 | 9 |
MainClass | 9.00 | 9 | 9 |
Parser | 4.29 | 9 | 30 |
PreProcess | 6.20 | 12 | 31 |
Trigo | 2.67 | 11 | 24 |
Total | 159 | ||
Average | 3.79 | 10.00 | 22.71 |
method | CogC | ev(G) | iv(G) | v(G) |
---|---|---|---|---|
Calculate.add() | 7 | 3 | 4 | 4 |
Calculate.Calculate(HashSet, HashSet) | 0 | 1 | 1 | 1 |
Calculate.mul() | 3 | 1 | 3 | 3 |
Calculate.sub() | 7 | 3 | 4 | 4 |
Calculate.triMul(HashSet, HashSet) | 8 | 3 | 5 | 5 |
Element.Element(BigInteger, int, HashSet, HashSet) | 0 | 1 | 1 | 1 |
Element.equals(Object) | 4 | 3 | 5 | 7 |
Element.equalsWithoutCoe(Object) | 4 | 3 | 4 | 6 |
Element.getCoe() | 0 | 1 | 1 | 1 |
Element.getCoss() | 0 | 1 | 1 | 1 |
Element.getExp() | 0 | 1 | 1 | 1 |
Element.getSins() | 0 | 1 | 1 | 1 |
Element.hashCode() | 0 | 1 | 1 | 1 |
Element.setCoe(BigInteger) | 0 | 1 | 1 | 1 |
Element.toString() | 38 | 2 | 16 | 17 |
Element.tri(String) | 6 | 1 | 4 | 4 |
Element.triFirst(String, boolean) | 7 | 1 | 5 | 5 |
Lexer.Lexer(String) | 0 | 1 | 1 | 1 |
Lexer.next() | 7 | 2 | 6 | 7 |
Lexer.peek() | 0 | 1 | 1 | 1 |
MainClass.main(String[]) | 13 | 3 | 11 | 11 |
Parser.exprValue() | 9 | 3 | 5 | 5 |
Parser.factorValue() | 14 | 1 | 9 | 9 |
Parser.getTrigos(HashSet, int, String) | 13 | 5 | 7 | 10 |
Parser.getVar(String) | 2 | 2 | 2 | 2 |
Parser.make(String) | 0 | 1 | 1 | 1 |
Parser.Parser(Lexer) | 0 | 1 | 1 | 1 |
Parser.termValue() | 13 | 3 | 6 | 6 |
PreProcess.addFunc(String) | 21 | 1 | 12 | 12 |
PreProcess.function(String) | 22 | 1 | 11 | 12 |
PreProcess.PreProcess() | 0 | 1 | 1 | 1 |
PreProcess.simple(String) | 0 | 1 | 1 | 1 |
PreProcess.sum(String) | 13 | 1 | 6 | 7 |
Trigo.equals(Object) | 4 | 3 | 3 | 5 |
Trigo.equalsWithoutExpt(Object) | 3 | 3 | 2 | 4 |
Trigo.getElements() | 0 | 1 | 1 | 1 |
Trigo.getExpt() | 0 | 1 | 1 | 1 |
Trigo.hashCode() | 0 | 1 | 1 | 1 |
Trigo.isExpr(String) | 3 | 2 | 2 | 3 |
Trigo.setExpt(int) | 0 | 1 | 1 | 1 |
Trigo.toString() | 17 | 3 | 12 | 13 |
Trigo.Trigo(HashSet, int) | 0 | 1 | 1 | 1 |
Total | 238 | 72 | 163 | 180 |
Average | 5.67 | 1.71 | 3.88 | 4.29 |
作业3的代码复杂程度与作业2相近。
4.4 bug分析
作业3的的bug较少,但都是之前作业出现过的bug类型:
- 由于作业二的 Trigo 中没有 Element,Element 的 equals 方法只会在比较 Element 时被调用,但作业3 Trigo 中含有 Element,比较 Trigo 时会调用 Element 的 equals 方法,这里需要两个 Element 完全相等,而之前加减法所用的比较标准是除系数外完全相同,索要需要增加新的比较方法。否则还会因为误判相等而出现 bug。
- 在输出三角函数时,我为了在三角函数内是单独的常数或幂函数时不输出额外括号,再次用正则表达式匹配,但再次犯错,bug原因跟作业1类似,正则表达式匹配了并不配对的括号。
4.5 心得体会
由于作业2重构时做了充分考虑,作业3的代码量很小。易于迭代的代码可以大大减小工作量,在以后编写代码时也应注意代码的可扩展性。
5. hack策略
互测其实是阅读他人代码、学习反思的好机会,但由于时间所限,我没有针对代码进行互测,只是随机构造数据,这些数据也同样用于我自己代码的测试。测试数据可以按题目的形式化表述逐级构造,从常数、幂函数等简单因子开始,测试基础功能,之后再组合构造项和表达式。最后可以测试一些边界数据、复杂数据。互测中发现有同学在化简 1*x 的输出时直接把 1*x 替换成了 x,这样就会出现把 11*x 输出 1x 的 bug,总之字符串替换往往并不安全。
6. 设计体验
作业1由于时间原因没有充分思考如何设计,盲目下手编写代码导致经历了多次局部重构。面对一份庞大的作业时,先想好设计结构再动手写代码可能会起到事半功倍的效果。
OO课程的作业是不断迭代的过程,好的架构的重要性不言而喻。但在实现某次作业的需求时,并不能准确预测接下来会增加什么需求,如果过早过多地考虑怎样扩展,可能会浪费时间还使代码变得一团乱,如果完全不考虑后续迭代,又可能导致大规模重构。实际操作时应该仔细权衡写代码时是否要为迭代做额外的处理。
到第三次作业时我的代码几乎没有什么新鲜bug,但之前写过的同类型bug竟然还会再写一次。在以后单元的作业中应该及时总结,避免在同一个地方跌倒多次。第一次作业强测出了bug,第二、三次作业虽然强测没有bug但互测都被测出了bug,可见通过测试并不代表完全正确,自己还应该不断做测试。
第一单元是我初次接触面向对象编程,学到了很多新知识,但仍存在很多不足。在第二次作业重构后,虽然代码实现的功能更强大了,并且易于迭代,但我认为我的代码并没有很好地体现面向对象的特征。希望经过不断练习可以在后续作业中写出良好的面向对象的代码。