【面向对象】一单元简单表达式求导总结

前言 


  这一周写了三个简单表达式求导的相关作业。这三次作业从易到难,从简单的只对x求导到嵌套链式求导,逐步引导我们使用面向对象的结构来解决问题。虽然难度是逐级增加的,但是对于从来没有接触过Java的我来讲,完成这几次作业有些辛苦。虽然我在网上搜索了许多资源 ,也在课下和同学们交流思路,但是还是有不太明白的地方。我希望自己能够通过这次博客,总结学到的面向对象思想、代码结构、编写测试集等知识。

基于度量来分析自己的程序结构


  在查看学长的博客后,我使用了IDEA的Metrics进行代码结构分析。先对Metric Reloaded的用法进行总结。

  学长博客链接:https://www.cnblogs.com/qianmianyu/p/8698557.html

  • Complexity Metrics
    • ev(G):Essentail Complexity,用来表示一个方法的结构化程度,范围在[1,v(G)]之间,值越大则程序的结构越“病态”,其计算过程和图的“缩点”有关。
    • iv(G):Design Complexity,用来表示一个方法和他所调用的其他方法的紧密程度,范围也在[1,v(G)]之间,值越大联系越紧密。
    • v(G):循环复杂度,可以理解为穷尽程序流程每一条路径所需要的试验次数。OCavg为平均循环复杂度;WMC为总循环复杂度。
  • Dependency Metrics
    • Cyclic:指和类直接或间接相互依赖的类的数量。这样的相互依赖可能导致代码难以理解和测试。
    • Dcy和Dcy*:计算了该类直接依赖的类的数量,带*表示包括了间接依赖的类。
    • Dpt和Dpt*计算了直接依赖该类的类的数量,带*表示包括了间接依赖的类。

 第一次作业

  这次作业比较简单,只涉及了正则表达式处理和简单数据结构和算法,类图和流程图如下:

             

  • 结构分析——复杂度分析

       

  在选择复杂度分析(Complexity Metrics)后,可以得到图示结果。在左图中,我对Poly类中的CheckAndConstructPoly方法和Simplify方法被判定为“病态结构”,与其他方法的紧密程度也过高。这是因为我在拆分表达式时,使用了正则表达式的方法,第一项进行特殊处理之后用while循环的形式拆解其余项,并存入系数、次方ArrayList<BigInteger>中。循环导致了程序的复杂度提升。并且,我使用的是边判断边保存的方式,导致代码行数过长,过于复杂。同样,在Simplify方法中,我使用了两层for循环来合并同类项,之后还有一层for循环将正数项化为第一项。这几个循环结构为Simplify方法带来了极高的复杂度。

  在回顾了自己这两段极长的代码后,我认为存在几点可以改进的。首先是对CheckAndConstructPoly方法的改进,因为我对于此方法的使用是用作了“构造函数”使用的,那么我就不应该在其中穿插与“构造表达式”完全无关的内容。我应该再建立一个Format类,储存检出是否合法的方法。对于输入,我先判断输入是否合法,之后再进行操作。先判断合法的另外一个好处就是可以把表达式“标准化”(即把多个加减化成一个),然后用加号去split整个表达式为单独的项,比用group方法要简单。我在第二次作业中用的就是这个方法。在第二次讨论课中,有同学对“正则表达式是否是面向对象”这件事情提出了质疑,因而助教给出了Parser方法来判断合法性。之后改进Simplify方法,我认为应当把这个很长的方法拆成两个,一个合并同类项,另一个提前正数项。或者,可以像我第二次作业时做的那样,对Arraylist override一个add方法,每次加入list的时候就检查是否已有重复项。

  • 结构分析——依赖度分析

  emm我的IDEA的metric插件分析dependency metric会报错,我先解决软件的问题,再把这块补上。

