OO-2022-Unit1-BeihangCSE

第一单元总体分析

第一单元的训练,在我看来,主要包括两个部分。

  • 第一部分是训练我们对面向对象编程中根据用户需求(也就是题目要求)对类进行设计以及方法中继承、实现、多态等概念的运用。当然从hw2开始,因为类的高度抽象以及多层嵌套,还需要我们掌握引用、对象的区别和实现方式以及深浅克隆等概念。
  • 第二部分则是通过“优化”训练我们思维的完备性,比如化简中的x**2 -> x*x、正向提前等优化思路,比如要避免优化导致的符号问题、输出不符合规范化表述等问题,还比如有一些优化了不如不优化、优化导致运行超时、优化与正确性难以解耦等问题。

三次作业单独分析

第一次作业

类的基本数据分析

类名 类属性数量 类方法数量 类总规模
Polynomial 1 7 84
Parser 2 4 80
Term 2 4 61
Lexer 3 4 40
Expression 1 5 35
Number 2 3 27
Var 1 3 23
Divisor 1 1 22
Main 0 1 17
Factor 0 2 7

经典OO度量分析



类图展示

设计上存在的比较鲜明的特点是:解析、存储数据结构、运算三个部分都分离开,实现了高内聚低耦合,调试debug都比较容易。

  • 其中解析部分参考了构造栏目中的代码。存储结构则是按照形式化表述对每个类进行了抽象和封装,并且还使用了factor接口抽象了表达式、常数、幂函数类的行为。运算部分主要由Polynomial类实现。

  • 相比存储数据结构,另外两个部分明显的太单一,Parser类、Polynomial类的类复杂度都很高。尽管其功能非常固定,且有明确的解析方法、运算规则做指导,并不容易产生bug,但我们还是应该尽量避免,(后续bug分析中也提到了这部分出现的bug)

  • 存储数据结构部分做了很多抽象,但事实上没有提供性能、空间甚至后续迭代开发上的收益,存在一些无用的设计。比如Divisor类。它只包含一个sign属性以及一个构造方法。设计Divisor的目的是为了统一Number和Var。但事实上它只提供了判断符号这一个功能,而这个功能在hw1的要求中,Var类根本不需要实现。而且,事实上,在Parser类中就已经完成了对因子的符号的处理,导致Divisor的存在更加没有必要。(不过反过来思考,Parser分析符号,是不是一定程度上算越位了?这本应该是直接交由存储数据的类来处理的。)

Bug分析

本次实验成功通过强测。互测阶段受到hack:5/19,发起hack:0/14。

  • Bug点:Bug点出现在输出时的暴力字符串替换。位于Polynimial类的toString()方法中。为了优化性能,在最后输出时直接将“1*”换为空串,预想能达到不输出不必要的系数“1”。然而忽略了可能存在“211*x”被替换为“21x”的情况,导致了错误。属于考虑不周。

  • 复杂度分析:Polynomial类的toString()方法的圈复杂度为9,明显高于其他方法。为了尽可能的缩短输出的表达式,在该方法中存在大量的if-elseif语句用于判断是否输出某些量。在最后为了省事,就直接使用暴力字符串替换“1*”,而没有全面的考虑可能的输出情况,导致了错误。

  • 互测经验:分析room内同学的bug时主要针对了“一main到底”的同学,尽管最后没有结果:)。

第二次作业

类的基本数据分析

类名 类属性数量 类方法数量 类总规模
Parser 2 4 103
Polynomial 1 6 85
Main 0 1 73
Divisor 1 5 70
Lexer 3 4 52
Function 3 3 49
Expression 2 3 48
SumFun 4 2 44
Term 2 4 37
Trio 3 5 37
Var 2 5 37
Number 1 4 29
Factor 0 3 10

经典OO度量分析

类图展示

hw2主要是在hw1的基础上对Polynomial做了重构。删除了hw1中不必要的Divisor,增添了作为变量HashMap的Divisor(好像并不应该使用同一个类名)。有了Divisor帮助Polynomial进行运算和存储,后者的类复杂度在hw1的基础上并没有增加太多就实现了更加丰富的功能。

问题在于,把函数的代入放在了main()方法中。这导致Main类以及main()方法的复杂度急剧上升。这并不是一个很好的架构。好在思考全面且hw2不要求实现嵌套,在main()方法中没有出现严重的bug。

Bug分析

本次作业强测4个点WA,互测阶段发起 hack: 2/12,受到 hack: 5/22

  • Bug点;Bug点分别位于Parse类的parseFactor()方法和Polynomial类的toString()方法中。前者在index=0时直接仍然原样返回了factor,而在factor的处理中没有对index=0的情况做特殊判断,而是原样输出。后者则是没有考虑空串输出的情况,即Polynomial类中HashMap的value全为0是,应该输出0,而不是空串。

  • 复杂度分析:Polynomial类的toString()的圈复杂度为12,Parse类的parseFactor()方法圈复杂度为8,都明显高于平均水平。前一bug因为花了更多的精力更加全面的考虑输出的化简情况,一定程度导致了在极小的地方,也是就输出空串的情况,出错。事实上,这一情况在hw1已经考虑到了,但在迭代过程中,因为重构导致大规模的删除代码,使得一些合理的架构没有保持下来。后一bug的情况是类似的。为了实现递归下降读取三角函数,相比hw1中的parseFactor()方法,圈复杂度增加了一倍。index=0时返回new Number(1)在hw1中就已经纳入考虑,而在迭代过程中因为大量删改代码,导致这部分被遗忘。甚至还出现了我以为我写了,但其实我没写的怪事。

  • 互测分析:分析room内同学的bug时主要根据hw2迭代过程中新增的部分设置。例如对三角函数中出现的因子(常数可以有前缀正负,x可以有指数等等)。当然,在了解到自己的bug后,还针对性的测试了其他同学是否也存在这些bug。

