444
社区成员
在第一次作业架构之前,参考学习了第一单元训练training的相关内容,training中给出两种处理思路;正则表达式思路和递归下降的思路。
对于正则的方法,针对第一次作业处理起来可能更直接,更易于理解,但考虑到后续的迭代增量设计,正则方法对于当前简单的表达式可能易于处理,但当问题逐渐复杂,结构复杂的表达式几乎很难用正则分析清楚,所以正则表达式思路并不是长远之计。
通过查阅资料,笔者初步学习了递归下降的文法分析方法,这种方法可以让代码结构更清晰,更具可扩展性。对于后续复杂嵌套的表达式,它可以从顶自下的逐层解析,将表达式字符串解析成有纵向结构的表达式树来存储分析。
这种思路下,文法分析中比较关键的类是lexer类和parse类,下面即是这两个类的功能作用比较浅显直白的理解
lexer:词法解析,是数字?字母?符号?返回相应的String
parse:文法解析,String------prase------> Operator对象/容器对象
![屏幕截图 2023-03-19 180743](C:\Users\联想\Desktop\屏幕截图 2023-03-19 180743.png)
架构思路上,笔者将第一次作业中需求的实现大概拆解为了这样几个步骤:
输入预处理
解析、存储
计算(展开+合并)
处理输出为标准输出
1&4.在Mainclass类中主要进行输入预处理(preprocess()方法),并且处理输出为标准输出:
为了方便解析,笔者思路第一步是把输入的文法定义的表达式格式处理成统一化的、规整的、易于处理的表达式
比如:由于空格在文法定义的表达式中不起任何作用,过滤掉表达式中所有空格;由于指数符号**与乘号*解析时候易于混淆,把**替换成^符号,表示幂次;对于文法定义中表达式中重复出现的+-号,将其最终合并为一个+-号,等等
2.解析、存储:lexer类和parse类是解析的核心类,通过它们将表达式字符串解析为实实在在的有层次结构的表达式对象。
而对于表达式对象的存储,第一次作业时,笔者参考expr实验的设计思想,Expr类存储着整个表达式的框架,而expr是由term项加减组成,两者有composion关系,因而设计Term类。Term类的对象作为arraylist内的元素,包含在Expr类中。
同理,每个term可以看成由很多factor相乘组成,factor的具体形式存在不同,可以为常数,也可以是幂函数,也可以是表达式因子,也就是说他们管理的数据各不相同,但他们作为term的因子,行为均是一致的。所以考虑设Factor为接口类,Number,Power,Expr依次实现Factor。Factor类的对象作为arraylist内的元素,包含在Term类中。这也就形成了上述的类图架构。
由于Expr类的对象既可以是整个表达式的整体框架,有可以作为表达式因子,出现在Term类的arraylist数组之中,这就形成了递归的结构。
3.在计算的时候,正利用上述自顶向下的层次结构,逐层调用各个类中的expansion方法,将Term类中arraylist里面的Expr对象,去括号,与其他项相乘,调用mul()方法,分配展开,实现表达式去括号功能。
问题出在了表示式的预处理上,在处理数字的前导0时候,把某些情况下数字中间的0也给去除吃掉了,导致常数存储的错误
之后反思,实际上前导0根本就不用处理,在将读入的字符串转为数字,存储在Number对象中的时候,由于java的自带方法,已经过滤掉了前导0。
int num=Integer.parseInt("0001");
所以即使不处理,也不会造成解析异常,而自己的额外前导0处理反而画蛇添足了。
第二次作业中由于问题进一步复杂化,第一次架构中存在的问题被暴露出来。第一次作业中,Expr的各级层次结构,我使用了Arraylist来表示它们之间的组合关系;这样的容器结构虽然易于管理操作,但在同类因子合并幂数,以及同类项合并上,带来了不小的问题。第二次作业引入了多层嵌套的括号,以及三角函数。第一次作业隐藏在mul方法中的合并处理,本来就使mul方法显得臃肿,引入三角函数后,还需要根据三角函数内部的表达式是否相同,判断是否需要合并,使得我的当前结构很难合理的支撑进一步的需求。在研讨课上,听了大家的新点子之后,决定进行重构。
我并没有进行改变,在第一次作业中类间关系,通过分析感觉设计的已经较为合理。我主要把各级之间得到存储结构进行了较多的改动。
由于各个factor和term之间均存在合并的行为需求,笔者将存储它们的对象的上层结构中的容器更改更改为了HashMap;
由于HashMap内元素以hashcode的特殊方式进行组织,可以利用重写hashcode()和equal()方法,将可合并的项或者因子的hashcode设置为相同,利用HashMap的hash特性,使得每次将元素加入hashmap时候,直接比较键是否相同,若相同,则值直接相加,非常简单的实现了同类项合并问题,也避免了同一个方法的臃肿问题。
但需要注意:由于hashmap的put方法:对一个容器中相同的键,进行值替换,而非累加!!!
所以在实现上,笔者自己在Expr类,Term类中分别定义了一个myput静态方法,实现值的累加
public static void myput(HashMap<K, V> hashMap, K key, V value) {...}
expr:sin(expr)
少移动了一个pos
lexer.next();
无穷递归调用
bug :
输入 : sin(0)
输出 :sin(())
保留必要的括号后,也要保留必要的0占位
bug : sin的幂次被理解为内部ExprOfTrigo夺取解析
问题<=>如何区分sin(x==)==^2的右括号 和普通多项式因子 (x+y==)==^2的右括号
不可以不吃sin(的左括号,否则,理解为里面多项式的幂次;
然而左括号与右括号不匹配,会带来文法上的bug;
利用不匹配:
不让ExprOfTrigo parse power的解决方案:
方案一:在Expr类中添加新变量isExprOfTrigo,重载方法,传入isExprOfTrigo参数,来决定是否parse power
方案二:在Expr类中加入新方法parseExprWithoutPower,在parse sin因子的时候,调用parseExprWithoutPower
注意三角函数sin()和自定义函数f()左右括号的吃
//Parser类 parseFactor方法
else if (lexer.now().matches("sin|cos")) {
boolean isSin = lexer.now().equals("sin"); //变量因子(三角函数) //并在lexer里面吃了sin()的左括号
lexer.next();
Expr temp = parseExprWithoutPower().getexpansion();
lexer.next();//吃sin()的右括号
int power = parsePower();
//Parser类 parseSelfFunct方法
public String[] parseSelfFunct() {
String[] factorsString = new String[5];
lexer.next();//吃 f()的左括号
factorsString[1] = MainClass.preprocess(parseFactor().toStringWithTempPower());
if (lexer.now().equals(",")) {
lexer.next();
factorsString[2] = MainClass.preprocess(parseFactor().toStringWithTempPower());
}
if (lexer.now().equals(",")) {
lexer.next();
factorsString[3] = MainClass.preprocess(parseFactor().toStringWithTempPower());
}
lexer.next();//吃 f()的右括号
return factorsString;
}
bug:
preprocess 不到位:
String equation = preprocess(scanner.nextLine());//预处理equation,过滤掉空格并替换相关
...
split[1] = preprocess(split[1]);//预处理split[1]自定义函数多项式,去除前导+
...
String input = preprocess(input0);//预处理input0待化简的表达式
factorsString[1/2/3] = MainClass.preprocess(parseFactor().toStringWithTempPower());
- 调用表达式:
先解析出三(二or一)个实参因子;
再把实参因子toString(),字符串替换u,v,w,注意因子外面加括号;
再次解析整个替换后的字符串
String[] ss = new String[5];// String[]中的每个String依然ss[i]=null,而不是ss[i]=""
bug:对于自定义函数的调用表达式的实参因子,该用什么方法解析??
//Parser类 parseSelfFunct方法
public String[] parseSelfFunct() {
String[] factorsString = new String[5];
lexer.next();//吃 f()的左括号
factorsString[1] = MainClass.preprocess(parseFactor().toStringWithTempPower());
if (lexer.now().equals(",")) {
lexer.next();
factorsString[2] = MainClass.preprocess(parseFactor().toStringWithTempPower());
}
...
}
方案a. public Factor parseFactor()
问题:由于方便合并的hashmap存储结构,Var,TrigoFunct 的幂次,存储在Term类里面的hashmap factors的键值中,调用Var,TrigoFunct 的toString方法,并不把幂次打印出来
方案b.public Term parseTerm(int sign)
问题1:由于方便合并的hashmap存储结构,若实参因子是Num常数因子,其值存储在Expr类里面的hashmap terms的键值中,调用Term 的toString方法,并不把幂次打印出来
问题2:对于表达式因子,由于其并没有展开,仍存储在Term类里面的ArrayList exprs中,toString并不会把表达式因子打印出来!!!导致若传参表达式因子,只打印出空串""
,形参被替换成()
,由于空串,括号里什么都没有,带来解析错误,Expr=null,抛异常
方案c. public Expr parseExpr()
问题1:parseExpr()方法在最后parsePower(),会往后多吃一个字符??(待验证)
只能用parseExprWithoutPower()
问题2:对于表达式因子,由于其并没有展开,仍存储在Term类里面的ArrayList exprs中,toString并不会把表达式因子打印出来!!!导致若传参表达式因子,只打印出空串""
,形参被替换成()
,由于空串,括号里什么都没有,带来解析错误,Expr=null,抛异常
=> 因此只能选择方案a,在Factor接口中加入String toStringWithTempPower();
类,并在implement的Expr Num Var Trigo类中一次实现
对于自定义表达式,由于第三次作业出现了表达式定义时候的可以调用其他“已定义的”函数,所以对之前的自定义函数的定义表达式的处理进行了改进。
对于自定义函数,笔者先将其定义表达式进行解析并展开,这样不但可以先一步化简定义表达式,避免后续解析的时候重复化简,又可以适应增添的调用其他“已定义的”函数
的需求。
![屏幕截图 2023-03-19 180803](C:\Users\联想\Desktop\屏幕截图 2023-03-19 180803.png)
由于第二次作业的整体重构,第三次作业的增量迭代相较而言很容易。在这次作业在整体的架构上,基本上与第二次与第三次作业非常类似。
与第一次作业相比,主要新增了三角函数类,作为Factor的实现。而三角函数内部有可以是Expr,所以其与Expr有着包含的关系。并且在MainClass类中加入了自定义函数的定义表达式的处理存储功能
与第二次作业相比,在各个层次的类中,新增了求导过程。由于求导是自顶向下,逐层向内的递归过程,这与表示式计算时候的展开分配相乘的过程比较相似,因而仿照expansion和mul,在各层级设计了derive求导方法。
类复杂度
class | OCavg | OCmax | WMC |
---|---|---|---|
expr.Expr | 2.6363636363636362 | 7.0 | 29.0 |
expr.Num | 1.0 | 1.0 | 6.0 |
expr.Term | 2.7333333333333334 | 9.0 | 41.0 |
expr.TrigoFunct | 1.875 | 3.0 | 15.0 |
expr.Var | 1.5 | 3.0 | 12.0 |
Lexer | 1.8 | 4.0 | 9.0 |
MainClass | ==5.2== | 9.0 | 26.0 |
Parser | ==3.4== | 8.0 | 17.0 |
Total | 155.0 | ||
Average | 2.4603174603174605 | 5.5 | 19.375 |
方法复杂度
method | CogC | ev(G) | iv(G) | v(G) |
---|---|---|---|---|
expr.Expr.addTerm(Term) | 2.0 | 1.0 | 2.0 | 2.0 |
expr.Expr.addTermMulK(Term, BigInteger) | 2.0 | 1.0 | 2.0 | 2.0 |
expr.Expr.derive(String) | 6.0 | 1.0 | 4.0 | 4.0 |
expr.Expr.Expr() | 0.0 | 1.0 | 1.0 | 1.0 |
expr.Expr.getexpansion() | 3.0 | 1.0 | 3.0 | 3.0 |
expr.Expr.getTemppower() | 0.0 | 1.0 | 1.0 | 1.0 |
expr.Expr.mul(Expr) | 7.0 | 1.0 | 7.0 | 7.0 |
expr.Expr.myput(HashMap, Term, BigInteger) | 2.0 | 1.0 | 2.0 | 2.0 |
expr.Expr.setTemppower(int) | 0.0 | 1.0 | 1.0 | 1.0 |
expr.Expr.toString() | 6.0 | 1.0 | 5.0 | 5.0 |
expr.Expr.toStringWithTempPower() | 0.0 | 1.0 | 1.0 | 1.0 |
expr.Num.getNum() | 0.0 | 1.0 | 1.0 | 1.0 |
expr.Num.getTemppower() | 0.0 | 1.0 | 1.0 | 1.0 |
expr.Num.Num(BigInteger) | 0.0 | 1.0 | 1.0 | 1.0 |
expr.Num.setTemppower(int) | 0.0 | 1.0 | 1.0 | 1.0 |
expr.Num.toString() | 0.0 | 1.0 | 1.0 | 1.0 |
expr.Num.toStringWithTempPower() | 0.0 | 1.0 | 1.0 | 1.0 |
expr.Term.addFactor(Factor) | 6.0 | 1.0 | 5.0 | 5.0 |
expr.Term.derive(String) | ==21.0== | ==4.0== | 8.0 | 9.0 |
expr.Term.equals(Object) | 5.0 | ==4.0== | 3.0 | 6.0 |
expr.Term.getexpansion() | 3.0 | 1.0 | 3.0 | 3.0 |
expr.Term.getTempk0() | 0.0 | 1.0 | 1.0 | 1.0 |
expr.Term.hashCode() | 0.0 | 1.0 | 1.0 | 1.0 |
expr.Term.hasSin0() | 4.0 | 3.0 | 3.0 | 4.0 |
expr.Term.isExprsEmpty() | 0.0 | 1.0 | 1.0 | 1.0 |
expr.Term.isHashMapEmpty() | 0.0 | 1.0 | 1.0 | 1.0 |
expr.Term.mul(Term) | 2.0 | 1.0 | 3.0 | 3.0 |
expr.Term.myput(HashMap, Factor, int) | 2.0 | 1.0 | 2.0 | 2.0 |
expr.Term.setTempk0(BigInteger) | 0.0 | 1.0 | 1.0 | 1.0 |
expr.Term.Term(BigInteger, HashMap) | 0.0 | 1.0 | 1.0 | 1.0 |
expr.Term.Term(int) | 0.0 | 1.0 | 1.0 | 1.0 |
expr.Term.toString() | 4.0 | 1.0 | 4.0 | 4.0 |
expr.TrigoFunct.derive(String) | 1.0 | 1.0 | 2.0 | 2.0 |
expr.TrigoFunct.equals(Object) | 2.0 | 3.0 | 1.0 | 3.0 |
expr.TrigoFunct.getTemppower() | 0.0 | 1.0 | 1.0 | 1.0 |
expr.TrigoFunct.hashCode() | 0.0 | 1.0 | 1.0 | 1.0 |
expr.TrigoFunct.setTemppower(int) | 0.0 | 1.0 | 1.0 | 1.0 |
expr.TrigoFunct.toString() | 0.0 | 1.0 | 1.0 | 1.0 |
expr.TrigoFunct.toStringWithTempPower() | 2.0 | 3.0 | 3.0 | 3.0 |
expr.TrigoFunct.TrigoFunct(boolean, Expr, int) | 3.0 | 1.0 | 1.0 | 3.0 |
expr.Var.derive(String) | 0.0 | 1.0 | 1.0 | 1.0 |
expr.Var.equals(Object) | 2.0 | 3.0 | 1.0 | 3.0 |
expr.Var.getTemppower() | 0.0 | 1.0 | 1.0 | 1.0 |
expr.Var.hashCode() | 0.0 | 1.0 | 1.0 | 1.0 |
expr.Var.setTemppower(int) | 0.0 | 1.0 | 1.0 | 1.0 |
expr.Var.toString() | 0.0 | 1.0 | 1.0 | 1.0 |
expr.Var.toStringWithTempPower() | 2.0 | 3.0 | 3.0 | 3.0 |
expr.Var.Var(String, int) | 0.0 | 1.0 | 1.0 | 1.0 |
Lexer.getNumber() | 2.0 | 1.0 | 3.0 | 3.0 |
Lexer.getTrigoNumber() | 0.0 | 1.0 | 1.0 | 1.0 |
Lexer.Lexer(String) | 0.0 | 1.0 | 1.0 | 1.0 |
Lexer.next() | 4.0 | 2.0 | 3.0 | 4.0 |
Lexer.now() | 0.0 | 1.0 | 1.0 | 1.0 |
MainClass.getFunct(char) | 3.0 | ==4.0== | 1.0 | 4.0 |
MainClass.inputPreprocess(String) | 1.0 | 1.0 | 2.0 | 2.0 |
MainClass.main(String[]) | 14.0 | 1.0 | 6.0 | 9.0 |
MainClass.replaceAllSelfFunct(String) | ==22.0== | ==5.0== | 5.0 | ==11.0== |
MainClass.simplifyFromInputToOutput(String) | 3.0 | 2.0 | 2.0 | 3.0 |
Parser.parseExpr() | 6.0 | 1.0 | 4.0 | 4.0 |
Parser.parseFactor() | 7.0 | 7.0 | 8.0 | 8.0 |
Parser.parsePower() | 2.0 | 2.0 | 2.0 | 2.0 |
Parser.Parser(Lexer) | 0.0 | 1.0 | 1.0 | 1.0 |
Parser.parseTerm(int) | 1.0 | 1.0 | 2.0 | 2.0 |
Total | 152.0 | 95.0 | 136.0 | 161.0 |
Average | 2.4126984126984126 | 1.507936507936508 | 2.1587301587301586 | 2.5555555555555554 |
整体来看,笔者实现的代码复杂度不是特别高,类与类耦合度也可以接受,内聚性也较高。复杂度高的方法,如Node.toString(),expr.Term.derive(String),MainClass.replaceAllSelfFunct(String)。均需要针对不同的字符串进行分情况处理,使用了大量if-else if-else的语句,拉高了复杂度。
对于MainClass类的复杂度较高的原因是,笔者将输入和输出的预处理和最终处理均放在了MainClass中。改进的思路是,单独建立专门负责输入输出字符处理的相关类,每个类和方法的职责分工明确,可以改进复杂度较高的情况。
由于第二次作业较好的架构和第三次作业充足的测试,在互测和强测中,均未出现bug。
通过一单元的学习,我认识到了合理的有预见性的架构的重要性,也进一步理解了程序开发时的SOLID原则。
另外,关于如何寻找bug和发现bug后更高效的定位bug,有一些小的体会,总结如下:
根据各种造出的数据以及读代码,肉眼debug(依赖于穷举构造用例,来锁定bug)
运行debug
打好标志性断点:
不仅在出错的地方打好断点
在程序运行的每个关键步骤处也可以打断点,方便人脑跟上程序
如:第一单元作业,在+-处,*处,(处打断点,方便跟踪程序运行到了哪
一组好的测试样例:
有针对性的构造测试样例!并不是越长越好!
好的一组测试样例是,每个都特别精短,但每个样例都精准指向一个潜在的可能bug
bug修复:
对于测出的bugs,应该每修复一个就进行一次提交记录,这样不但是一个良好的习惯,也会让我们对问题有一个更清晰更具体的认识,更利于我们跟踪错因,提炼出经典的错误模式,在下次编写代码时候,有意识的来注意避免。
对于测出的bugs,应该每修复一个就进行一次提交记录,这样不但是一个良好的习惯,也会让我们对问题有一个更清晰更具体的认识,更利于我们跟踪错因,
同意。