前言
对于像我这样的第一次接触面向对象编程的人来说,在写第一次作业的时候我就一直在思考,我的程序怎么样才能写的有面向对象思维,在翻阅了许多书籍以后,我也只是粗略地有个模糊印象,比如每个类里面的方法需要尽量地减少对其他类方法的依赖,做到尽量地独立。在分析问题的时候要先考虑问题中的元素能够抽象成什么样的对象,比如第三次作业设计的时候,我在看完指导书就有了一个架构的设计:通过表达式->项->因子->表达式构建一个树形结构来进行链式求导,因此对于求导问题就可以抽象为表达式、项、因子之间的交互,最终通过它们各自的求导规则来完成所要求的求导服务。因子又可以作为一个父类,三角函数、幂函数和常数作为它们的子类,对构造器和方法进行重写,再次使用它们各自的求导规则进行求导,通过第三次作业我也渐渐明白了面向对象的思想,希望能在下一个单元的学习中有更多的收获。
1.需求分析
本次作业总体目标是构建一个能判断输入是否合法、并且能对合法的输入表达式进行求导的程序。如果输入不合法,则输出WRONG FORMAT! 如果输入合法,则应当求出正确答案。
2.程序设计思路与结构分析
这部分我采用了IDEA的Metrics插件的Complexity mertics,对于这个插件有一些术语在这里需要解释一下:
- ev(G): 一个方法或者类的结构化复杂度,值越高复杂度越高。
- iv(G): 一个方法及其调用的其他方法的紧密程度,值越高则紧密程度越高。
- v(G): 一个方法或类的循环复杂度,值越高则循环复杂度越高。在类中,有OCavg和WMC两个指标,分别代表类的方法的平均循环复杂度和总循环复杂度。
2.1:第一次作业
- 思路:
第一次作业只要求了简单多项式的求导,因此思路也很简单,只需使用公式即可。
-
输入处理
一开始的时候本人采用的是大正则匹配法,即使用一个正则表达式来对所有合法情况进行处理,这样带来的问题是会导致输入字符串过长的时候stackOverflow,所以后来我就采用了项匹配法:
1 String next = "[ \\t]*[+-][ \\t]*[+-]?" + "(" 2 + "([ \\t]*x([ \\t]*\\^[ \\t]*[+-]?\\d+)?" + 3 "|\\d+[ \\t]*\\*[ \\t]*x([ \\t]*\\^[ \\t]*[+-]?\\d+)?" 4 + "|\\d+)[ \\t]*)";//一项的正则表达式
每找到一项以后,再匹配下一项:
1 while (m.find()) { 2 start = m.start(); 3 if (start != end) { //如果这次开始和上次结束的位置不一致,则匹配失败 4 System.out.print("WRONG FORMAT!"); 5 return; 6 } 7 end = m.end(); 8 } 9 if (end != input.length()) { 10 System.out.print("WRONG FORMAT!"); 11 return; 12 }
这样就成功地化整为零,避免了爆栈。
-
求导函数设计思路
程序设计思路较为简单,只需采用公式法对每一项进行求导即可,为此构建一个Poly类进行相应的操作,具体架构见第三部分。
-
程序结构分析
整体结构如下图:背景为壁纸
Main为主函数,调用Poly类函数的方法进行求导与输出,derivate为求导方法,combine为合并同类项方法,output为输出表达式方法,isnature为检查是否为正数方法。而outout调用combine,combine调用derivate,main调用output,这次作业还是有很多面向过程的思维存在。
- 复杂度分析:(class\method)
可以看出,本次作业中我各个类的循环复杂度都较高,因为我用的是Arraylist,所以查找效率较低,建议可以采用Hashmap。而方法的结构复杂度较低,有少数几个方法的紧密度>=10而标为了红色,这是因为多次调用了isnature方法来检查是否为正数;循环复杂度还是由于采用实现简单但效率低下的Arraylist结构,导致了循环复杂度较高。
- 总结:
第一次作业还是有较多的面向过程思维的存在,可能题目本身就适合面向过程写吧并且为了贪图实现的方便采用了Arraylist结构,没有使用效率更高的Hashmap,其次本次优化也很简单,只需正项提前即可。
2.2:第二次作业
-
思路
第二次作业相对于第一次作业更加复杂了一点,除了引入三角函数之外,还需要支持因子的连乘,但变化不是很大,只需给每一项设置4个属性:常数、幂函数指数、三角函数(sin、cos)指数即可,而对于a*xc*sin(x)d*cos(x)e,是有确定的导函数的,可以通过公式来实现求导。
- 输入处理
同样采用上一次作业的项匹配法,匹配到一个项的时候,就将其作为term的一个对象存入。这里就不再重复展示代码。
- 求导函数设计
和第一次作业一样,采用求导公式:
1 Poly output(Poly temp, String s) { 2 init(getTerm(s)); 3 if (op == '-') { 4 num[0] = num[0].multiply(BigInteger.valueOf(-1)); 5 } 6 temp.addItems(temp, num[1].multiply(num[0]), 7 num[1].subtract(BigInteger.ONE), num[2], num[3]); 8 temp.addItems(temp, num[2].multiply(num[0]), 9 num[1], num[2].subtract(BigInteger.ONE), 10 num[3].add(BigInteger.ONE)); 11 temp.addItems(temp, 12 num[3].multiply(num[0]).multiply(BigInteger.valueOf(-1)), 13 num[1], num[2].add(BigInteger.ONE), 14 num[3].subtract(BigInteger.ONE)); 15 return temp; 16 }
看起来很复杂,只是普通的求导函数的写法而已。
- 程序结构分析
整体结构如下图:
main函数调用Poly的output方法进行输出,Poly类构造term的对象,并调用term中的求导函数完成求导以后的项,存到Poly中,最后通过三次合并(第一次合并同类项,第二、第三次进行三角恒等变换),输出最终结果。
- 程序复杂度分析
类和方法的复杂度如下:
可以看出各个类的循环复杂度都较高,因为在合并同类项的时候有三次遍历搜索,所以效率非常低,而各个方法中也能看出,输出部分和合并同类项部分的循环复杂度和与其他方法的紧密很高,因为都有一个遍历的过程,并且在合并同类项的时候频繁调用了一些判断是否相差2(ifLessTwo)、减2(subTwo)、删除项等方法。好在其他的方法依赖度并不高,结构也控制的较好。
- 总结
本次作业中我开始有了一些面向对象思维的展现,把表达式、项各当成了一个对象,而因子可以归为项的一个属性,因此也就没有增加因子类,这次的设计中除了在优化过程中的循环复杂度较高外,依赖度和结构复杂度都有了一个较好的控制。
2.3 第三次作业
- 思路
第三次作业和第二次作业有了难度较大的提升,表达式支持嵌套、允许有括号的存在使得我前两次作业的思路完全无法参考,但是在看指导书的过程中,表达式->项->因子->表达式...这个树形结构的存在,使我有了构造表达式树的想法,并且这次作业完全可以抽象为多个不同的类,完全可以使用面向对象的思维进行解答。
- 输入处理
我的输入处理较为复杂,在使用正则表达式匹配的过程中,我对于表达式因子采用了 \\(.+\\)这种匹配方法,但是遇到(x)+(x)的时候就会匹配成(x)+(x)整个项,这显然是不符合要求的,因此我采用了栈的方式手动对其进行纠正。
因子的正则表达式:
1 private static final String factor = 2 "[ \\t]*(((sin[ \\t]*\\(.+\\)" + 3 "|(cos[ \\t]*\\(.+\\))" + 4 "|x[ \\t]*)([ \\t]*\\^[ \\t]*[+-]?\\d+)?)" + 5 "|[+-]?\\d+|\\(.+\\))[ \\t]*";
对正则表达式的匹配纠正(较长,因此折叠)
1 private void makeTerm() { 2 try { 3 String term = ""; 4 StringBuilder temp = new StringBuilder(input); 5 int start = 0; 6 int end; 7 int stack = 0; 8 char s; 9 char lastchar = '\0'; 10 boolean flag = false; 11 for (int j = 0; j < input.length(); j++) { 12 s = input.charAt(j); 13 if (j == input.length() - 1) { 14 term = temp.substring(start, j + 1); 15 if (term.matches(pattern)) { 16 Term newTerm = new Term(term); 17 childs.add(newTerm); 18 } else { 19 throw new FormatException(); 20 } 21 }//读到行末,结束 22 if (String.valueOf(s).matches("\\s")) { 23 continue; 24 }//跳过空格 25 if (s == '(') { 26 stack++; 27 } else if (s == ')') { 28 stack--; 29 } 30 if (flag && (s == '+' || s == '-') 31 && (lastchar == '*' || lastchar == '^')) { 32 continue; 33 }//跳过'*'和'^'后面的'+'和'-' 34 35 if (s != '+' && s != '-' && !flag) { 36 flag = true; 37 }//跳过前导加号 38 39 if (stack == 0 && (s == '+' || s == '-')) { 40 if (flag) { 41 end = j; 42 term = temp.subSequence(start, end).toString(); 43 start = j; 44 if (term.matches(pattern)) { 45 Term newTerm = new Term(term); 46 childs.add(newTerm); 47 stack = 0; 48 flag = false; 49 } else { 50 throw new FormatException(); 51 } 52 } 53 }//满足条件则存入 54 lastchar = s; 55 }