OO_Unit1_万物皆对象

第一次作业——PO?OO?

第一次作业的题目是简单幂函数的多项式求导,形如-1 + x ^ 233 - x ^ 06

根据第一次作业的类图,可见第一次作业的框架非常简单,也非常面向过程。读完第一次作业的指导书之后,便很自然的定义了两个类,一个类是Poly,即多项式类,另一个类是PolyCal,即负责多项式计算的类,main函数也在里头。(这一点也不面向对象)

Poly中,利用一个HashMap构造多项式,其中每一个元素对应多项式的一个项。同时,在该类中还构造了一些列方法,如add(加上一个项),sub(减去一个项),isConstant(判断是否是常数),differentiate(求导)以及show(格式化输出)。

PolyCal中,构造了一些WRONG FORMAT的方法,例如emtpyError(空输入),intError(整数格式错误),degreeError(系数格式错误)以及itemError(项格式错误)。当时的思路是,当判断完空输入和整数格式错误之后,便可以将字符串中所有的空格(space&\t)去除,方便之后的格式化处理。判断完各种WRONG FORMAT之后,利用getOp方法获得运算符号(用项分割得到运算符),通过getPoly方法得到表达式(用运算符分割得到各项),然后运用Poly.differentiate求导,Poly.show输出结果。

可见,表达式的处理方式十分繁琐,用项分割得到运算符,用运算符分割得到项的方法也十分愚蠢,但是由于没有大正则匹配,所以没有出来爆栈的问题。同时,整体的层次也比较混乱,没有运用面向对象的思想,这也注定了第二次作业的重构。

在第一次作业中,虽然过了强测,但是还是在互测中被hack出现了一些bug。其中有第一次作业的大众bug——\f\v;还有自己在码代码中因疏忽判断符号比项多的情况导致的bug,如下所示:

if (opArray.size() > coeffArray.size()) {
    System.out.println("WRONG FORMAT!\n(new bug)");
    System.exit(0);
}

通过面向bug编程,在五行内修复了这两个bug,但是能预感到在程序中还存在这不少潜在的bug,字符串解析构造是bug的重灾区。

从Metrics中可以看出,getOpgetPoly方法的模块设计复杂度高,意味着模块耦合度高,难以维护和复用。用多项式作为最小基本单元实属鼠目寸光。

ev(G)基本复杂度是用来衡量程序非结构化程度的,非结构成分降低了程序的质量,增加了代码的维护难度,使程序难于理解。因此,基本复杂度高意味着非结构化程度高,难以模块化和维护。实际上,消除了一个错误有时会引起其他的错误。

  Iv(G)模块设计复杂度是用来衡量模块判定结构,即模块和其他模块的调用关系。软件模块设计复杂度高意味模块耦合度高,这将导致模块难于隔离、维护和复用。模块设计复杂度是从模块流程图中移去那些不包含调用子模块的判定和循环结构后得出的圈复杂度,因此模块设计复杂度不能大于圈复杂度,通常是远小于圈复杂度。

  v(G)是用来衡量一个模块判定结构的复杂程度,数量上表现为独立路径的条数,即合理的预防错误所需测试的最少路径条数,圈复杂度大说明程序代码可能质量低且难于测试和维护,经验表明,程序的可能错误和高的圈复杂度有着很大关系。

 

 在测试别人的程序时,我通常先采用边界值特殊值的方法对一些可能出现的错误进行群体攻击,之后再阅读每个人的代码,针对每个人代码的薄弱环节进行测试。事实证明,这样的手段非常低效,远不如所谓的对拍自动化评测。但是通过阅读别人写的代码,可以学习到一些不一样的思维方式和编码习惯,别人写得精彩的地方可以借鉴,别人写得不好的地方也可以加以警醒。总之,取其精华,取其糟粕,阅读别人的代码也是一种难得的学习过程(想必这也是OO课程组的初心吧)。

第二次作业——更OO一点

第一次作业中一点也不OO的设计方法注定了第二次作业的重构。通过课上纪一鹏老师对抽象化结构层次的讲解,这次的设计就显得更OO一点了。第一次作业的代码零复用率让我在这次作业的设计中多花了点心思——如何实现更强的可扩展性?

本次作业,需要完成的任务为包含简单幂函数和简单正余弦函数的导函数的求解,形如-1 + x ^ 233 * x ^ 06 - sin(x) * 3 * sin(x)

在这次的设计中,我决定通过对象化编程让每个类自扫门前雪,实现高内聚低耦合,提高代码的可维护性和可扩展性。在这次的作业中,我将多项式的最小单元抽象为项为第三次作业的重构埋下伏笔。每一个多项式都可以由若干项组成,每个项都抽象为包含四个基本属性的对象,这四个基本属性分别是常数因子幂函数指数正弦函数指数以及余弦函数指数。通过这四个基本属性便可以唯一确定一个项。在Item类中定义了项的求导法则,即乘法求导法则

每个项求导可以得到三个项,用一个ArrayList将他们保存起来,并作为方法的返回值。

Poly类中,构造了多项式的求导法则,即各项求导再通过运算符连接起来。而Poly构造方法与第一次作业相似,通过分割得到各项,此处不再赘述。

