面向对象设计与构造第一单元总结

面向对象设计与构造第一单元总结

作业结构与设计分析

第一次作业

  • 主要类图

  • 主要类分析

    第一次作业总体结构比较简单,不涉及嵌套,总共只有3个类

    • Poly

      表示一个多项式,由多个项(Term)相加构成。由于项的个数不固定,使用HashMap容器存放各个项。类的主要的方法有:

      • add:添加一个项
      • derive:求导,通过各个项自身求导在相加实现
      • toString:重写toString方法,将多项式转化为字符串
      • Poly:构造方法,传入一个字符串,做简单的化简,然后根据 '+' '-' 号分离出项,生成多个Term对象
    • Term

      表示一个项,由系数coef和指数degree表达。该类的主要方法有:

      • Term :构造方法,传入一个字符串,解析出系数和指数
      • addTerm:合并同类项,传入Term引用,判断指数相等后系数相加
      • toString:重写
    • MainClass:主函数,程序入口

  • 代码复杂度分析

    • 类复杂度

      Class OCavg OCmax WMC
      MainClass 1 1 1
      Poly 4.4 9 22
      Term 3.17 7 19

      类行数统计

      Class CLOC JLOC LOC
      MainClass 0 0 8
      Poly 0 0 90
      Term 2 0 91
    • 方法复杂度

      为了简化输出结果,Poly类生成字符串时对较多的情况进行了讨论,构造方法中条件和循环语句较多,导致复杂度增加

    Method CogC ev(G) iv(G) v(G)
    MainClass.main(String[]) 0 1 1 1
    Poly.Poly(String) 13 1 9 9
    Poly.add(Term) 2 1 2 2
    Poly.derivate() 3 1 3 3
    Poly.simplify(String) 0 1 1 1
    Poly.toString() 17 3 8 10
    Term.Term(BigInteger,BigInteger) 0 1 1 1
    Term.Term(String) 10 1 8 8
    Term.addTerm(Term) 1 1 2 2
    Term.getCoef() 0 1 1 1
    Term.getDegree() 0 1 1 1
    Term.toString() 15 3 4 7

第二次作业

  • 主要类图

  • 主要类分析

    • Expression

      父类,所有项和因子均继承自此类。定义了求导derivetoString方法

    • Poly

      多项式类,由多个项相加来表达。使用ArrayList容器存放构成此多项式的每个Term。主要方法有:

      • 重写了derivetoString方法:与第一次作业不同,derive方法直接返回字符串
      • 构造方法:传入一个多项式字符串进行分割处理。为了辅助化简,去除多余的括号,定义了hasParermOuterPare方法
      • polyMerge:判断并合并同类项
    • Term

      项类,有多个因子相乘来表达。类似Poly类,使用了ArrayList容器来存放构成的各个因子。主要方法有:

      • 重写derivetoString方法
      • 构造方法:传入一个表示项的字符串,解析出各个因子并加入容器
      • split:将字符串按 '*' 分割为多个因子,辅助构造方法解析
      • matchPoly:检查包含的多项式因子,若只有一项,则将其合并到基本因子中,减少递归的层数
    • Pow

      表示幂函数,由系数coef和指数degree构成。主要方法:

      • 重写derivetoString方法
      • 构造方法:用正则表达式匹配,解析出系数和指数
      • multiply:计算两个多项式相乘
    • Trig

      三角函数,由系数coef,指数degree和类型type构成,type区分sin和cos。主要方法同上面的Pow类。将sin和cos写在同一个类中导致了derive和toString等方法显得臃肿,把sin和cos分成两个类或许更好

    • Num

      表示带符号整数,重写了derivetoString方法

    • Factory

      工厂类,用于创建多项式。在本人的设计中,表达式的各种成分都在构造方法中递归创建,所以工厂模式体现得并不明显。Factory主要封装了表达式字符串前期简化工作

    • Output

      输出处理,处理求导后的字符串,保证输出的合法性

  • 主要类之间的关系

    本人采用递归下降的思想构建表达式,表达式之间的树形关系图如下(不体现继承关系):

