BUAA_OO_2022_第一单元总结

面向对象 第一单元总结

第一次作业

总体架构

​ 万事开头难,在经过若干次脑海中的模拟、推导、重构的循环后,我终于确定了本次作业的总体架构:先对输入进行递归解析,建立起一颗表达式树;之后再对表达式树进行计算与化简。因此,本次作业的各类的整体结构分为三个部分:MainClass 类为程序主控类,负责输入输出等主要流程的控制;LexerParser 类负责对读入字符串进行处理,通过递归解析建立起一颗表达式树;ExprTerm 等其余各类均为表达式树中的不同结点。

类图分析

  • MainClass
    • 负责读入字符串,进行解析处理并输出最后结果的流程控制。
  • Lexer
    • 负责遍历字符串进行词法分析,其中 findPattern(String) 方法用于正则化匹配输入的模式。
  • Parser
    • 利用 Lexer 类遍历字符串,对字符串各部分内容进行递归解析,最终建立起一颗表达式树。
  • Factor
    • 根据因子可以有多重类型而设置统一的 Factor 作为接口,便于对各因子类别进行无差别引用与访问。
    • 各项因子都具有 coefexp 两个属性。
  • Expr
    • 表达式类,实现了 Factor 接口。由于表达式是由若干项与符号组合而成,故通过两个 ArrayList 分别储存表达式的各项及其符号。
    • 在本类中实现了计算化简的核心方法 compute() ,将各项依据符号进行计算,并将结果储存在 nowExpr 中,最后通过 toString() 输出最终化简结果。
  • Term
    • 该类与表达式类相似,由于项是由若干因子相乘得到,故通过 Hashmap 储存项的所有因子。
    • 在本类中同样实现了计算化简的核心方法 compute() ,将项内各因子通过多项式相乘的方式进行合并计算,最终返回 ArrayList<BigIntger> ,可理解为下标为幂次值为系数的化简结果。
  • Variable
    • 作为因子的一种实现了 Factor 接口,重写了基本的 toString() 方法
  • Number
    • 作为因子的一种实现了 Factor 接口,重写了基本的 toString() 方法
  • 优点
    • 整体上解耦设计,将代码分成流程控制、表达式解析和表达式计算三个部分。这样可以在完成代码时分步进行实现与测试,保证前一部分没有 bug 时再完成下一部分,减小了 debug 难度。
    • 将因子作为统一的抽象接口,这样使得可以在解析时递归建立表达式树,并且递归进行计算化简,有利于后续的迭代开发。
    • 将表达式计算结果看作幂次与系数的多项式储存,其中幂次作为数组下标,系数作为对应的值,便于计算、合并同类项与输出。
  • 缺点
    • 部分类的解耦程度还不够高,尤其是表达式和项的计算部分,导致这两个类过于臃肿。
    • LexerParser 部分功能杂糅,导致经常出现意想不到的 bug,且不利于debug。

度量分析

Method CogC ev(G) iv(G) v(G)
Term.compute() 30.0 9.0 10.0 12.0
Expr.compute() 24.0 6.0 12.0 13.0
Expr.toString() 11.0 1.0 7.0 7.0
Parser.parseExpr() 7.0 1.0 6.0 6.0
Parser.parseFactor() 6.0 3.0 5.0 5.0
Parser.parseTerm() 6.0 1.0 5.0 5.0
Parser.parseExp() 4.0 2.0 4.0 4.0
Lexer.next() 3.0 2.0 3.0 4.0
Total 99.0 51.0 80.0 86.0
Average 3.09 1.59 2.50 2.68

​ 上表为各方法的度量分析,从中可以看出方法整体复杂度与类的耦合度都较低,但是存在个别方法的复杂度过高。如 ExprTerm 类中的 compute() 方法作为表达式计算的主要部分,其中包含了多个 if-else 判断语句与递归调用情况,导致其代码行数、复杂度与耦合度都较高。

Class OCavg OCmax WMC
Parser 3.8 5.0 19.0
Term 3.6 12.0 18.0
Expr 3.25 13.0 26.0
Lexer 1.83 4.0 11.0
Variable 1.25 2.0 5.0
MainClass 1.0 1.0 1.0
Number 1.0 1.0 3.0
Total 83.0
Average 2.59 5.42 11.85

​ 上表为每一个类的度量分析,同样可以看出复杂度最高的三个类是 ParserTermExpr , 这是因为 Parser 类作为负责递归解析表达式的部分内容较多,而其他两个类由于实现了化简计算的相关方法导致内容较多且复杂度较高。

