面向对象程序设计第一单元总结(求导程序)
2019面向对象课程序设计第一单元总结
前言
对于像我这样的第一次接触面向对象编程的人来说,在写第一次作业的时候我就一直在思考,我的程序怎么样才能写的有面向对象思维,在翻阅了许多书籍以后,我也只是粗略地有个模糊印象,比如每个类里面的方法需要尽量地减少对其他类方法的依赖,做到尽量地独立。在分析问题的时候要先考虑问题中的元素能够抽象成什么样的对象,比如第三次作业设计的时候,我在看完指导书就有了一个架构的设计:通过表达式->项->因子->表达式构建一个树形结构来进行链式求导,因此对于求导问题就可以抽象为表达式、项、因子之间的交互,最终通过它们各自的求导规则来完成所要求的求导服务。因子又可以作为一个父类,三角函数、幂函数和常数作为它们的子类,对构造器和方法进行重写,再次使用它们各自的求导规则进行求导,通过第三次作业我也渐渐明白了面向对象的思想,希望能在下一个单元的学习中有更多的收获。
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 }
其次,由于在分割因子的时候'*'会出现在括号内部,也必须重写split函数进行纠正:
1 private ArrayList<String> split(String s) { 2 int stack = 0; 3 int index = 0; 4 int i = 0; 5 ArrayList<String> out = new ArrayList<>(); 6 StringBuilder t = new StringBuilder(s); 7 String temp; 8 for (i = 0; i < s.length(); i++) { 9 if (s.charAt(i) == '(') { 10 stack++; 11 } else if (s.charAt(i) == ')') { 12 stack--; 13 } 14 if (stack == 0 && s.charAt(i) == '*') { 15 temp = t.substring(index, i); 16 out.add(temp); 17 index = i + 1; 18 } 19 } 20 out.add(t.substring(index, i)); 21 return out; 22 }
最后在sin(*)中只能是因子,因此必须也要进行判断,也要注意sin((x)+(x)) 和sin(- 9)这种不合法情况。
最后是表达式解析的类的属性的声明,总体的思路就是表达式解析成多个项,项解析成多个因子,因子若有括号则解析成表达式:
1 public class Poly { 2 private String input = ""; 3 private ArrayList<Term> childs; 4 } 5 6 public class Term { 7 private char op; 8 private String stringTerm; 9 private ArrayList<Factor> terms; 10 } 11 12 public class Factor { 13 private String function = ""; 14 private BigInteger level; 15 private static final BigInteger limit = new BigInteger("10000"); 16 private ArrayList<Poly> polys; 17 }
- 程序结构分析
总体的构造我已在输入分析时说明,factor类是constant、sin、cos、power的一个父类,visit方法是一个对树的遍历和求导,derivate是对各个类的求导。
- 程序复杂度分析
对于类的循环复杂度还是较高,主要是几个主类的对输入合法模式的判定需要遍历,而结构复杂度,Poly类在处理输入函数的时候必须添加很多条件才能保证其是合法的,这一点可能还有很多可以优化的地方,但各个方法的紧密度都不是很高,结构复杂度也都在init(),即初始化函数上,这点可能是无法避免的,可能需要有一个更好的架构来实现。
- 总结
本次作业虽然难度大大增加,但是只要化整为零,把整体划分为类,通过类中定义的方法独立实现各个类的求导需求,其实并不困难,本次作业我没有采用任何的优化,因为在第二次作业优化导致强测错了好多点的情况下,我更倾向于使用更安全的方法。
程序bug分析:
在本单元的作业中,第一次和第三次作业我没有被发现任何bug,只有第二次作业的强测中错了两个测试点,原因是写优化函数的时候忽略了一个符号的问题,导致求导结束后符号出现了错误,非常粗心的一个bug,以后在写完程序的时候还需要更加仔细地测试。
发现别人bug所采取的策略:
前两次作业中,因为求导的方法非常的简单,并且由于必须在强测中拿到50%的分数才能进入互测,我也就默认他们的求导函数是正确的,因此我只看每个人的输入处理和输出处理部分,输入处理只需看其正则表达式的行为是否有错误,而输出环节出错情况较多,例如有一位同学:
1 out = out.replaceAll("1\\*", "");
这显然是不可取的,我自己也有错过,但在测试中发现了所以改正了,如果求完导的结果是:sin(x)^111*12,结果显然是错误的,应该增加一个判断条件
1 if (out.matches("[+-]1\\*.*")) { 2 out = out.replaceFirst("1\\*", ""); 3 }
只对每一项的第一项的1*进行化简,并且由于求完导没有'+',也不会有-1次幂,这样做是安全的。
如果看了代码以后不明白,可以随便测试几组容易错的数据再看,这样可以更快发现bug,或者采用自动化测试方法。
Applying Creational Pattern:
在三次作业中我也对自己的结构进行了说明,每次作业之间面向对象思想也逐渐体现了出来,但是对创建设计模式的五种形态还不是特别了解,但我的构造模式类似于工厂方法的模式,以第三次作业为例,我的方法中没有应用接口,并且对象是在判断了类型以后才实例化到子类的,这点和工厂方法的思想类似,所以其实可以把因子类抽象为一个接口,其他四种类型的因子通过接口实现函数。
接口定义:
1 public interface Factor { 2 void derivate(){}; 3 void init(){}; 4 void addchild(){}; 5 }
创建实体类:
1 public class Constant implements Factor { 2 private Function; 3 private level; 4 private void derivate(){}; 5 private void init(){}; 6 private addchild(){}; 7 } 8 public class PowerFunction implements Factor { 9 ... 10 } 11 public class SinFunction implements Factor { 12 ... 13 } 14 public class CosFunction implements Factor { 15 ... 16 }
定义工厂方法:
1 public class FactorFactory{ 2 public Factor getFactor(String Factor){ 3 if(Factor.matches(Pattern_Constant){ 4 return new Constant(Factor); 5 } else if(Factor.matches(Pattern_power)){ 6 return new PowerFunction(Factor); 7 } else if(Factor.matches(Pattern_sin)){ 8 return new SinxFuntion(Factor); 9 } else if(Factor.matches(Pattern_cos)){ 10 return new CosxFuntion(Factor); 11 } 12 return null; 13 } 14 }
创建对象:
1 main: 2 input = scan.nextline(); 3 FactorFactory s = new FactorFactory(); 4 //创建一个工厂 5 Factor t = s.getFactor(input); 6 //创建一个指定类型的对象
7 if(!t)
8 t.init(); 9 //调用其求导函数
10 else
11 ...
总结:
由于本人是第一次接触java,对面向对象的编程思想和java的一些语法还不是很熟悉,但是在写代码的过程中通过查阅资料、参考讨论帖等方式大致明白了面向对象的编程思想和基础语法,并且本单元作业中本人幸运地没有被发现很多bug,也许只是恰好测试没有覆盖到我的bug吧。并且在动手写代码之前的设计非常重要,一个好的设计可以事半功倍,写代码时也能思路清晰,速度更快。对于自动化测试的知识我还是较为欠缺,希望能多多学习讨论区大佬们的测试方法,在以后的作业中多应用自动化测试来减轻测试压力和互测压力。