面向对象程序设计第一单元总结(求导程序)

2019面向对象课程序设计第一单元总结

前言

  对于像我这样的第一次接触面向对象编程的人来说,在写第一次作业的时候我就一直在思考,我的程序怎么样才能写的有面向对象思维,在翻阅了许多书籍以后,我也只是粗略地有个模糊印象,比如每个类里面的方法需要尽量地减少对其他类方法的依赖,做到尽量地独立。在分析问题的时候要先考虑问题中的元素能够抽象成什么样的对象,比如第三次作业设计的时候,我在看完指导书就有了一个架构的设计:通过表达式->项->因子->表达式构建一个树形结构来进行链式求导,因此对于求导问题就可以抽象为表达式、项、因子之间的交互,最终通过它们各自的求导规则来完成所要求的求导服务。因子又可以作为一个父类,三角函数、幂函数和常数作为它们的子类,对构造器和方法进行重写,再次使用它们各自的求导规则进行求导,通过第三次作业我也渐渐明白了面向对象的思想,希望能在下一个单元的学习中有更多的收获。

 

1.需求分析

  本次作业总体目标是构建一个能判断输入是否合法、并且能对合法的输入表达式进行求导的程序。如果输入不合法,则输出WRONG FORMAT! 如果输入合法,则应当求出正确答案。

 

2.程序设计思路与结构分析

  这部分我采用了IDEA的Metrics插件的Complexity mertics,对于这个插件有一些术语在这里需要解释一下:

  • ev(G): 一个方法或者类的结构化复杂度,值越高复杂度越高。
  • iv(G): 一个方法及其调用的其他方法的紧密程度,值越高则紧密程度越高。
  • v(G): 一个方法或类的循环复杂度,值越高则循环复杂度越高。在类中,有OCavgWMC两个指标,分别代表类的方法的平均循环复杂度和总循环复杂度。

 

   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             }
View Code

    其次,由于在分割因子的时候'*'会出现在括号内部,也必须重写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     }
View Code

    最后在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吧。并且在动手写代码之前的设计非常重要,一个好的设计可以事半功倍,写代码时也能思路清晰,速度更快。对于自动化测试的知识我还是较为欠缺,希望能多多学习讨论区大佬们的测试方法,在以后的作业中多应用自动化测试来减轻测试压力和互测压力。

posted @ 2019-03-24 14:32  那个夏天的蝴蝶  阅读(639)  评论(2编辑  收藏  举报
Live2D