OO_Unit1——表达式求导

OO_Unit1——表达式求导

1 程序结构分析

  1.1 Home1——简单多项式求导

    • 基本思路
      • 第一次作业的情况较为理想化,只有简单的多项式,并且无非法输入,因此求导方法单一,只需要对系数coeff和指数degree做一些特殊判断即可,故我的做法就是建立一个Poly项类,和一个PolyDiff表达式重构类(主类),利用容器保存TreeMap来保存各个项并去重,因此生成的UML图很简单。   
    • Metrics复杂度分析: 
      • Poly.toString的三种复杂度都比较高,原因是将每一个抽象的项转化成一个具体的表达式字符串的过程中,需要考虑系数和指数为+1, -1, 0的特殊情况,因此该方法中有较多的if-else分支语句。 
      • 主类中的main方法也有较高的复杂度,原因是其中包含了对从TreeMap中取出的各个项的组合以及输出语句字符串的处理,没有对输出进行额外的封装成方法调用。 
      • PolyDiff.parsePoly的基本复杂度很低,但是模块设计复杂度以及圈复杂度较高,原因是其功能较复杂,是本次作业的核心处理部分,需要从输入中提取出每一项并在判断系数和指数的过程中有较多的if-else来进行特殊处理。

OO_Unit1——表达式求导_第1张图片OO_Unit1——表达式求导_第2张图片


 

  1.2 Home2——新增sin和cos项   

    • 基本思路:
      • 这次作业相对于前一次的递进在于,新增了sin和cos项,求导方式多样化,并且需要判断非空白符的非法输入。对于输入WRONG FORMAT的判断,我采用的是将指导书中的形式化表述直接翻译成分层正则表达式,不过其中对于项和表达式的形式化表述存在轻微的递归,因此正则编写时稍微做了些等效修改,这样一来能完全通过正则来匹配判断非法并且非常严谨不出错误。   
      • 而对于新增了三角函数的求导方式,并不会造成太大的难度,我仍然只需要用到和上一次作业类似的两个类,一个是主类MainClass,另一个仍然是项类Term,而本次的项类与上次不一样,上的一个项只由系数+x幂指数组成,这次一个项由系数+x幂指数+sin幂指数+cos幂指数组成,求导方式改为乘积求导。但这样做在优化上只进行了系数合并而没用到三角恒等关系合并,因此另外再写了一个专门优化的函数。   
    • Metrics复杂度分析:     
      • 因为第二次作业是在第一次作业框架基础之上添加新功能而得来的,因此在复杂度上基本和第一次类似,都集中在main方法、parsePoly方法和toString方法三处存在较复杂的结构。

OO_Unit1——表达式求导_第3张图片OO_Unit1——表达式求导_第4张图片

 


 

  1.3 Home3——新增嵌套求导     

    • 基本思路:
      • 本次作业相比于之前的两次,有一个较大的难度飞跃,首先新增了对于空白字符的WRONG FORMAT判断,其次项内分布存在多样的嵌套关系,表达式的复杂度大大提升,输出还必须符合一定的格式要求,因此无法像前两次作业那样以项为单元进行存储,而更加具体细化为对因子的存储和处理,所以我的做法相比于前两次有了很大的区别,基本相当于重构。    
      • 我的思路流程:判断空白字符的非法→判断非空白字符的非法→拆解表达式,构建表达式二叉树→利用表达式二叉树的根节点链式生成求导表达式并输出    
    • 结构分析:
      • 建立一个总接口TreeNode,之后建立两个子接口继承TreeNode,分别为Factor和Combination,分别作为表达式二叉树的叶子结点和分支结点,叶子结点是表达式因子,包括Constant,Power,SinFactor和CosFactor,分支结点是运算Plus,Minus,Mult,Nest(嵌套),之后再在Combination包中建立一个Optimizer类用于优化,最后是MainClass用于统筹调用。   
    • Metrics复杂度分析:       
      • MainClass.checkRecur用于递归检查输入表达式的合法性,因为存在自身多重递归调用以及用于判断的if-else分支语句,因此该方法复杂度偏高。
      • MainClass.getDegree用于提取表达式中的指数,因为内部逻辑存在多重循环,并且有较复杂的判断语句,可读性不高,复杂度偏高。
      • MainClass.parseTree用于拆解表达式,是表达式解析的核心部分,因此分支语句很多,偏向于面向过程,修改起来不方便,容易出bug,故复杂度很高。     

OO_Unit1——表达式求导_第5张图片

OO_Unit1——表达式求导_第6张图片

 


 