第三次作业

类的基本数据分析

类名 类属性数量 类方法数量 类总规模
Parser 2 4 99
Polynomial 1 6 85
Main 0 1 74
Divisor 1 5 70
Function 3 3 50
Expression 2 6 48
Lexer 2 4 47
SumFun 4 2 45
Trio 3 5 36
Var 2 5 36
Spliter 2 3 33
Term 2 4 33
Number 1 4 28
Factor 0 3 10

经典OO度量分析

类图展示

整体上与hw2变化不大。为了实现函数的嵌套调用,引入Spliter类帮助Function分析函数中的变量。

做的不好的地方包括:

  1. main()方法的复杂度还是太高。原因与hw2是相同的。但这部分解析和替换完全也可以交给Spliter类处理,或者是新建一个类来识别表达式中的函数因子。
  2. 仍然使用了字符串替换的方式来实现函数调用。尽管使用多层括号嵌套保证了其正确性,但多层嵌套导致的性能损失是不可忽略的。

Bug分析

本次作业成功通过强测,互测阶段发起 hack: 2/21,受到 hack: 0/35。

  • 复杂度分析:在修复了hw2的bug后,架构没有太大的调整,圈复杂度相比hw2没有太大变化。新增类的方法及其相关方法的圈复杂度也没有明显过高的情况出现。

  • 互测分析:分析room内同学的bug时,因为开放了sum的使用,因此主要针对了sum类发起了hack。例如sum中不限制数据,即允许大整数、复数等,结合求和表达式中的sin()**2等一起构造数据。

架构设计

hw1

第一次作业主要使用了练习题中的递归下降这一思路并在练习题代码的基础上适当修改完成了对表达式的分析。将表达式的分析与最后的化简求值分离开,首先把表达式的各个组成部分转化为Polynomial类,在按照多项式的运算合并规则化简为标准的表达式。

hw2

第二次作业因为引入了三角函数这一变量,尽管依然可以使用递归下降的思路分析表达式,但使用多项式来表达最后的结果显然是不合适。为了尽可能的避免深克隆浅克隆的问题,使用HashMap<String, BigInteger>来表示一项中存在的非整数部分。此时仍然使用Polynomial类来表达最终的结果,但其数据结构转变为<HashMap<String, BigInteger>, BigInteger>。

因为使用递归下降方法分析表达式,因此可以用于分析任意多层的括号嵌套,因此利用字符串替换的方法实现函数的调用。

hw3

第三次作业仅仅增加了函数调用和三角函数中因子的情况。因此只需增加表达式作为因子时返回的字符串(即增加一层括号)以及新建Spilt类帮助我们识别函数调用中的各个变量。

心得体会

因为之前没有接触过java,对java的了解仅仅局限于pre2、pre3中对类、方法、继承、接口实现等概念的学习和使用,尚不非常熟悉。在最开始面对hw1时,一度非常困惑。还好期间有助教的训练栏目提供了一个递归下降的解析思路做参考。但即使是这样,我也花费了大约2、3天时间才完成了解析部分的设计。但明显能看到类的设计、方法的设计中还存在冗余、累赘的部分,当时认为这些多余的设计或许能在之后的迭代过程中提供方便,但事实上,这些设计并没有在之后的作业中派上用场。而且这些设计本身就相当别扭,例如,对因子类都定义了一个int sign属性,但是它的setter是通过Parser类中传递字符串“-”实现的。又例如Polynomial的加法与减法,减法就显得非常没有必要,使用hw2中的neg()方法(取负)就能很容易且优美地将这些加减统一起来。

hw1到hw2的迭代跨度很大。也是在这期间我才真正理解到java中对象、引用、构造等概念的真实意义与底层的实现方法(当然,其实也只是很表层的理解了,应该说是意识到了这些概念的区别,意识到java和c的区别)。为了实现三角函数组合成的项的运算,必然要实现的是判断可乘可加等情况。为了偷懒,我用HashMap<String, BigInteger>构造了Divisor类,两个java原生类的设置让我很方便的实现了equals(),clone()等方法。最后也使用了最简单的字符串替换实现了函数的代入。应该说hw2的时间相比hw1更加紧张,在前期我自信地写完了大部分代码,但在运行后发现各种浅拷贝导致的变量变化、乱用equals()导致的判断错误等问题。最后用Divisor类完成代码后,也没有太多时间检查,勉强通过了中测就行的心态,一定程度上给之后强测暴毙埋下隐患。

hw2到hw3的迭代在hw2的基础上就显得非常简单。我只增加了一个Spliter类用于识别函数中可能存在的变量嵌套。如果之后有时间,我一定重构代码,用表达式树等更面向对象的方法完成。

posted @ 2022-03-25 14:38  Danny121008  阅读(44)  评论(0编辑  收藏  举报