graph TB Poly --> Term Term --> Num Term --> Pow Term --> Trig Term --> B[Poly] B[Poly] --> A[Term] A[Term]--> C[NUm] A[Term]--> D[Pow] A[Term]--> E[Trig]
  • 代码复杂度分析

    • 类复杂度

      从下图中可以看到,Poly Term Trig 类复杂度过高。主要原因是构造方法的解析字符串部分比较繁琐。以及Trig类没有把sin和cos分开。另外,Term类求导方法必须使用乘法法则,多重循环增加了复杂度

      Class OCavg OCmax WMC
      Expression 1 1 3
      Factory 1 1 4
      MainClass 1 1 1
      Num 1.43 4 10
      Output 2 2 2
      Poly 3.6 10 36
      Pow 2.43 5 17
      Term 5.8 14 58
      Trig 3.62 11 29

      类行数统计

      Class CLOC JLOC LOC
      Expression 1 0 12
      Factory 0 0 29
      MainClass 0 0 10
      Num 0 0 37
      Output 1 0 24
      Poly 5 0 151
      Pow 0 0 68
      Term 8 0 286
      Trig 1 0 136
    • 方法复杂度

      Term类的derive方法复杂度最高,完成求导过程需要较多的条件和循环嵌套,此外输出合法的字符串也增加了复杂度

      Method CogC ev(G) iv(G) v(G)
      Expression.derive() 0 1 1 1
      Expression.multiply(Expression) 0 1 1 1
      Expression.toString() 0 1 1 1
      Factory.Factory() 0 1 1 1
      Factory.generate(String) 0 1 1 1
      Factory.replace(String) 0 1 1 1
      Factory.simplify(String) 0 1 1 1
      MainClass.main(String[]) 0 1 1 1
      Num.Num(BigInteger) 0 1 1 1
      Num.Num(String) 0 1 1 1
      Num.derive() 0 1 1 1
      Num.equals(Object) 3 4 1 4
      Num.getValue() 0 1 1 1
      Num.multiply(Num) 0 1 1 1
      Num.toString() 0 1 1 1
      Output.outputSimplify(String) 1 1 2 2
      Poly.Poly(String) 19 1 16 16
      Poly.add(Term) 0 1 1 1
      Poly.derive() 4 1 3 3
      Poly.getFirstTerm() 0 1 1 1
      Poly.getTermsNum() 0 1 1 1
      Poly.hasPare(String) 13 3 5 8
      Poly.polyMerge(Term) 20 7 8 9
      Poly.rmOuterPare(String) 1 1 2 2
      Poly.simplify(String) 0 1 1 1
      Poly.toString() 1 1 2 2
      Pow.Pow(BigInteger,BigInteger) 0 1 1 1
      Pow.Pow(String) 8 1 4 6
      Pow.derive() 4 1 4 4
      Pow.getCoef() 0 1 1 1
      Pow.getDegree() 0 1 1 1
      Pow.multiply(Pow) 0 1 1 1
      Pow.toString() 4 4 4 4
      Term.Term(String) 10 4 6 7
      Term.derive() 45 1 14 14
      Term.getCoef() 0 1 1 1
      Term.getFactors() 13 1 8 8
      Term.init() 0 1 1 1
      Term.matchPoly(String,ArrayList) 5 1 4 4
      Term.merge(Term) 6 1 5 7
      Term.noNest() 15 1 9 9
      Term.split(String) 8 1 6 7
      Term.toString() 10 1 5 5
      Trig.Trig(String) 26 1 11 13
      Trig.Trig(int,BigInteger,BigInteger) 0 1 1 1
      Trig.derive() 18 1 5 7
      Trig.getCoef() 0 1 1 1
      Trig.getDegree() 0 1 1 1
      Trig.getType() 0 1 1 1
      Trig.multiply(Trig) 1 1 2 2
      Trig.toString() 9 5 4 5

第三次作业