第二次作业

  这次作业相比于第一次作业来讲有些复杂,因为其引用了乘法,增加了类间关系的复杂度。我的代码结构基本借鉴了第一次的方法,先拆分表达式化为项,将因子保存到自定义项类中,最后全部存入仓库。然后对仓库进行求导即可。这次作业中我采用了较为简单的三角函数化简(sin(x)^2+cos(x)^2=1和两个变种)方式。我没有考虑指数为负数时要不要化简,因为我认为有时负数指数会把式子化的更长。我认为这次的难点是在于对格式的合法性判定,因为有许多空格情况、省略情况,这块有些难理解。

  • 结构分析——复杂度分析

             

     

  在左图中,我的GetFactorType函数被指明是“病态结构”,这是因为我在规范化表达式后,使用了正则表达式的方法判断四种“因子”形式(const,x,sin(x),cos(x)),导致if-else结构过于长。不得不说,这块也成为了我debug的核心区域。而图中还被判定依赖关系过于复杂的PrintExpression函数,是由于我采用了“for循环”的方式完整输出结果,有大量if-else结构。这块也是我debug核心区域,因为很容易多输出*或者压根不输出*。右图中被判定为复杂度都是我的化简类重点方法,我的化简方法很白痴,都是不停的for循环来查找同类项合并,但是也就极为冗长。

  以全局的角度考察这次代码,我发现自己又犯了“构造函数里面一堆无关内容”的错误,可见我面向对象思想还不明确。在这次作业中,我把整个项看成是一个key,因为因子无非只有那么几种存储方式而已。下图可以示意我的项的结构设置:

 

      

这样做的话我认为可以大大简化这次作业的架构,但是有很大缺点。比如,这非常不符合面向对象思想,并且拓展性极差。还有一点最为致命的是,我在疯狂的get、set函数中给自己带来了浅拷贝的风险!虽然BigInteger类型不至于担心,但是第三次作业中我自定义类型就面临了许多困难,debug到绝望。为了这个架构,我在add时必须对整个仓库循环遍历,因而导致我的程序充满了for循环,OCavg疯狂红色。

  • 结构分析——依赖度分析

第三次作业

  由于我尝试着在第二次作业的基础上写这次嵌套求导的作业,导致我的结构极为复杂!我认为这次我写的很不好,不光是WF判断没有考虑周全,另外优化也没有写,打印输出的时候还会多打印一堆括号和其它无关内容。这次写代码的经验告诉我——该重写就重写!我学习了其他同学使用接口写的的代码,虽然他们是边求导边输出,但能保证括号之中的内容输出时是“原封不动”的(也就是使用String存储)。而且经过划分Cos、Sin、X、Cons几个类后,明显代码结构清晰了许多。

  下图是令人窒息的流程图和我的核心结构:

  • 结构分析——复杂度分析

   其实如果不用判断合法性的话,我的结构还不算是过于疯狂,但是加上合法性判断后,我必须在构造求导仓库之前提前判断合法性,这导致了我基本上又是重新写了一套代码。

            

            

  和上次作业基本一样,我的Print方法、GetType方法和add方法都被化为“病态结构”,因为其充满了for循环和if-else。而且尤其是Print方法,根据我“递归存储”的结构,我的Print也是递归打印的,导致括号输出极多无比,然后还有一堆1*(),output长度不可思议地长。

         

如果使用接口的方法来实现这次作业,我起码不用设置那么多的ArrayList,因为我可以直接存储Object类型。Key之间的关系为加法,Key内部的关系为乘法。

  • 结构分析——依赖度分析

分析Bug


   经过三次作业后,我已经大概能够“感觉到”会出bug的位置了。第一个是正则表达式,第二个是大型if-else嵌套,第三个是大型if条件,第四个是charAt。在课下测试和互测中,我都发现了自己程序的这些方面的Bug。

第一次作业

  第一次作业中我在互测时被找出了空串错误。由于我在CheckFormat中没有判断全空格问题,导致消除所有空格时会导致串为空,然后没有任何输出。之后我补加了后面对于空串的判断代码之后解决了这个问题。我认为空白字符位置判断有些难,因为情况很多、很复杂。在之后的作业中,我把多个空格和制表符都化简为一个空格,简化了判断情况。

