整体架构
第一次实验中问题比较简单,我的设计架构十分面向过程,只有两个类这里就没脸说了,平均的方法复杂度也是蛮高的。
method | ev(G) | iv(G) | v(G) |
Expression.addExp(BigInteger,BigInteger) | 1.0 | 2.0 | 2.0 |
Expression.Expression(String) | 7.0 | 6.0 | 9.0 |
Expression.getCoeff(String) | 5.0 | 8.0 | 8.0 |
Expression.getDifferential() | 4.0 | 14.0 | 15.0 |
Expression.getExpSet() | 1.0 | 1.0 | 1.0 |
Expression.getMultiply(String) | 2.0 | 2.0 | 2.0 |
Expression.getPower(String) | 2.0 | 3.0 | 3.0 |
Run.main(String[]) | 1.0 | 2.0 | 2.0 |
Total | 23.0 | 38.0 | 42.0 |
Average | 2.875 | 4.75 | 5.25 |
到了第二次我才分出了Expression, Term, 和Factor类,Factor之间相乘形成Term,Term之间相加形成Expession。但这次我并没有运用接口或抽象类,还是没有很面向对象。方法的平均复杂度下降了一些,但也有一些方法的复杂度仍然较高。
第二次作业类图
Term.toString() | 4.0 | 7.0 | 8.0 |
Term.Term(String) | 2.0 | 5.0 | 8.0 |
Term.Term(Factor,Factor,Factor,BigInteger) | 1.0 | 1.0 | 1.0 |
Term.Term() | 1.0 | 1.0 | 1.0 |
Term.simplify(Term) | 4.0 | 10.0 | 11.0 |
Term.multiply(Term) | 2.0 | 1.0 | 2.0 |
Term.hashCode() | 1.0 | 1.0 | 1.0 |
Term.getRemain(FactorType) | 5.0 | 2.0 | 5.0 |
Term.getDifferential() | 1.0 | 4.0 | 4.0 |
Term.getCoefficient() | 1.0 | 1.0 | 1.0 |
Term.equals(Object) | 1.0 | 1.0 | 1.0 |
Run.main(String[]) | 1.0 | 2.0 | 2.0 |
FormatException.FormatException() | 1.0 | 1.0 | 1.0 |
Factor.toString() | 11.0 | 8.0 | 11.0 |
Factor.multiplySameType(Factor) | 2.0 | 2.0 | 2.0 |
Factor.getPower() | 1.0 | 1.0 | 1.0 |
Factor.getFactorType() | 1.0 | 1.0 | 1.0 |
Factor.getDifferential() | 6.0 | 5.0 | 6.0 |
Factor.Factor(String) | 4.0 | 4.0 | 4.0 |
Factor.Factor(FactorType,BigInteger) | 1.0 | 1.0 | 1.0 |
Factor.equals(Object) | 1.0 | 1.0 | 1.0 |
Factor.construct(String) | 1.0 | 1.0 | 2.0 |
Expression.toString() | 4.0 | 6.0 | 7.0 |
Expression.simplify() | 4.0 | 4.0 | 4.0 |
Expression.replaceBack(String) | 1.0 | 1.0 | 1.0 |
Expression.replace(String) | 1.0 | 1.0 | 1.0 |
Expression.getDifferential() | 1.0 | 3.0 | 3.0 |
Expression.Expression(String) | 7.0 | 3.0 | 8.0 |
Expression.Expression(ArrayList) | 1.0 | 2.0 | 2.0 |
Expression.Expression() | 1.0 | 1.0 | 1.0 |
Expression.combine(Expression) | 1.0 | 3.0 | 3.0 |
Expression.cleanSign(String,int) | 4.0 | 5.0 | 6.0 |
Expression.checkSpaceError(String) | 2.0 | 1.0 | 2.0 |
Expression.checkSign(String) | 2.0 | 2.0 | 3.0 |
Expression.addExp(Term) | 2.0 | 2.0 | 3.0 |
Total | 85.0 | 96.0 | 120.0 |
Average | 2.361111111111111 | 2.6666666666666665 | 3.3333333333333335 |
第二次作业复杂度分析
第三次作业中我对Factor类使用了抽象类,并由四个子类NumFactor, SinCosFactor, PowerFactor, ExpFactor分别实现Factor类中声明的方法。但还是没有对Expression,Term, Factor这几个顶层类运用接口,也未将字符串处理单独分离出来形成类,而是嵌入了Expression,Term,Factor的构造方法之中,颇显臃肿。类与类之间的耦合也较严重,类图画出来颇显杂乱。但方法的平均复杂度进一步降低,个别复杂度高的方法的数量也减少了很多。
第三次作业类图
method | ev(G) | iv(G) | v(G) |
Construct.constructFactor(String) | 6.0 | 6.0 | 6.0 |
Construct.outerBracketMatch(String) | 5.0 | 6.0 | 6.0 |
ExpFactor.combine(Factor) | 1.0 | 1.0 | 1.0 |
ExpFactor.equals(Object) | 1.0 | 1.0 | 1.0 |
ExpFactor.ExpFactor(Expression) | 1.0 | 1.0 | 1.0 |
ExpFactor.ExpFactor(String) | 1.0 | 1.0 | 1.0 |
ExpFactor.getDifferential() | 1.0 | 1.0 | 1.0 |
ExpFactor.toString() | 2.0 | 2.0 | 2.0 |
Expression.checkSign(String) | 2.0 | 2.0 | 3.0 |
Expression.checkSpaceError(String) | 2.0 | 1.0 | 2.0 |
Expression.cleanSign(String,int) | 4.0 | 5.0 | 6.0 |
Expression.combine(Expression) | 1.0 | 1.0 | 1.0 |
Expression.combineTerm() | 5.0 | 5.0 | 5.0 |
Expression.Expression() | 1.0 | 1.0 | 1.0 |
Expression.Expression(ArrayList) | 1.0 | 1.0 | 1.0 |
Expression.Expression(String) | 5.0 | 6.0 | 8.0 |
Expression.getDifferential() | 1.0 | 2.0 | 2.0 |
Expression.handleMultiSigns(String) | 4.0 | 1.0 | 4.0 |
Expression.onlyOneFactor() | 1.0 | 1.0 | 1.0 |
Expression.replace(String) | 1.0 | 1.0 | 1.0 |
Expression.replaceBack(String) | 1.0 | 1.0 | 1.0 |
Expression.simplify() | 1.0 | 2.0 | 2.0 |
Expression.toString() | 4.0 | 3.0 | 4.0 |
Factor.Factor() | 1.0 | 1.0 | 1.0 |
FormatException.FormatException() | 1.0 | 1.0 | 1.0 |
NumFactor.combine(Factor) | 2.0 | 2.0 | 2.0 |
NumFactor.equals(Object) | 2.0 | 2.0 | 2.0 |
NumFactor.getDifferential() | 1.0 | 1.0 | 1.0 |
NumFactor.getNumber() | 1.0 | 1.0 | 1.0 |
NumFactor.NumFactor(BigInteger) | 1.0 | 1.0 | 1.0 |
NumFactor.NumFactor(String) | 1.0 | 1.0 | 1.0 |
NumFactor.toString() | 1.0 | 1.0 | 1.0 |
PowerFactor.combine(Factor) | 2.0 | 2.0 | 2.0 |
PowerFactor.equals(Object) | 2.0 | 2.0 | 2.0 |
PowerFactor.getDifferential() | 1.0 | 1.0 | 1.0 |
PowerFactor.getPower() | 1.0 | 1.0 | 1.0 |
PowerFactor.PowerFactor(BigInteger) | 1.0 | 1.0 | 1.0 |
PowerFactor.PowerFactor(String) | 3.0 | 2.0 | 3.0 |
PowerFactor.toString() | 1.0 | 2.0 | 2.0 |
Run.main(String[]) | 1.0 | 2.0 | 2.0 |
SincosFactor.combine(Factor) | 3.0 | 3.0 | 3.0 |
SincosFactor.equals(Object) | 2.0 | 2.0 | 2.0 |
SincosFactor.getDiffCoeff() | 2.0 | 2.0 | 2.0 |
SincosFactor.getDifferential() | 1.0 | 1.0 | 1.0 |
SincosFactor.getDiffType() | 2.0 | 1.0 | 2.0 |
SincosFactor.getPower() | 1.0 | 1.0 | 1.0 |
SincosFactor.SincosFactor(String,String,Type) | 3.0 | 2.0 | 3.0 |
SincosFactor.SincosFactor(Type,Factor,BigInteger) | 1.0 | 1.0 | 1.0 |
SincosFactor.toString() | 1.0 | 2.0 | 3.0 |
Term.canCombine(Term) | 3.0 | 3.0 | 3.0 |
Term.checkCombine() | 5.0 | 5.0 | 5.0 |
Term.checkZero() | 3.0 | 10.0 | 10.0 |
Term.combine(Term) | 1.0 | 1.0 | 1.0 |
Term.getDifferential() | 1.0 | 2.0 | 2.0 |
Term.isEmpty() | 1.0 | 1.0 | 1.0 |
Term.onlyOneFactor() | 1.0 | 1.0 | 1.0 |
Term.simplify() | 2.0 | 1.0 | 2.0 |
Term.Term(ArrayList) | 1.0 | 1.0 | 1.0 |
Term.Term(String) | 2.0 | 5.0 | 5.0 |
Term.toString() | 1.0 | 2.0 | 2.0 |
Total | 112.0 | 123.0 | 137.0 |
Average | 1.8666666666666667 | 2.05 | 2.283333333333333 |
第三次作业复杂度分析
听了课上同学们的分享,再和自己的架构进行对比后,深感自己对面向对象理解还很不到位,很多思想仍然很面向对象,决心好好学习QAQ!
输入处理思路
这次的表达式求导实验中,我认为最难的点在于对输入字符串的识别和处理,在第一次作业中由于输入的格式较为固定,所以我使用了正则表达式+类似于状态机的识别方法,从头到尾依次根据输入是否有符号有几个符号,是否有数字,是否有x项及是否有次方等,对当前状态进行转移并处理(但我写的时候并没有认识到这是一个状态机,所以直接用if和else嵌套一大堆来实现了orz)。
但这个方法到了第二次作业就失效了,因为第二次作业不仅加入了sin(x),cos(x)因子,还没有了对因子位置和数量的限制。所以我就转变了思路,开始使用在第一次实验中我本身不太认可的方法——将整个表达式用+/-分割为若干项,再将每一项用*分割为若干因子。在分割之前,需要对是否有非法字符、是否有非法空格位置,以及连续出现的+/-号是否合法等问题进行处理,且在利用+/-进行分割时,由于表达式中不仅在项和项连接处会出现+/-,在项内也会出现,但都与*或^相连,所以我将这些项内的 ^+/^-/*+/*- 用其他字符进行了替换处理再进行分割(在分割后再换回)。用*分割项时直接分割即可。另外,在将表达式分割时,默认分割的各项是合法的,在分割项时也默认各因子是合法的,其具体合法性在分割结束后传入下层结构(Term/Factor)进行识别时再分别判断。
1 //蠢之又蠢的替换QAQ 2 private String replace(String str) { 3 return str.replaceAll("\\^\\+", "@").replaceAll("\\^-", "#") 4 .replaceAll("\\*\\+", "&").replaceAll("\\*-", "%"); 5 } 6 7 private String replaceBack(String str) { 8 return str.replaceAll("@", "^+").replaceAll("#", "^-") 9 .replaceAll("&", "*+").replaceAll("%", "*-");10 }
在第三次作业中,如何将第二次作业的方法继续应用又使我伤了脑筋,我本着能修改就不重构的原则(误)最终还是将第二次的方法完美使用在了第三次作业中。
第三次作业的特点是加入了因子和表达式的嵌套,但我们发现,所有嵌套的发生都伴随着两个特征符号即为”(“ ”)“的出现,如果无视括号内的内容,将括号作为一个整体来看,就可以完全地使用第二次作业中的办法进行处理。怎么使程序将括号整个当做整体看待呢?我能想到的就只有替换了,既然正则表达式无法很好地完成括号匹配的任务,那么我就请出了括号匹配界的专家——栈,使用栈对表达式的括号进行匹配检查,并找到当前字符串内“最外侧”的括号,将这些括号内的字符串内容用一个识别串(含编号)进行替换,并将其内容存入Arraylist中。这样就可以套用第二次的方法无视括号内的内容进行分割,在分割后再将之前替换掉的内容根据编号替换回来即可。最外层括号识别替换在处理Expression和Term的时候均有使用,至于多层括号的情况,就交给Expression,Term,Factor之间的嵌套来自动处理啦!
1 //匹配最外层括号,返回他们的位置 2 public TreeMapouterBracketMatch(String str) { 3 Stack stack = new Stack<>(); 4 TreeMap brackets = new TreeMap<>(); 5 for (int i = 0; i < str.length(); i++) { 6 if (str.charAt(i) == '(') { 7 stack.push(i); 8 } else if (str.charAt(i) == ')') { 9 if (stack.empty()) {10 return null;11 } else {12 int t = stack.pop();13 if (stack.empty()) {14 brackets.put(t, i);15 }16 }17 }18 }19 return brackets;20 }
以上就是我的输入处理方法,写出来真的蛮显冗长,也不推荐大家使用,但还是希望能给大家带来一些想法吧。看了大佬的递归下降方法之后,我就觉得我的方法实在是难以入目了,滚去膜大佬了。
BUG
本次实验中比较幸运的是,在公测和互测中一共只在第二次作业的互测中被找出一个bug(但是被残忍地刀了二十多次),这个bug是由于我在解析表达式,对表达式进行分割时使用了String.split(“*”),但是忽略了这个方法在分割时会自动忽略头尾*的特点(我以为如果头尾有*的话,会多分出来一个空字符串,然而并不会),解决方法是加一个头尾*的检测判断,虽然有效,但这样使得整个程序颇有拆拆补补的感觉,这也是我的整体设计上不好的地方,十分容易出现这种小的问题需要分别考虑并解决。
在检查他人bug时,我基本使用的是经验数据来进行测试。我发现大家除了在第一次作业中存在爆栈的情况外,之后的作业中大多不会在正常或者较长的数据中产生bug,相反的是容易在一些较短的特殊数据中产生bug,例如:
*++1231 x1*
等仅有一个或数个符号,或仅有一个常数,或多了少了一个符号的地方发生错误。这说明了大家在考虑复杂问题的时候,忽略掉了简单的边界问题。
Applying Creational Pattern
(这一节是要说自己的改进想法吗?)
在我的实验中,我没能充分利用面向对象设计构造中接口的特性,所以最重要的,我会在Expression, Term, Factor上定义一个Computerable的接口,其中声明getDifferential,simplify,combine等方法,由Expression,Term,Factor等类和接口进行继承和实现;并且将字符串处理单独建立ExpParser, TermParser, FactorParser等类和接口进行处理,并且尝试使用面向对象的递归下降算法对输入进行处理和解析。