与第二次作业相比,本次作业在总体架构上变化不大,主要类图基本一致。在类之间的关系上,增加了三角函数嵌套因子。不过,本次作业的难点在于合法性判断

  • 主要类图

  • 主要类分析

    主要类与第二次作业相同,增加了三角函数嵌套因子

    graph TB Poly --> Term Term --> Num Term --> Pow Term --> M[Trig] Term --> B[Poly] B[Poly] --> A[Term] A[Term]--> C[NUm] A[Term]--> D[Pow] A[Term]--> E[Trig] M[Trig]--> N[Num] M[Trig]--> P[Pow] M[Trig]--> Q[Trig]

    改变较大的类:

    • Factory

      增加了输入合法性判断的内容。先检查是否包含无关字符,再检查空白符的合法性,最后检查 '+' 和 '-' 的合法性。增加了辅助判断的方法isLegal,opLegal

    • Trig

      增加了嵌套因子合法性以及嵌套类型的判断。derive方法涉及链式法则求导,复杂度增加

  • 合法性判断思路

    自定义异常类WrongFormatException,各级构造方法中抛出异常。

    对于无关字符、空白符、加减号的合法性判断在工厂类Factory中完成。其他情况的合法性判断主要依靠各级构造方法中的正则表达式匹配,若出现不匹配则逐级向上抛出异常,在main函数捕获异常并处理

  • 复杂度分析

    • 类复杂度

      相比于第二次作业,Poly Trig Term Factory 这些类的复杂度增加。主要原因在于增加了合法性判断的内容。最理想的合法性判断应该与表达式构造过程分开,降低耦合程度,但这一点似乎在我的程序中很难实现,我只能将部分判定过程放在表达式构造之前完成

      Class OCavg OCmax WMC
      Expression 1 1 3
      Factory 5.75 19 46
      MainClass 1 1 1
      Num 1.57 4 11
      Output 2 2 2
      Poly 4.22 12 38
      Pow 2.57 7 18
      Term 4.47 14 67
      Trig 4.54 10 59

      类行数统计

      Class CLOC JLOC LOC
      Expression 1 0 12
      Factory 2 0 168
      MainClass 0 0 14
      Num 0 0 42
      Output 0 0 30
      Poly 3 0 157
      Pow 0 0 77
      Term 9 0 360
      Trig 5 0 289
      WrongFormatException 0 0 2
    • 方法复杂度

      从第二次作业到第三次作业,我的基本方法变化不大,原来较为复杂的方法仍然保持很高的复杂度。新增的isLegal opLegal 等合法性判定方法使用了较多的条件判断,复杂度很高

      Method CogC ev(G) iv(G) v(G)
      Expression.derive() 0 1 1 1
      Expression.multiply(Expression) 0 1 1 1
      Expression.toString() 0 1 1 1
      Factory.Factory() 0 1 1 1
      Factory.generate(String,boolean) 1 2 1 2
      Factory.isLegal(String) 18 9 13 16
      Factory.opLegal(String) 35 19 18 25
      Factory.opOne(StringBuilder,int) 9 6 9 11
      Factory.pareLegal(String) 8 6 3 6
      Factory.replace(String) 0 1 1 1
      Factory.simplify(String) 0 1 1 1
      MainClass.main(String[]) 1 1 2 2
      Num.Num(BigInteger) 0 1 1 1
      Num.Num(String,boolean) 1 2 1 2
      Num.derive() 0 1 1 1
      Num.equals(Object) 3 4 1 4
      Num.getValue() 0 1 1 1
      Num.multiply(Num) 0 1 1 1
      Num.toString() 0 1 1 1
      Output.outputSimplify(String) 2 1 3 3
      Poly.Poly(String,boolean) 23 4 16 19
      Poly.add(Term) 0 1 1 1
      Poly.derive() 4 1 3 3
      Poly.getFirstTerm() 0 1 1 1
      Poly.getTermsNum() 0 1 1 1
      Poly.hasPare(String) 14 4 5 9
      Poly.polyMerge(Term) 20 7 8 9
      Poly.rmOuterPare(String) 1 1 2 2
      Poly.toString() 1 1 2 2
      Pow.Pow(BigInteger,BigInteger) 0 1 1 1
      Pow.Pow(String,boolean) 12 6 5 9
      Pow.derive() 4 1 4 4
      Pow.getCoef() 0 1 1 1
      Pow.getDegree() 0 1 1 1
      Pow.multiply(Pow) 0 1 1 1
      Pow.toString() 3 3 3 3
      Term.Term(String,boolean) 20 9 7 11
      Term.createCos(String) 3 1 2 3
      Term.createNum(String) 1 1 1 2
      Term.createPow(String) 1 1 1 2
      Term.createSin(String) 3 1 2 3
      Term.derive() 45 1 14 14
      Term.getCoef() 0 1 1 1
      Term.getFactors() 13 1 8 8
      Term.init() 1 1 1 2
      Term.matchPoly(String) 4 1 3 4
      Term.merge(Term) 6 1 5 7
      Term.multiply(Term) 1 1 2 2
      Term.noNest() 15 1 9 9
      Term.split(String) 8 1 6 7
      Term.toString() 10 1 5 5
      Trig.Trig(String,boolean) 22 4 8 12
      Trig.Trig(int,BigInteger,BigInteger) 0 1 1 1
      Trig.createCosNest(String) 7 2 1 7
      Trig.createSinNest(String) 7 2 1 7
      Trig.derive() 25 1 6 8
      Trig.getCoef() 0 1 1 1
      Trig.getDegree() 0 1 1 1
      Trig.getIsNest() 0 1 1 1
      Trig.getType() 0 1 1 1
      Trig.isFactor(String) 8 7 5 7
      Trig.multiply(Trig) 1 1 2 2
      Trig.nestDerive() 19 2 7 8
      Trig.toString() 33 9 8 9