程序bug分析

  • 本次作业由于是初次设计整体架构,因此在公测时不可避免的出现了较多 bug。其中一类问题是在递归解析表达式时 LexerParser 之间的配合有误,导致表达式解析错误,此类错误就只能单步测试来慢慢 debug。
  • 公测时发现的另一个错误是当表达式化简结果为零时程序不会输出任何内容。该 bug 的修复较为容易,只需在 toString() 方法最后添加一行判断输出是否为空的代码即可。
  • 在互测时,由于我将表达式表示为幂次与系数的多项式,而误将多项式的最高幂次设置为小于8,导致在遇到边界数据时出了问题。而最终只需将小于号修改为小等于号即可修复该 bug。因此,在修复前后几乎没有对代码行和圈复杂度造成影响,属于是一个由于疏忽而导致的愚蠢 bug。

测试策略

  • 由于第一次作业复杂度不高,我主要使用手动构造样例对代码进行测试。其中,在互测时我针对一些特殊样例如 -x**+a*b 成功 hack 到了一些存在问题的代码

第二次作业

总体架构

​ 本次作业新增了三角函数、自定义函数和求和函数三个新的因子,由于架构较为合理,所以只需在第一次作业结构的基础上添加 CosSinSumCustom 几个类。主要内容在于对更新因子内容后的解析、计算、输出代码逻辑进行迭代更新。

类图分析

  • Lexer
    • 新增 peekN(int)nextN(int) 方法,便于对字符串进行遍历。
  • Parser
    • 新增 parserFunc()parserSin()parserCos()parserSum() 来解析新加入的几类因子。
    • 对于自定义函数,先将其定义的表达式进行解析并储存。当遇到函数调用时,调用已经解析的函数表达式的 replace() 方法对其变量进行替换。
    • 对于求和函数,由于其展开表达式长度可能过长,因此在解析时并不进行展开,等到计算化简时再展开。
  • Custom
    • 自定义函数类用于储存自定义函数的函数名、变量名、表达式等性质。
  • Factor
    • 对所有因子添加 copy() 方法,用于因子的深拷贝。
  • Expr
    • 新增两种 replace() 方法,分别针对求和函数和自定义函数调用进行变量的替换。由于被替换的只能是变量因子,因此只需将解析好的表达式结点替换至对应变量的位置即可。注意,替换时默认为原地替换,则会破坏原本表达式树。因此,必须为每个类实现 copy() 方法实现深拷贝,从而每次在一个全新的表达式树上进行替换。
    • 将表达式的计算结果更改为 ArrayList<Term> 来储存,这是因为新增了三角函数以及取消了指数的限制,故无法继续使用上次作业的定长数组来存储结果。
    • 新增 simplify() 方法对运算结果进行化简,主要实现了合并同类项。逻辑为使用二重循环遍历结果的每一项,判断两项是否可以合并,若可以则将其系数相加,删去后一项。
  • Term
    • 由于项的内容有所更新,因此重新表示项的内容为 coef + exp + sinFactors + cosFactors
    • 新增 simplify() 方法对项进行化简,主要是合并相同的三角函数,并在化简后进行排序便于后续比较。
    • 新增 isSame(Term) 方法判断两项是否可以进行合并同类项操作。
  • SinCos
    • 作为新增因子实现了 Factor 接口,使用 Factor 形式存储三角函数内因子便于后续的迭代。
  • SinComparatorCosComparator
    • 实现了 Comparator<> 接口,用于对 SinCos 两类进行排序。
  • Sum
    • 作为新增因子实现了 Factor 接口,实现了 compute() 方法对求和函数实现边展开边计算,避免先展开再计算导致的计算压力。
  • 优点
    • 由于第一次作业结构设计较为得当,因此本次作业对于新增的几种因子只需要实现 Factor 接口即可,并不需要进行重构。
    • 建立了表达式树的结构,对于自定义函数及求和函数的因子代入只需进行树上结点的替换即可,而没有使用类似字符串替换的方法,这样避免了一些不必要的 bug,并且便于后续迭代工作。
    • 对每个类实现了深拷贝的方法,从而将表达式树分成静态与动态两种:在解析时建立一颗静态表达式树,而在运算化简时建立一颗动态表达式树,保证了不会因为运算化简而破坏原表达式树的内容。
  • 缺点
    • 表达式化简仅仅实现了同类项合并,对于 cos(0)sin(0) 之类的因子没有考虑化简,导致部分测试点性能分数过低。
    • 将大部分重要功能放在了 ExprTerm 两类中,导致这两个类越来越臃肿,不利于维护与迭代。

度量分析