第二次作业

  第二次作业中我在if判断条件中进行错误。第一个条件我没有加入“&& s.chaAt(0) != ' '”,导致只要第一个不是符号,就会添加符号,然后表达式就会在第一项前加入一个符号,导致在接下来的运算中产生误判断WF错误。在大型if条件判断中,由于语句顺序问题,会导致错误。

第三次作业

  第三次作业中一开始我的错误都来源于这种对于()嵌套判断方法。这样会导致(x)+sin(x)^6判断错误。在强测中我错了两个数据点,一个是由于GetType方法中我放过了空串,导致最后一个符号是乘号的时候我都认为表达式是对的。这个错误的根本原因是if-else没有else。还有一个错误是由于判断因子的时候没考虑第一项单独判断,导致认为--sin(x)都是对的。这个是由于正则的原因。于是我使用了栈的方法对括号进行判断,之后提取里面的String继续判断。但是这样十分复杂。看过课上助教说的parsor方法,我觉得很好,符合面向对象的思维也容易理解。

从分类树角度分析程序在设计上的问题

  分类树方法是由Grochtmann和Grimm在1993年提出的,是在软件功能测试方面一种有效的测试方法,通过分类树把测试对象的整个输入域分割成独立的类,方便设局出有深度、范围广的测试数据。按照分类树方法,测试对象的输入域被认为是由各种不同的方面组成并且都与测试相关。对于每个方面,分离和组成各种类别,而分类结果的各类又可能再进一步地被分类。这种通过对输入域进行层梯式的分类表现为树状结构。随后,通过组合各种不同分类的结果来形成测试用例。使用分类树方法,对于测试人员来说最重要的信息来源是测试对象的功能规格说明书。使用分类树方法的一个重要的好处是:它把测试用例设计转变成一个组合若干结构化和系统化的测试对象组成部分的过程 - 使其容易把握,易于理解,当然也易于文档化。

  分类树方法的基本原理是:首先把测试对象的可能输入按照不同的分类方式进行分类,每一种分类要考虑的是测试对象的不同的方面。然后把各种分开的输入组合在一起产生不冗余的测试用例,同时又能覆盖测试对象的整个输入域。

  因此,可以把使用分类树方法设计测试用例的过程分为3大步骤:

  1、 识别出测试对象并分析输入空间。

  2、 对测试对象的输入空间进行分类。

  3、 画出分类树、组合成测试用例。

参考资料:http://www.51testing.com/html/79/n-220479.html

  虽然我之前并不知道这种测试方法,但是我是依照这种思路设计的测试集。我认为分类树的方法是基于功能的测试,也属于黑箱测试;如果是按照自己的代码进行测试(对if-else,while等),那这是白箱测试。两种测试都应该进行。

 

Bug寻找策略


 测试策略

  第一次作业的互测环节中,我先是阅读了别人的代码,之后针对他们的逻辑错误(比如if-else语句没写全)写测试样例。我还发现“大正则”很容易爆栈。但是第二次、尤其是第三次测试,这样就不太具有可行性了。于是我先用分类树的方法几乎枚举了所有WF形式,找出了一两个错误。之后我将写好的各种因子进行嵌套组合,对程序进行测试。在第二次研讨课时,有同学讲了自己自动化测试的方法,我认为这样更加高效,我会进行学习。

  根据代码结构写测试自然很有效,但是会花费大量的时间。且我认为在实际工程中,没有人会去进行纯“白箱测试”,因而“黑箱测试”更加合理。