三次作业bug分析

发现bug策略

本人采用手动构造测试样例的方式进行测试,样例大致分两类:

  • 复杂样例:尽可能增加嵌套的层数,增加样例长度。

    • 使用大整数
    • 构造多层嵌套,三角函数、表达式因子、项之间组合嵌套,尽可能多地枚举嵌套形式
    • 采用多层无效括号,检查化简,如(((((((((sin(x))))))))))
    • 枚举加减号组合,如+-, -+, ++, +++等,尽可能穷举加减号搭配的形式
  • 简单样例:控制样例长度,使用小整数,主要测试细节问题。很多时候程序能够正确地计算很长的表达式,却在简单表达式上出错。例如,+-x, sin(+ 8), +-+ 8, 1*(+0), cos(x)**0, sin(x)**2 + cos(x)**2等。在系数、指数为0,1,-1等情况下做边界测试。

自己程序的bug

第一次作业相对简单,强测中没有出现bug。从第二次作业开始,bug数量开始明显增加。

  • 第二次作业典型bug
    • 多项式括号处理不完善。例如,+((sin(x) + x**5)) 和 ((((((((x)))))))) 这样的样例曾经导致本人的程序崩溃,在Poly类中增加了去除多重括号的功能之后才得以解决。该bug出现在Poly类中,该类总行数达到了150行。
    • 化简导致的bug。在合并同类项的过程中搜索到值为0的项合并之后没有及时跳出循环,导致重复合并。该bug出现在Term类中,其总行数达到了286行,是本次作业代码行数最多的类。
  • 第三次作业典型bug
    • 指数合法性误判。当输入1*(sin(x)**50*sin(x)**1) 这样的表达式时,将多项式因子合并成sin(x)**51之后误判为非法输入。将合并部分与合法性检验部分分离之后得以解决。这也体现了程序耦合度过高导致的严重后果。该bug出现在Trig类中,其总行数为289行。
    • 带符号整数因子判断不充分。没有对三角函数内部的带符号整数因子的合法性进行判断。在外层Factory中判断完成空白符的合法性之后就将空白符消去了,导致形如sin(+ 6)这样非法的输入被误判为合法。该bug出现在Factory类中,其总行数为168行。