Method CogC ev(G) iv(G) v(G)
Expr.toString() 51.0 1.0 17.0 18.0
Expr.replace(Expr, ArrayList, ArrayList) 38.0 1.0 15.0 15.0
Expr.replace(Expr, Number) 38.0 1.0 15.0 15.0
Term.compute() 20.0 1.0 11.0 11.0
Parser.parseFactor() 19.0 9.0 12.0 12.0
Term.isSame(Term) 19.0 11.0 9.0 13.0
Term.simplify() 18.0 1.0 17.0 17.0
Expr.compute() 16.0 2.0 7.0 7.0
Term.computeExpr(ArrayList, ArrayList) 15.0 1.0 7.0 7.0
... ... ... ... ...
Total 297.0 132.0 239.0 251.0
Average 3.12 1.38 2.51 2.64

​ 上表为各方法的度量分析,和第一次作业相比因为实现功能复杂性的增加导致代码行数有显著增加,但是各方法的耦合程度以及复杂度都有所降低,这说明代码整体的可维护性变的更好了。

Class OCavg OCmax WMC
Expr 4.25 17.0 68.0
Parser 3.4 12.0 34.0
Term 2.94 11.0 56.0
CosComparator 5.0 5.0 5.0
SinComparator 5.0 5.0 5.0
Cos 1.25 3.0 10.0
Lexer 2.0 3.0 14.0
Sin 1.25 3.0 10.0
Sum 1.28 3.0 9.0
Variable 1.33 3.0 8.0
MainClass 2.0 2.0 2.0
Custom 1.0 1.0 6.0
Number 1.0 1.0 5.0
Total 232.0
Average 2.44 5.30 17.84

​ 上表为每一个类的度量分析,同样可以看出复杂度最高的三个类是 ParserTermExpr 。这是因为给这三个部分添加的功能越来越多,导致其结构越来越复杂,应当将其功能拆分成若干模块。但是整体的 OC 与第一次作业相比却有所下降,这说明各类之间的关系更加合理,符合高内聚低耦合的原则。

程序bug分析

  • 本次作业由于内容复杂度限制提升,导致在一些细节地方出了不该出现的 bug,在强测中挂了三个点。因为我使用的是正则表达式来匹配多余的正负号,而忘记在正则表达式中添加新增的变量名y、z、i,这就导致如果函数定义表达式或者求和表达式中这些变量前出现正负号,我的程序就会报错。最后修改 bug 的时候仅仅在正则表达式中加入新增变量名就改好这个 bug,实在是有些可惜。不过这也说明我在测试时并没有将可能出现的情况覆盖全。
  • 而在互测时,我再一次忘记将结果为零的表达式进行输出,被 hack 了若干次。以上两个 bug 都是一行代码就可以修复的问题,并且完全没有影响圈复杂度,在测试时理应被发现,所以我也算为自己的偷懒付出了代价。
  • 后来发现在进行多项式计算,尤其是指数运算时应当一边计算一边调用化简方法,否则多项式的项数将指数级增加进而导致爆栈。虽然修改前并没有出现错误,但是改进后的计算速度可以提升一个数量级。

测试策略

  • 本次互测我主要通过观察代码手动测试找到了一些存在的 bug:比如有同学的三角函数输出格式有问题,或者对于三角函数前以及因子内的符号处理不当,都比较容易被 hack 到。此外,还有人犯了和我一样的错误,没有输出结果为零的表达式。

第三次作业

总体架构

​ 第三次作业减少了上一次作业中因子的限制,因此需要修改的部分较少,且主要任务是查漏补缺与优化。

类图分析

  • Factor
    • 对所有因子添加 equals() 方法,用于在合并同类项时判断两个因子是否相等。
  • Expr
    • 优化原有的两种 replace() 方法,将原本针对不同因子类复杂的条件优化部分下放到各个类中,从而大幅度简化 Expr 类中的代码逻辑。
  • Term
    • 发现万物皆因子的特点,将 Term 也作为因子的实现,从而将所有类统一起来。
    • 利用新增的 equals() 方法对 simplify() 方法和 isSame() 方法进行优化,大幅度减少代码量。
  • SinCos
    • 由于三角函数内部因子也可以为表达式因子,因此需要添加对三角函数内部因子的计算方法,通过 compute() 方法实现。
    • 同样在三角函数类中需要实现 replace() 方法对函数调用、求和函数中变量进行替换。
  • SinComparatorCosComparator
    • 由于合并同类项时使用双重循环遍历,故不再需要对数组进行排序,将这两个类删除。
  • 优点
    • 由于万物皆因子的设定,使得在计算、化简时只需要分别考虑各方法在本类中该如何实现,极大减少了思维量,从而减少了出错的概率。
  • 缺点
    • 没有对输出部分的 toString() 函数进行优化,导致输出部分比较冗杂,从而难以进行一些化简工作,如将 sin(0) 替换成 0 等。

度量分析