在最顶层的main类中,我还定义了一个静态的validCheck方法,在将字符串送入到Poly的构造方法之前,先检查多项式格式的合法性。这里我运用了大正则的匹配方法:

    String signRegEx = "(( *)[+-]( *))";
    String constRegEx = "(( *)[+-]?\\d+)( *)";
    String powRegEx = "(( *)x( *)(\\^( *)([+-]?\\d+))?+)( *)";
    String sinRegEx = "(( *)sin( *)\\(( *)x( *)\\)( *)" +
                "(\\^( *)[+-]?\\d+)?( *))";
    String cosRegEx = "(( *)cos( *)\\(( *)x( *)\\)( *)" +
                "(\\^( *)[+-]?\\d+)?( *))";
    String str1 = str.replaceAll("\\t", " ");
    String factorRegEx = String.format("(%s|%s|%s|%s)",
                constRegEx, powRegEx, sinRegEx, cosRegEx);
    String itemRegEx = String.format("(%s(\\*%s)*)",
                factorRegEx, factorRegEx);
    String validRegEx = signRegEx + "[+-]?" + itemRegEx;

 通过判断前一次匹配的end(尾)是否是这一次匹配的start(头)的方法,实现输入格式的检查。这样的匹配方法简单且有效。

但是将这个方法写在main中无疑增加了该类的非结构化程度,应该将它与Poly的构造方法结合起来,在validCheck的同时就可以匹配出各个项,而不用在Poly中再次重复实现项的提取。这一点将在第三次作业得到修正。

在第二次作业的优化中,暴露出了一些非常愚蠢的bug,使得正确性也产生了一些问题。优化有风险,优化需谨慎。归根结底,还是因为优化的时候采用了错误的方法以及没有测试完全才导致一些潜在的bug。所以若不考虑周全,优化的过程就是一个制造新bug的过程。这次血淋淋的惨案教训我,准确性是优化的前提,当无法保证准确性时,任何优化都是不可取的!

第三次作业——继承!

第三次作业实在第二次作业的基础上实现复合函数求导,形如 (-1 + x ^ 233)* sin(x^2) ^ 06 - cos(sin(x)) * 3 * sin((x))

面对第三次作业,我的内心出现了一点小波动:沿用第二次作业的设计框架,还是参考教程中通过构建树进行链式求导的方法。由于之前没有运用过链式求导的方法,所以最终还是选择了基于现有的设计模式。

由于表达式因子的存在,表达式可以是表达式,也可以是因子,这使得我必须在一个低层次(Factor)的类中调用一个高层次的类(Poly)。这一点令我担心嵌套会出现问题。

在第二次作业的层次基础(挖的坑)上,我又在抽象出了更底层的Factor类,其中定义了derivative求导方法和toString方法。Factor类作为父类(其实是一个抽象类),其下构造PowFactor(幂函数因子)、sinFactor(正弦函数因子)、cosFactor(余弦函数因子)以及Poly(多项式因子)四个子类继承Factor,并在子类中实现derivativetoString方法。通过继承关系,便可以在Item类中通过一个ArrayList<Factor>对因子进行统一管理。

在进行求导的时候,实例化的对象poly调用其求导法则,根据item的求导法则,对每个item求导,item再调用每个factor的求导法则对每个factor求导,factor中可能含有poly,即多项式因子,所以继续调用poly的求导法则,实现递归调用。

值得注意的是,在构造各个类的derivative方法时,应该返回统一的变量类型,例如String或者StringBuilder,以便后期合并。同时,每次求导之后应该返回一个新的变量,而不是再原有的变量上进行修改,避免发生不必要的错误!

这样的设计框架思路清晰、容易实现,但是这样的嵌套调用使得各个类之间层层相扣,无疑增加了类间的耦合度以及方法的复杂度。(从Metrics中可以看出,由于PolyItem被反复嵌套使用,复杂程度高)

总结

通过这三次作业的练习,我渐渐理解和掌握了的面向对象编程的基本思想。从面向过程到面向对象无疑是思维方式的一次巨大转变。

面向过程就是分析出解决问题所需要的步骤,然后用函数把这些步骤一步一步实现,使用的时候一个一个依次调用就可以了;面向对象是把构成问题事务分解成各个对象,建立对象的目的不是为了完成一个步骤,而是为了描叙某个事物在整个解决问题的步骤中的行为。

继承、接口和多态是面向对象的重要组成部分。

继承可以提高代码的复用性,使类与类之间产生is-a的层次关系,是多态的前提。但是不要为了继承部分功能而去使用继承,滥用继承只会降低程序的鲁棒性,使层次结构紊乱。

接口是抽象方法和常量值的集合。从本质上讲,接口是一种特殊的抽象类,这种抽象类只包含常量和方法的定义,而没有变量和方法的实现。接口不能被实例化,且一个类如果实现了接口,要么是抽象类,要么实现接口中的所有方法。

多态指对象在不同时刻表现出来的不同状态。多态的前提是有继承或实现关系,有方法的重写,有父类引用指向子类的对象。多态的存在提高了程序的扩展性和后期的可维护性。

擅用继承、接口和多态可以挺高代码的利用率,避免重复造轮子,同时也可以提高代码的层次感和鲁棒性。

以上是这三次OO作业的一些心得体会,若有笔误,欢迎纠正。同时也欢迎各位读者在评论区留言,一起探讨和学习。

posted @ 2019-03-27 15:43  jay_w  阅读(180)  评论(0)    收藏  举报