总结三次作业出现bug的位置:出现bug的类在复杂度分析中都表现出很高的圈度复杂度。所有bug均出现在规模很大的类中,同时,这些bug也是位于代码行数较多的方法中。这充分体现了降低复杂度的必要性。

他人程序的bug

构造样例方式与自测样例构造相同。我在互测过程也使用了很多自测时构造的样例(主要是自测时发现bug的样例)。同时,我也结合了他人代码的特点来构造样例,例如,对于做化简处理的代码,测试含sin(x**2)的样例,检查是否出现sin(x*x)这样非法的输出;对于没有统一进行加减号简化处理的代码,测试不同加减号组合情况。

  • 符号处理。加减号连续出现的组合情况较多,在互测过程中有同学没有进行统一的简化容易导致bug。例如本人曾在互测中发现形如+-+8*x三个加减号连续出现化简的错误
  • 嵌套层数过多导致TLE。如((((((0))))))这样的样例出现运行时间过长。所以,采取循环去括号的措施还是必要的。
  • 格式判断细节问题。例如,x*这样的表达式被误判为合法。原因在于用字符串的split方法按 ’*‘ 分割的时候末尾乘号之后的内容会被忽略。所以,在输入字符串前期处理的时候就应该判断首尾是否为运算符。

重构经历总结

  • 第二次作业

    由于在做第一次作业的时候没有考虑留下增量开发的空间,所以第二次作业开始时我几乎重写了所有的derivetoString方法。第一次作业中derive方法不直接返回字符串,需要再次调用toString方法转化为字符串再输出,这种做法十分繁琐,无法适应第二次作业的要求,所以从第二次作业开始我直接在derive方法中返回字符串,求导结果即为最终字符串。

    从上文的类图中可以看出,重构之前没有使用继承结构,不便于对多种因子进行统一管理。

  • 第二次作业优化

    开始做第二次作业的时候,我先考虑的是实现正确性要求,完全没有顾虑性能,连基本的合并同类项工作都没有做,这导致输出结果十分丑陋。表达式稍微复杂就甚至连输出结果的正确性都难以判断。所以我又临时决定做一些优化。主要完成合并同类项,去除多余的括号,将只有一项的表达式因子打开合并。但是,原来的代码耦合程度过高,对任何一个地方稍作修改都会“牵一发而动全身”。所以我再次重写了derivetoString方法。把Term类的抽象为基本因子和嵌套因子组成,将每个因子的系数提取出来,保证Pow Trig 这些因子的系数都为1,简化了底层的derive方法。相比于上一个版本,代码总体上更加简洁,但也导致Term类显得臃肿(从上文复杂度分析结果可以看出)。

    第三次作业基本上在第二次作业的基础上新增功能,没有进行重构。

心得体会

  • 设计架构比编写代码更加重要。OO作业迭代开发的模式要求我们必须设计出良好的架构,否则寸步难行。在第二次作业开始的时候我有些操之过急,没有深入思考设计,导致了两次重构(甚至重构之后产生了新的bug),耗费了很大的精力。在今后的作业中,我必须先思考清楚再动手,前期设计越详尽,代码编写也就越顺利。
  • 不要害怕重构。当发现自己的设计很难再满足新增要求的时候就要果断设计新的思路,重新编写代码。找到正确的思路之后再编写代码比在原来的架构上强行写更有效率。只要思路正确,重构之后能让接下来的增量开发更容易。
  • 充分的测试很重要。从这三次作业来看,中测的强度比较低。第二、三次作业我都在通过中测的情况下发现了不只一个bug。所以测试的关键在于尽可能多地组合可能出现的输入情况。自己构造的测试样例不必太长,利用简单样例进行特殊条件下的边界测试是很有效的。
posted @ 2021-03-29 22:16  李雨东  阅读(115)  评论(1)    收藏  举报