2 BUG分析     

  2.1 Home1       

    • 本次作业复杂度较低,结构短小精悍并且覆盖全面,优化上也能够穷尽所有情况,所以最终强测与互测都没有产生bug,优化也达到了极致

  2.2 Home2     

    • 本次作业难度的提升主要在优化上,因为很难穷尽所有情况,并且实现起来较复杂,在强测中比较幸运未测出bug,互测中被hack了7次,事后证明都属于同质bug,原因在与优化时,将两项进行三角恒等变换合并后就直接put到了HashMap中,而未考虑HashMap中本身就存在与其相同Key值的项,从而将原有的项覆盖了。这个Bug很隐蔽(我的样例生成器都没产生这样的样例,说明这种bug的样例随机产生的概率很低)但也很致命。

  2.3 Home3   

    • 本次作业不仅在保证正确性上就费了我九牛二虎之力,最后的输出中存在大量的括号,因此优化上在这有限的一周时间内基本无法预测并合理减少长度,保不准还会出现难以预知的隐藏bug点,而我就是因为优化而导致了bug,强测挂了一个点,互测被hack了三次,最终证明都是同质bug,是在优化是对1的省略上考虑不充分所致。   

 

3 测试策略

——事实上,这三次作业,hack别人的测试策略都是与给自己测试的策略一致的

  3.1 Home1     

    • 利用python的xeger和sympy两个包,xeger用于根据我提前设计好的正则表达式,提前设定好了字符串长度范围,来生成相应的输入样例;sympy用于从txt中读入程序的输出和xeger的产生的输入,进行[-10,10]内的1000次代x求值并进行比较,基本和指导书中所说的测评机测评方式完全一致,整个过程都是半自动化的。效果很好,使自己的程序排除了所有bug顺利进入Aroom,因此在互测中也为测出其他人的bug。

  3.2 Home2   

    • 采用的方式和第一次作业基本一致,只对产生样例的正则表达式做了修改来满足第二次作业的输入条件,效果也很好,测出了许多bug,但是没有测出自己的bug,说明随机生成样例想要覆盖全面需要大量的测试,毕竟产生特定样式的样例的概率太低。

  3.3 Home3   

    • 这一次没有办法自动生成覆盖全面的样例了,因为指导书的形式化表述中有大量的递归无法通过java的正则表达式实现,因此就通过手动构造来进行测试,但是之前用sympy写的自动测试程序还是可以用来验证输出的正确性,因此第三次作业采用的是手动构造样例并自动验证输出的正确性,这就需要边写程序边思考有哪些坑点,来提前构思出有效的样例,并将自己测试过程中用过的样例保留来用于之后互测时测试别人,同时,经常和室友相互共享一些可能的坑点和有效的样例,因此在互测中还是有比较可观的收获。       

 


     

4 对象创建模式            

  • 因为前两次作业都是对每一项来建类,也就是说实现的每一个对象都是属于同一个类,因此在对象创建上十分简单,不必构建对象创建模式。    
  • 而第三次作业,我不仅需要对每一个基本因子建类,还需要对每一种运算方式建类,因此会需要创建很多不同的对象,这正是使用工厂模式大展身手的时候了!因此我建立了一个因子工厂和一个运算工厂,来分别统一了因子对象和运算方式对象的创建,从而很方便的就建立起了我所需要的的表达式二叉树。         

   

5 对比和心得体会        

  • 因为我一向的作风就是,采用最简洁的方法,利用最少的代码,来完成当前所需要的工作,达到当前所需要的功能就适可而止,没有想过要为下次作业奠定一些接口,所以我在前两次作业都只写了两个类,一个主类和一个项类,而且这刚好能够达到我所需要的目标,在互测中看到有的同学一下子写了十几个类,感到十分不可思议,认为这是画蛇添足小题大做。
  • 而到了第三次作业,我终于发现了这样做的眼光长远之处,即便是一直以来吝啬于代码量的我,也由原来的的两个类重构到了15个类,这起伏之大给了我很大的震撼,在之前的两次作业中,即便是功能复杂的主类,其代码量我也能控制在200行以内,但是到了第三次作业,主类的代码量就翻了一番,这不禁让我深刻反思自己之前的观念是否合理。
  • 的确,仅仅对于一次作业来说,越精简的代码越便于控制,bug越容易找,但是当功能大量扩充之后,代码的结构势必要错综复杂,既然代码量激增的趋势不可避免,那么就应该采用一种方法对整体的代码结构进行肢解,各司其职,就像去年学c语言时,由一main到底的作风转变为函数封装调用的过程一样,我现在也需要由传统的单一结构转变为层次化、继承、抽象的高效结构,试想以后在工作中,所面临程序往往是工程级别的功能复杂多样的,而非今日此时这般简单。
  • 而这种转变不是一朝一日的改变,而是需要一个长期的过渡期,在以后的单元中,对于第一次作业,不应该仅仅局限于对当前简单功能的解决,而应该先思考之后的作业会怎样在这次的基础之上进行拓展,会扩充哪些功能,对于有可能的合理的预测,应该提前预留好相应的结构,不能吝啬于建类,因为当前的看似繁杂都是为了之后迭代上的得心应手!

你可能感兴趣的:(OO_Unit1——表达式求导)