Method CogC ev(G) iv(G) v(G)
Expr.toString() 51.0 1.0 17.0 18.0
Cos.toString() 24.0 11.0 14.0 14.0
Sin.toString() 24.0 11.0 14.0 14.0
Term.compute() 20.0 1.0 11.0 11.0
Expr.replace(Expr, Number) 18.0 1.0 9.0 9.0
Cos.toStringHelper() 17.0 9.0 12.0 12.0
Sin.toStringHelper() 17.0 9.0 12.0 12.0
Expr.compute() 16.0 2.0 7.0 7.0
Term.equals(Factor) 16.0 8.0 5.0 11.0
... ... ... ... ...
Total 396.0 203.0 306.0 339.0
Average 3.6 1.84 2.78 3.08

​ 上表为各方法的度量分析,可以看到最复杂的几个方法都是和输出相关的。这是因为我在输出时没有进行有效化简,导致没加一条化简方法就要多加一倍的代码量,显得十分复杂。

Class OCavg OCmax WMC
Expr 3.77 17.0 68.0
Cos 3.92 11.0 51.0
Sin 3.92 11.0 51.0
Term 3.0 11.0 60.0
Parser 3.18 9.0 35.0
Variable 1.85 5.0 13.0
Number 1.5 4.0 9.0
Lexer 2.0 3.0 14.0
Sum 1.25 3.0 10.0
MainClass 2.0 2.0 2.0
Custom 1.0 1.0 6.0
Total 319.0
Average 2.9 7.0 29.0

​ 上表为每一个类的度量分析,其中 CosSin 两类的复杂度明显增加了不少,这就是在屎山上不断加代码的后果。

程序bug分析

  • 由于本次作业的增量较少,所以在公测时出现的 bug 也比较集中。一个发现的 bug 是对于三角函数内嵌套三角函数情况的忽略,导致忘记对内层三角函数的因子进行计算化简。因此我将表达式类中的 compute()replace() 方法分别加入到三角函数类中,即可解决。
  • 还有一个之前作业中就存在的 bug 是求和函数中如果下限大于上限则会陷入死循环中,但是并没有被上周的强测以及互测发现,所以侥幸在第三次作业中进行了修改。

测试策略

  • 覆盖测试使用了自动生成的数据与 sympy 库进行自动测试与检验,成功发现了上述 bug。
  • 在互测中主要构造了一些边界测试数据,如求和函数上下限均超出 int 范围,但是均未成功 hack 到别人。后来根据测试发现一个代码的化简部分有误,故使用包含 sin(x)**2+cos(x)**2 的数据成功进行了 hack。

架构设计体验

​ 通过第一单元的作业,让我领悟到了架构设计的重要性,以及何为层次化设计。有幸我在第一次作业中花了不少时间来确定架构,最终使用递归下降的解析方式建立一颗表达式树,并使用表达式-项-因子-表达式这样的循环层级结构来进行数据表示。所以在第二次作业中,面对新增的求和函数、三角函数、自定义函数,我并没有对整体架构进行大的重构,只是将它们加入到原有的架构中即可。同样第三次作业我也只需要在前一次的基础上进行了微量的修改,省了不少的功夫。但是,我同样也有写的很烂的屎山。从一开始的 toString() 方法本来较为简单,但是在不断迭代的过程中我随意添加各种判断优化条件,导致其长度暴增,最后到了根本无法去重构的地步。所以,我认为在项目开始之初就设计一个合理的架构是极为重要的。同样,在迭代过程中一定不能有偷懒或者投机取巧的方法,因为你会发现大部分这样的想法到了下一次作业就会成了你要重构的痛点:)

心得体会

​ 我是一个理论驱动实践的人,总想先把问题想的差不多了再去解决。这种心态就让我在写第一次 oo 作业的时候受尽了折磨,对着一片空空的屏幕苦苦思索。但是也要感谢这种心态,使得我可以在第一次作业时就对项目架构有了一个较为合理的设计,为之后的迭代减轻了不少负担。我认为,适度思考有助于项目设计,但是过度思考也会损耗过多的精力,我们应当进行适当的平衡与取舍。同时,在完成三次作业的过程中也反映了我的一些缺点,就是有时写完了代码就不愿意再去花更多的时间来验证自己代码的正确性。综合起来所有被找到的 bug,几乎所有问题都是只需要改一两个字符或者符号就可以解决的 bug,但是自己在编写代码或者测试时却没有去考虑这些细节。这个毛病在不断的强测、互测、强测、互测被 hack 的过程中逐渐得到了改正。所以,在完成一个单元之后回头看过去,发现自己确实在不断的学习,不断的成长,虽然这个过程中可能充满了痛苦:)

​ 最后还是很感谢这门课程的老师、助教以及帮助过我的同学,让我顺利的度过了第一单元的考验,也让我对接下来的挑战充满期待。

posted @ 2022-03-23 16:25  JackyZhuo  阅读(72)  评论(1编辑  收藏  举报