OO第一单元总结博客

OO第一单元作业总结

        本单元的三次作业内容为多项式求导。第一次作业中,表达式支持因子形式为带符号整数,以x为底数的幂函数。第二次作业中,表达式因子形式在第一次作业的基础上,支持标准三角函数(sin(x)、cos(x))为底数的幂函数。第三次作业中,表达式因子进一步支持三角函数嵌套形式以及用小括号包装的表达式。

一、程序结构分析

1.1 三次作业类的规模分析

(一)

第一次作业中,我总共运用两个类完成任务,各因子属于Poly类,表达式属于Polynomial类。

(1)

Poly类代码35行。Poly类中,属性in、ex分别代表因子的系数与指数。类中四个方法,构造方法两行,calculate方法十行,index、exp方法各一行。calculate方法中,有两个控制分支,用于区分指数为0或非0时的求导方法。

class Poly {
    private BigDecimal in;     //系数
    private BigDecimal ex;    //指数
    
    public Poly(BigDecimal n,BigDecimal m) {}    //构造方法
    public Poly calculate() {}                   //因子求导
    public BigDecimal index() {}                 //提取系数
    public BigDecimal exp() {}                   //提取指数
    }
(2)

Polynomial类中代码135行,两个属性 indexList、expList分别为表达式项的系数列表与指数列表。类中4个方法。initNew方法用来处理输出,初始化表达式,该方法56行,总共7个分支用于解析不同输入格式的表达式中的项以及判定非法输入。me方法5行,对读入表达式进行合并同类项,进行化简。derive方法48行对表达式中的项进行求导,最终输出结果。main函数45行,对console中字符串预处理,化为理想形式,初步判断输入字符串是否合法。

public class Polynomial {
    private static LinkedList<BigDecimal> indexList = new LinkedList<>();//项的系数列表
    private static LinkedList<BigDecimal> expList = new LinkedList<>();  //项的指数列表

    private static void initNew(String line0) {}             //拆分字符串,初始化表达式
    private static void me(BigDecimal[] exp, BigDecimal[] index, int[] mark) {}// 化简
    private static void derive(BigDecimal[][] poly, Poly[] element) {} //求导函数
    public static void main(String[] args) {}                          
}
(3)第一次作业OO类图以及性能度量

 ①类图

 ②性能度量

可以看出,在Polynomial类中derive方法,读入字符串初始化表达式的initNew方法耦合度高,且圈复杂度高,维护难度较大。

(二)

第二次作业中,我总共运用了三个类。Factor类记录因子,Term类记录由乘号将因子链接起来组成的项,Polynomial类记录项由加减号链接起来组成的表达式。

(1)

Factor类有两个属性,co为指数,facType为底数类型。该类110行。

两种构造方法分别针对不同构造需求设计,构造方法1为初始化构造,2行,构造方法2服务后续化简合并同类项操作,7行。getConstant、facDiff、printFactor方法均为单项因子求导相关函数分别占5行,33行、8行,分别具有2个、4个、2个条件分支。same、addCo、resetCo、equal、subtract方法服务后续化简,均占1行。

class Factor {
    private BigInteger co;          //指数
    private String facType;         //类型

    public Factor(BigInteger co,String str) {}     //构造方法1,用输入拆分构造
    public Factor(Factor b) {}                     //构造方法2,用factor赋值构造
    public BigInteger getConstant() {}             //求导时提取指数
    public void facDiff(ArrayList<Factor> part) {} //单因子求导,将求导结果添加到ArrayList中
    public String printFactor() {}                 //得到单因子的导数
    public boolean same(Factor b) {}               //比较两因子类型相同
    public BigInteger addCo(Factor b) {}           //两个类型相同的因子指数相加
    public void resetCo(BigInteger newInt) {}      //重置因子指数
    public boolean equal(Factor b) {}              //判断两个因子完全相同
    public Factor subtract(Factor b) {}           //因子类型相同,指数相减,得到新的因子
}

(2)

Term类有两个属性,con表示项中常数因子,facList是项的因子序列。

Term类总共400行,其中Term()为构造方法,termDifferent()、printTerm()方法分别用来单项求导以及最后输出渠道结果的方法。其余方法都是用来化简的方法。

经过分析OO性能,可看出构造方法的基本复杂度、耦合度、圈复杂度都较高,主要因为构造项时直接将字符串传入构造方法,没有构造parser类对字符串预处理。printTerm() 方法耦合度较高,圈复杂度大主要是在输出求导结果时,为尽量减少输出的符号因为存在很多循环,条件分支较多。canCom(),br(),canSub(),canSubNew()几个方法都是用来化简表达式的,主要思路是对于包含多个项的表达式中的两个项,判断是否有利用三角函数的性质化简的空间,如果有,则对原表达式进行更改。在这几个方法中,用的编程手段比较原始,故复杂度较高,优化空间其实很大。

(3)

Polynomial 类有三个私有属性,orTerms保存读入表达式, diff保存初步求导表达式结果,last保存对初步求导结果化简后得到的表达式。Polynomial 类总共260行。