Applying Creational Pattern


    根据维基百科的定义和我自己的理解,我认为Creational pattern是指一种将设计目标进行划分,并对划分的对象进行生成、组织、使用的设计模式。

  设计模式的六大原则

    1.开闭原则:对扩展开放,对修改关闭

    2.里氏转换原则:子类的能力、返回值、抛出异常的情况必须大于等于父类

    3.依赖倒转原则:面向接口编程,依赖于抽象而不依赖于具体

    4.接口隔离原则:使用多个隔离的接口,比使用单个接口要好

    5.最少知道原则:一个实体应当尽量少的与其他实体之间发生相互作用,使得系统功能模块相对独立

    6.合成复用原则:原则是尽量使用合成/聚合的方式,而不是使用继承

  五大创建模式

  在阅读了几个创建模式后,我觉得工厂模式是最适合这次代码的。所以在此我先对工厂模式进行详细描述。

    1.工厂方法模式

  普通工厂模式就是建立一个工厂类,对实现了同一接口的一些类进行实例的创建。下面是我找到的普通工厂模式示例代码,我觉得写得很清晰。

      

 1 package com.mode.create;
 2  
 3 public interface MyInterface {
 4     public void print();
 5 }
 6 
 7 package com.mode.create;
 8  
 9 public class MyClassOne implements MyInterface {
10  
11     @Override
12     public void print() {
13         System.out.println("MyClassOne");
14     }
15  
16 }
17 
18 package com.mode.create;
19  
20 public class MyClassTwo implements MyInterface {
21  
22     @Override
23     public void print() {
24         System.out.println("MyClassTwo");
25     }
26  
27 }
28 
29 package com.mode.create;
30  
31 public class MyFactory {
32  
33     public MyInterface produce(String type) {  
34         if ("One".equals(type)) {  
35             return new MyClassOne();  
36         } else if ("Two".equals(type)) {  
37             return new MyClassTwo();  
38         } else {  
39             System.out.println("没有要找的类型");  
40             return null;  
41         }  
42     }
43  
44 }
45 
46 package com.mode.create;
47  
48 public class FactoryTest {
49  
50     public static void main(String[] args){
51         MyFactory factory = new MyFactory();  
52         MyInterface myi = factory.produce("One");  
53         myi.print();
54     }
55  
56 }

  第三次作业的求导中可以使用这个方法。先创建一个求导接口,之后针对每个类型的因子创建接口class,implement求导接口。当然在创建表达式时也可以使用这个方法,我的初步方法是创建两个方法,一个getType一个getFactor。这两个方法作为工厂判定表达式类型,创建因子时可以使用。

  第二次作业也可以使用这个方法。

public static Derivation getFactor(String type,String str){
        if (type.equals("CONS")) {
            return new CONS(sttr);
        }
        // 其他省略不写
}    

public static String getTyper(String str){
        if (str.matches("[+-]?\\d+")) {
            return "CONS";
        }
        // 其他省略不写
}    

    2.抽象工厂模式

  工厂方法模式有一个问题就是,类的创建依赖工厂类,也就是说,如果想要拓展程序,必须对工厂类进行修改,这违背了闭包原则。创建多个工厂类,这样一旦需要增加新的功能,直接增加新的工厂类就可以了,不需要修改之前的代码。

    3.单例模式

  会引发多线程问题。(这块我没太理解,在多线程学习之前我会再去学习)

    4.创造者模式

  是将一个复杂的对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。

    5.原型模式

  该模式的思想就是将一个对象作为原型,对其进行复制、克隆,产生一个和原对象类似的新对象。

总结


  再次查看这三次作业的代码,我感觉自己第一二次作业的时候思路还算是清晰的。第三次作业由于方法使用不当,浪费了许多时间,非常可惜。相信我以后会吸取这一次作业的教训,先想清楚了再写代码。

  感谢几位同学在讨论课的分享。尤其是pmxm同学分享的“先构造测试集再写代码”,经过亲身经历之后我对这句话很认同。在感谢dyj同学分享的自动化测试方法,令我耳目一新,我会去尝试编写评测机的~

 

 

猜你喜欢

转载自www.cnblogs.com/Wendy-Zheng/p/10585708.html