北航OO第一单元总结
第一单元(表达式求导)总结博客
第一次作业
第一次作业较为简单,因子只包含常数因子与幂函数因子,保证输入合法。
1. 程序结构分析
-
类的属性个数、方法个数、每个方法规模、每个方法的控制分支数目、类总代码规模
类名称 属性个数 方法个数 总代码规模 MainClass 0 1 10 Poly 1 5 81 Term 2 10 114 -
MainClass类分析
方法名称 方法规模 控制分支数 main()5 0 -
Poly类分析
方法名称 方法规模 控制分支数 Poly()23 3 equals()10 2 hashcode()2 0 toString()15 3 diff()10 1 -
Term类分析
方法名称 方法规模 控制分支数 Term(BigInterger, BigInteger)3 0 Term(String)30 4 equals()10 2 hashcode()2 0 getCoeff()2 0 getDegree()2 0 combine()5 1 multiply()2 0 toString()20 1 diff()5 1
-
-
类图
![]()
- 思路:将输入的表达式分为表达式和项两个层级,分别用一个类来进行存储。Poly类中以Map存储各个Term,关键字为每个Term的指数,便于合并;Term类中直接以系数和指数的方式存储幂函数。最终以Poly类的求导函数调用Term类的求导函数,完成表达式的求导。
- 优点:整体结构较为清晰,类之间有基本的层次调用关系。把输入输出处理独立于主类之外,便于程序的基本调试。
- 缺点:基本数据层次没有理解清楚,没有构造“因子”这一层级,直接把常数作为幂函数的系数与幂函数混杂在项之中,导致程序可扩展性差(这也是第二次作业重构的原因之一)。此外,只是对数据存储结构进行了分层,却同时也把很多类无关的方法等混杂在其中,代码耦合度较高,没有完全做到类的各司其职。
-
方法复杂度分析
![]()
可以看出Term类的实现比较糟糕,其构造器 和
toString()方法的设计复杂度达到预警,这是由于对程序功能的分解不够到位,将功能相互独立的程序段混合在一起导致的复杂度过高。注:
- ev(G)是基本复杂度,衡量非结构化程度,ev(G)高意味着难以模块化和维护。
- iv(G)是模块设计复杂度,衡量模块之间的调用关系,iv(G)高意味着模块之间的耦合性高,难以隔离和复用。
- v(G)是圈复杂度,衡量结构的复杂程度,v(G)是说明代码难以测试和维护。
- CogC是认知复杂度,衡量代码的难以理解的程度,CogC高说明代码比较难以理解。
2. 程序bug分析
本次采用了比较暴力的方式来编写程序,这样的结构非常不好。幸而这次作业难度不高,并没有出现很大的bug,性能优化也较为简单,只需注意基本的合并同类项、正负号优化等,就可以拿到很高的性能分。但这次由于本身程序思路不清晰,再加上对指导书中多项式规则没有理解到位,导致在互测时出现了三个正负号连用时程序出错的问题。这一bug在互测时竟然被hack了31次,让我十分崩溃。后面发现这时由于解析表达式时使用replaceAll()方法替换时考虑不周全,而当时使用的是split()方法对表达式解析,这才导致出错。
出现bug的地方是Poly类的构造器Poly(),是Poly类中规模最大的类,圈复杂度中等。
3. 发现别人程序bug
第一次作业虽然尝试着hack,但没有成功hack到别人的bug。
第二次作业
第二次作业难度陡然上升,新增了三角函数因子【sin(x)、cos(x)】和表达式因子,保证输入合法。由于第一次层级设计很差,显然第二次作业不得不进行重构。
1. 程序结构分析
-
类的属性个数、方法个数、每个方法规模、每个方法的控制分支数目、类总代码规模
类名称 属性个数 方法个数 总代码规模 MainClass 0 1 9 Expression 1 7 106 Term 1 6 156 Factor 0 1 7 ConsFactor 2 4 25 PowerFactor 2 4 47 SinFactor 2 4 44 CosFactor 2 4 44 ExpreFactor 2 4 25 Factory 5 1 32 -
类图
![]()
- 思路:本次加入了三角函数因子和表达式因子,这意味着之前的数据结构不能再沿用。但上一次的层级思路仍然有意义,借鉴于上次的思路和同学分享的思路,将数据分层为表达式、项、因子三层。采用递归下降的分析方法,一层一层对输入的表达式进行解析,直到因子层使用工厂模式进行因子的创建。所有因子都继承一个公共的Factor接口(仅含一个求导的
diff()方法)。值得一提的是,表达式因子内部直接保存了一个顶层的表达式,形成了一个递归的结构。 - 优点:类的个数总体上较为适中,既不会太多冗杂,也不会太少过聚。整体有一个清晰的继承调用结构,利于一层层调试。使用工厂模式对因子层级进行生成,减轻了类之间的耦合,扩展性好。将因子统一继承一个接口,便于类与类之间的协作。
- 缺点:把输入处理、递归下降的过程植入在Expression和Term级别的构造器中,同时还在构造的同时进行简化,这导致两个构造器的工作过于沉重。Factory类有点“伪工厂模式”的感觉,里面只有一个静态的
make()方法,似乎并不是“正统”的工厂模式。
- 思路:本次加入了三角函数因子和表达式因子,这意味着之前的数据结构不能再沿用。但上一次的层级思路仍然有意义,借鉴于上次的思路和同学分享的思路,将数据分层为表达式、项、因子三层。采用递归下降的分析方法,一层一层对输入的表达式进行解析,直到因子层使用工厂模式进行因子的创建。所有因子都继承一个公共的Factor接口(仅含一个求导的
-
方法复杂度分析
![]()
这里只放出了复杂度最高了几个方法。可以发现,就如之前说到的,将多个功能混杂在Expression和Term的构造器中,导致这两个方法的复杂度完全爆表。为了对表达式更好地简化,simplify()函数做了很多工作(如合并常数、去除多余的1、简单合并三角函数等),这才导致其基本复杂度不高,却耦合性很高。
2. 程序bug分析
这次因子的大幅增加,求导法则自然也有所增加。求导过程的难度提升,自然也增加了程序bug的出现几率。首先是乘法法则的实现,就让我伤透了脑筋,乘法法则细节上掌握的偏差,导致了我的程序在这一点上出现了一点毛病。此外,递归下降细节上处理不到位,也给我带来了一些麻烦。虽然强测的结果令我很满意,但在互测中还是被测出了一个bug,这是由于我未对(准确说应该是忘记了)幂函数乘积进行合并,互测时的阴间测试点反复使用复杂的乘法法则卡死了程序。
在修复这个bug后,很不幸地我又陷入了其他测试点TLE的问题。后面发现这是因为我为了充分化简求导结果,将求导的结果(String类型)扔入,创建一个新的Expression以简化,再输出。这看起来是一个好方法,但实际上当求导结果非常复杂时(考虑到阴间测试点,这其实是大多数情况),解析化简求导的结果对程序来说是一个极大的考验,导致TLE。我只好放弃一部分简化,直接输出结果,这才修复了bug。
出现bug的方法为Term.simplify()方法,其圈复杂度最高。
3. 发现别人程序bug
搭建评测机对我来说实在是一件非常繁琐的事情,而且我并不想成为每半个小时就hack别人的狼执着于hack的同学,所以都是手动寻找bug。这次发现room内一位同学在遇到0*时会出现空指针的问题,所以hack了一下。
第三次作业
第三次作业难度再次上升,三角函数内部支持替换为因子,且不再保证输入的正确性,需要判断输入格式正误。由于第二次重构后大体层次与这次相差不大,只是多出几个功能,所以不需要重构。
1. 程序结构分析
-
类的属性个数、方法个数、每个方法规模、每个方法的控制分支数目、类总代码规模
类名称 属性个数 方法个数 总代码规模 MainClass 0 1 18 Expression 1 5 92 Term 1 6 149 Factor 0 1 3 ConsFactor 1 4 23 PowerFactor 1 4 45 SinFactor 2 4 64 CosFactor 2 4 64 ExpreFactor 1 4 23 Factory 5 1 32 WrongFomatExc 0 0 2 FomatProc 0 6 130 -
类图
![]()
- 思路:第二次作业到第三次作业,相比于第一次作业到第二次作业而言,整体上结构变化不大,只是多了一点功能和细节上的区别。为了更好地应对格式判断和处理,我专门设置了一个新的类FormatProc。其中有几个静态的方法,如检查空格的合法性、检查正负号的合法性、检查指数的合法性等,并对输入进行初级的处理(如去除无用的空格、处理括号等)。为了应对层出不穷、防不胜防的错误格式,我创建了WrongFomatExc这个异常类,各个层级都进行检查,如果输入与期望不符便抛出异常。
- 优点:新建了一个FormatProc类来对基本的格式检查和格式处理,减轻了Expression和Term构造器的复杂度。使用抛出异常的方式来处理输入格式错误,使得所有的格式错误都可以最终在顶层捕获,而不需要在各个层级都进行处理。
- 缺点:对格式的检查分散在了各个层级、各个类之间,这对debug并不友好。当出现格式判断出现bug时,无法快速地定位到究竟是哪一层、哪一个类中的格式判断与期望不符。此外,这样的结构对格式判断的“分工”不够明晰,“一个错误格式究竟应该在哪里被发现”这个问题没能很好地解决,为之后出现bug埋下了种子。
-
方法复杂度分析
![]()
这里同样只放出了复杂度最高的几个方法。总体而言,复杂度最高的是FormatProc中的两个check方法,这是意料之中的,也是可以理解的。但不好的一点是尽管将格式判断和格式处理独立出来,Expression和Term的构造器仍然比较复杂。此外,由于链式法则的引入,Term级别的求导函数
diff()复杂度也不低。
2. 程序bug分析
第三次作业的难点在于:1. 输入不再保证输入正确,且有一些额外要求,需要判断输入的格式是否满足要求;2.三角函数不再仅限于sin(x)和cos(x),括号内部可以换成任意的因子,需要额外解析内部的因子类。尽管在开始时我已经有了解决的思路,但最终的实现并不像我想象的那么简单。
很不幸的一点在于,第一次提交,弱测和中测都有一个点没过。在这个基础上,我自己对程序进行再次测试,在之后的两次提交里分别找到了一个bug,但仍然WA。这让我百思不得其解。当时在讨论区、微信群等地方,这两个点也是讨论的最热烈的,时不时就有同学讨论“中测data3”、“弱5中3”,互相找bug。不少同学给出了一些测试点来帮助卡住的同学,我也尝试着使用这些来测试我的程序,但并没有找到我的问题所在。在急切的心情下,我坐在程序前试了几个小时的测试样例,也与相同测试点过不去的同学进行了交流,最终发现了自己的bug。我之前以为自己出现的bug应该在格式判断上(比如不是错误格式的判断成Wrong Format),但就像老师说的,“最安全的地方就是最危险的地方”,我出错是在三角函数的解析上。结论是千万别想着用正则表达式来偷懒,如果递归下降就递归下降到底。
在强测和互测中,我也出现了几个bug,主要问题仍然是在格式的判断上。由于沿用了第二次作业的将求导的结果(字符串)再次解析化简的方法,导致在判断指数绝对值是否小于50时,输入本身没有错误,但合并化简后就超过50导致误输出Wrong Format。此外还有对有符号数的格式判断不完备,原本认为两个及以下的正负号不会出错,却遗漏了三角函数内包含有符号数时的格式错误。
出现bug的方法为SinFactor.SinFactor()、CosFactor.CosFactor()、FormatProc.checkAdd()等方法,其复杂度高。
3. 发现别人程序bug
这次中测的强度比较高,所以基本大家都全副武装。没有找到别人程序的bug。
重构经历总结
这个单元的程序重构主要是在第一次作业与第二次作业的过渡上。在编写第一次作业时,我本来以为我可以在此基础上进行扩展和改写,来应对之后的功能提升。但接下来的几次理论课和那一周的研讨课,给了我很大的震撼。
在第一次作业中,由于对求导的结果进行了最大程度的简化,程序结构非常繁杂,数据的层级结构也不够完整,不足以支撑第二次作业的功能扩展。我同时预见到第三次作业将面临判断输入格式的问题,如果继续沿用第一次作业的正则表达式的分析方法,那么对输入进行分析将非常困难,可能在第三次作业时又需要进行重构,这是我不希望发生的。鉴于这两点,我毅然决然选择了重构。那一周应该是我第一单元中最痛苦的一周,重构整个代码,无异于从头开始对求导的程序进行编写。当时不仅要学习新的递归下降方法,还要弄清多个因子乘法法则的原理,同时要兼顾一定的化简,甚至还要为下一次功能扩展做准备,加上那一周时间本就不多,这对我而言是一个极大的挑战。那个周六晚上,我硬是熬到第二天凌晨3点,才基本完成了中测。
重构前后类图对比如下:

(↑重构前)

(↑重构后)
心得体会
- 第一单元是面向对象编程的开始,以表达式求导的一个数学问题引入,让我对面向对象的编程思想有了一个很好的了解。在这一个单元的三次编程作业中,我开始明白面向对象编程与面向过程编程的区别。面向对象编程,是把程序的工作看作是不同类之间的相互协作和相互调用,通过建立不同的相对独立的类,使得它们之间进行交互的同时又保留一定的独立性,降低程序的耦合性,提高程序的可扩展性。面向对象和面向过程两种编程方法,没有一定的谁比谁更好,只是在面对不同的问题时各有优劣。
- 第一单元需要对输入的数据进行简单的判断和处理,以更好地应对输入的多样性和不同的类的存储。为了完成这一部分,我对正则表达式和递归下降分析有了更深入的理解。正则表达式实际上就是一种字符串的模式,java中通过Pattern和Matcher类来对正则表达式和目标字符串进行匹配和搜索操作。递归下降偏向于一个面向过程的算法,但却非常简单好用。递归下降实际上是一个有限状态机,把语法分析分解为多层,每层在分析出下一层的结构时,就调用下一层的分析算法进行分析,直至最底层分析完成再返回上层,直至整体分析完成。由于分析过程分解到多层,每层只需完成自己的工作,减少了耦合性,同时还可以在分析过程中兼顾格式的检查。对于本单元的多项式解析,尤其是第三次作业加入Wrong Format的格式检查,非常简便好用。
- 第一次作业和第二次作业时,我面临了重构的问题。这是因为对面向对象思想理解还不够深刻、程序结构安排不适合导致的。这一次重构,花费了我大量的精力,同时也是对我的一个经验和教训。







浙公网安备 33010602011771号