可以看出,用于找到表达式中第一个系数大于0的项便于化简的方法基本复杂度高,非结构化程度高。求导后,合并同类项的方法combineDIff耦合度高。

(4) 第二次作业类图

                 

(三)

第三次作业的总体思路是递归“剥壳”,将表达式用树结构表示,叶子结点是无嵌套、加减、乘法包装的原子因子。在第三次作业中,我总共用了6个类,一个接口。Nest类、Add类、Mul类均继承Combine类,并继承CombineElement借口。Factor类代表常数,x的幂函数,sin(x)的幂函数、cos(x)的幂函数四种原子因子。Combine 类汇总了表达式中出现的嵌套、加减法、乘法运算中,递归分离孩子、判断包装类型、判断合法性等方法。

(1)

Combine类有四个属性,Combine类left表示左孩子,express属性表示该结点的表达式,combineType表示该结点的包装类(嵌套or加减or乘),brank熟悉表示该结点是否被括号包裹。Combine类270行。

 

Combine类构造方法耦合度高,圈复杂度高,主要原因是未对字符串进行处理,直接被我粗暴地投入构造方法中,在构造过程中分支极多,多出输出WF,退出程序,可以说这个操作很不优美。defineType方法用来判断结点包装类型,得到结点combineType,为进一步将结点分配给Add类、Mul类、Nest类做准备。在该方法中,多次使用循环增加了复杂度。

(2)

Add类主要负责被加减法包装的结点。除Combine类的属性外,还有right属性代表Combine类的右孩子、Addtype属性代表加法还是减法。Add类90行。

根据Add类OO性能表可以看出,用于分离左右孩子的方法getChild()基本复杂度较高,圈复杂度较高,主要原因可能是求左右孩子的过程中调用StringBuilder,拼接字符串,增加了复杂度。

(3)

Mul类主要负责被乘号包装的结点。除父类的属性外,还有Combine类的right代表右孩子。该类72行。

(4)

Nest类主要负责嵌套包装的结点。由于嵌套类支持指数,该类除继承父类属性外,还有coeff属性代表指数项,nestType属性表示嵌套类型(sin或者cos)。该类108行。

(5)

Factor类为叶子结点,原子因子,是递归结构的最内层。具有三个属性ex为指数,facType为叶子结点类型(有常数、x、sin(x)、cos(x))四种,sym代表系数为1还是-1,该类112行。

观察OO性能列表,发现求原子因子导数的方法facDiff基本复杂度高,圈复杂度高,主要因为在该方法中,我采用了switch根据facType条件选择,在没有个case中,又需要根据sym属性区别返回的导数类型,分支极多。其实比较系统化、工程化的处理方式应该是对常数、x、sin(x)、cos(x)单独建立类,它们继承叶子结点类,分别具有求导方法。

(6)

     Main类对输入进行预处理,利用递归构造树,求导输出。该类160行。

(7)第三次作业类图

1.2 基于三次作业类规模分析的总结

分析三次作业的OO性能图,发现自己第一单元写代码时存在如下问题:

    1. 构造方法中习惯将原始字符串直接作为输入,在构造方法中判断WF。这个操作导致三次作业,多个类中我的构造方法性能很差,复杂度极高。改善思路:可以考虑研讨课中助教建议的构建parser类,先对字符串进行处理,得到合理形式后,构造方法应该简洁明了。
    2. 方法中条件分支多,尤其是在后两次作业,因子中出现三角函数形式,我的处理方式都是switchcase,其实正如我上面在分析部分所说,完全可以建立子类。
    3. 第二次作业化简相关的方法以及第三次作业的判断结点包装类型、剥离结点左右孩子的方法中,运用循环较多,并且同一个方法内,结构相近的循环多次使用,造成很大重复,可以考虑对于循环搜索单独构造方法,在其他方法中进行调用,优化代码结构。

 这三次作业中的进步:

    1. 观察类图,发现自己最明显的进步在于前两次不会使用继承、接口,第三次作业中运用了面向对象编程这两大利器,让代码整体架构更清晰。
    2. 作业难度逐次增加,我在每一次作业中都是在前一次作业的思路基础上,改进代码,应对复杂的要求,并非彻底推翻重构。

总的来说,我这三次作业的代码还很“面向过程”,在对比了同学优秀代码后,我发现自己的代码设计中许多操作仍然很“幼稚”,希望在接下来的几个单元中代码风格会逐渐成熟起来。

二、程序bug分析

2.1 第一次作业bug分析

第一次作业初次上手,最大的困难是在处理输入。

刚开始思路不清晰,没有想到对输入字符串进行去空格等预处理,对于matcher的group方法不熟悉,导致我第一版代码在init方法中罗列将近10中正则表达式,出现了大端条件分支,判断表达式是否合法,分割每一项。第一版代码在本地测试中不堪一击,漏洞百出,主要原因是穷举法根本不可能遍历每一种合法的形式与非法形式,一旦出差错,轻则输出结果错误,重则程序报错。

重构第二版代码时,考虑到对输入字符串进行预处理,将清爽的字符串透给init方法。

