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


    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,这次作业还是有很多面向过程的思维存在。

    • 复杂度分析:(classmethod)

                   

                                 

            

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

  • 相关阅读:
    XMPP核心协议客户端
    平安中国
    读写XML的API们
    IM只是可以用来玩的东西
    再骂自己一句
    淡定
    自己打造SAX和DOM Parser
    Nickel Instant Messeging System
    XMPP RFC阅读笔记(二)
    think in java 笔记
  • 原文地址:https://www.cnblogs.com/bakahentai/p/10585660.html
Copyright © 2020-2023  润新知