private static void initNew(String line0) {
        String line = line0;
        Pattern pattern = Pattern.compile("^([+-])([+-])?(\\d+\\*)?(x)" +
                "(\\^[+-]?\\d+)?|^([+-])([+-]?\\d+)");
        Matcher match = pattern.matcher(line);
        while (match.find()) {
           ...
            line = match.replaceFirst("");
            match = pattern.matcher(line);
        }
        if (line.length() != 0) {
            System.out.print("WRONG FORMAT!");
            System.exit(0);
        }
    }

第一次作业在强测中性能分吃亏,但是没有错强测点以及互测点。性能分吃亏是因为没有注意性能要求是输出长度,所以没有把第一项挪成正系数项。

2.2 第二次作业bug分析

有了第一次作业处理输入的思路,第二次作业相对得心应手。

第二次的重点是优化(不优化真的不会有问题),丢人的讲,运用十分原始的手段合并项的时候主要干的事情是讲表达式的TermList传给方法,在Term类的方法中对比TermLIst中的成员与从TermLIst的下标参数中传过来的另一个成员比较是否除需要合并额外处理的因子不同外,其余因子都相同。在这个过程中,我犯了两个错误,第一个错误是我在双重循环中直接对TermLIst进行删除,增加元素,收到了Idea的警告。第二个错误,也是后来我被hack很惨的错误,是我只单方向判断了一个Term的因子另一个Term全包含,忘记反过来判断另一个Term的因子是否也被本Term全包含,就类似于充要条件被我弄成了充分条件。

第二次作业被hack的另一个点是输出格式问题。第二次作业中,我力求优化,对于输出*1这种没有考虑清楚,结果输出了+*这种不伦不类的东西。

第二次作业成也优化,败也优化。优化毕竟是更高的追求,要做好必然是要更细心的。

2.3 第三次作业bug分析

第三次作业相对复杂一些,在我构造递归结构的时候,每一个类输入形式都是很理想化的,这就需要在构造数的每一层级时想清楚自己当下的代码需要满足的条件,这个条件是否需要其他类中处理满足。显然,这一点没有做好,我前后思路脱节,导致后续自己本地测出各种bug,到最后也没用修复完全。

比如,我的处理中,最开始原子类没有支持-x,-sin(x)这种形式,导致我提交之前猛然发现我-x导求出来都是错的。又比如:在对(-(表达式))这个结构处理时,我是将-变成了(-1*(表达式)),同时我预处理的时候要把两个连续的正负号合并成一个符号。然而悲惨的我没有注意到两个处理应该注意先后顺序,导致我最后在强测点有一个输出了WF,因为没有识别出(++sin(x))。

加减法包装的类最开始我很轻视,认为是最简单的,实际上在后来测试中,发现加减法类考虑不完善,后患无穷。最严重的错误莫过于没有想到*+\\d以及^+\\d这种形式,直接导致combine类判断类型出错以及放到Add类中分离孩子出错。在减法求导时,要不要给右孩子的导数整体加括号又是门学问,比如x-x+x显然不同于x-(x+x)。最开始对Add类的轻视让我在后期debug时苦不堪言。不幸的是,由于我的粗心,我最后提交的时候并没有改全,还是被hack了。

三、狼人策略

第一次互测,人生地不熟的我很佛系的随便试了试数据,希望找到求导求错的,但是本组中并没有人把我设的弱弱的导求错。最后我只是提交了一个自己最后才想到的WF的坑,x^+ 2,果然砍了2个人。

第二次互测中,由于第一次没有被砍,我还是不知道怎么找错,最后找出来的错仍然是局限于没有判断出WF。第二出互测中,我集中观察了像我一样预处理的同学的预处理代码,发现了一些没有注意的空格以及加减号合并的问题,找到了一些可乘之机。

第二次作业被砍得很惨,发现自己代码中遍及WF,求导求错,输出格式错误的问题。第三次作业互测中,我牢记自己第二次被砍出的错误,试图挖掘同屋中输出格式错误,算错这样的错误。尝试了长输入,大致没有发现问题后,我转向短小复杂的输入形式,我相信第三次作业难就难在解析表达式的细节处理。果然,最后我构造的-(sin((-x))-((x)+(x^+2))),-(+-(2*x)),sin(-12)这样的短式子没有砍空。另外,我观察了一下同屋同学包装类型的处理,遗憾的发现,他们的这些类的构造方法中,对于输入的要求并没有我自己的设计中那样苛刻,所以本地测试错误的点,用在他们身上都失灵了。

四、Applying Creational Pattern

这三次作业,虽然我逐渐尝试更加“面向对象”一些,但是自己的代码仍然十分“面向过程”。对于接口,继承,只在第三次作业中尝试使用,但也正如上文所说,很多完全可以用继承优化实现的地方,我仍然很“质朴”地罗列着case。

另一方面,构造方法被我粗暴地投喂原始字符串也不是明智之举,应该考虑构造字符串处理类,把字符串加工出厂后再投喂给构造方法。

 

 

猜你喜欢

转载自www.cnblogs.com/jessyswing/p/10589300.html
今日推荐