航空系统pta作业集1-3
第一单元三次作业总结Blog
写在前面
课程的第一单元结束了。第一单元,以多项式求导程序为中心,不断增加新的功能,我学到了很多:
- 熟悉了Java基本语法;
- 初步建立起了面向对象的设计思路;
- 认识到了代码可拓展性的重要;
- 学会了构建自动化测试工具。
说真的,这三周过得挺不容易的。第一次作业我还觉得挺简单,第二次作业就开始头疼了,到第三次作业直接懵了。不过熬过来之后发现,确实学到了不少东西。这篇博客我就随便写写,记录一下这三次作业我都干了啥、踩了哪些坑。
Complexity Metrics(复杂度分析)
因为下面要用到复杂度分析,所以先简单说一下这些指标是啥意思。
我们需要使用的主要是方法和类的复杂度分析。
方法的复杂度分析主要基于循环复杂度的计算。循环复杂度就是看一个方法里面有多少条不同的执行路径。if else多了、for循环多了,这个数就大。数越大说明这个方法越复杂,越容易出bug。
- ev(G):表示一个方法的结构化程度,值越大说明代码结构越乱,各种break、continue满天飞。
- iv(G):表示一个方法调用其他方法的紧密程度,值越大说明方法之间耦合越紧。
- v(G):就是循环复杂度本身,可以理解为测试这个方法需要准备多少种不同的输入。
对于类,有OCavg(类的方法平均复杂度)和WMC(类的总复杂度)两个指标。WMC太大说明这个类干的事儿太多了,该拆分了。
下面我就从程序结构、公测互测、bug分析几个方面来总结一下我的三次作业。
第一次作业
作业要求
实现简单多项式的格式判断和求导。多项式由项相加减组成,项可以是幂函数(比如2*x^3这种)或者常数(比如-5这种)。
我是怎么写的
我的思路很简单:先判断输入的格式对不对,对了就把每一项拆出来,存到ArrayList里,然后挨个求导,最后把结果拼起来输出。
第一次作业代码规模如下图:
(这里放SourceMonitor的截图,显示总行数156行,源码行数132行)
从图上能看出来,代码不多,156行。但问题是,大部分代码都塞在Main类里了,其他两个类基本没啥用。
类图
第一次作业类图如下:

这个图画得很简单,因为我的设计就这么简单。Main类啥都干,解析、求导、输出全包了。PolyList就是个装项的列表,PolyNode就是存系数和指数。说实话这个设计挺烂的,但当时觉得能跑通就行,没想太多。
复杂度分析
第一次作业的复杂度分析如下:
(这里放SourceMonitor的复杂度表格)
方法复杂度:
| 方法名 | v(G) | 说明 |
|---|---|---|
| checkFormat | 12 | 格式判断,写了一堆if else |
| parsePoly | 10 | 解析多项式,也挺复杂 |
| printPoly | 15 | 输出化简,各种情况都要考虑 |
| derive | 4 | 求导,这个倒还好 |
类复杂度:
| 类名 | OCavg | WMC |
|---|---|---|
| Main | 10.25 | 41 |
| PolyList | 1.0 | 3 |
| PolyNode | 1.0 | 4 |
可以看出,Main类的平均复杂度10.25,总复杂度41。正常来说平均复杂度在5以下比较好,我这超了一倍。printPoly方法的v(G)=15,意味着有15种不同的情况要处理,难怪我当时写的时候觉得晕。
PolyList的构造方法和print方法的复杂度很高,因为用了很复杂的if/else条件判断。而PolyList的构造方法条件语句复杂,是因为我对正则表达式不熟,用了很多笨办法。
Bug分析
公测
笔者的程序在公测中没有出现正确性错误,所有测试点都过了。但是由于优化输出的时候忘记-1x中的1可以省略,导致一个点性能分不满。就是输出了"-1x"而不是"-x",少写了一个if判断,扣了分。
互测
笔者的程序在互测中被发现一个bug,原因是笔者在做输出优化时,只考虑了最后只剩一个系数为0的项,如果出现类似0*x3+0*x4这种两个系数为0的项,就会没有输出。
举个例子,输入"0*x+0",我的程序啥也不输出,应该是输出"0"。这是因为我把系数为0的项都跳过了,如果全都跳过,结果就是空的。改起来也简单,加个标志位就行。
笔者在互测中,hack了其他人17次,大部分都是正则表达式的错误使用导致的,主要表现在对输入各种情况考虑不周全。比如有人写的正则匹配不了"1+++2"这种连续加减号的情况。
测试方法
我是这么测试的:
-
用python的xeger包生成测试数据**。先写一个正则表达式(尽量覆盖所有合法输入),然后用xeger随机生成字符串,喂给我的程序。
-
用python的sympy算正确结果**。用subprocess调用我的java程序,拿到输出,然后用sympy对同一个表达式求导,对比两者是不是一样。这个自动化脚本帮了大忙。
-
读别人的代码找bug**。在互测阶段,有2个bug是我直接看别人代码发现的,然后针对性构造数据去hack。
第二次作业
作业要求
在第一次的基础上,增加了sin(x)和cos(x),还加了括号嵌套(比如(2*x+3)这种整体作为一个因子)。输入格式的判断也更严了。
我是怎么写的
第一次的代码根本没法扩展,所以我全部重写了。新的思路是:
- 搞一个Factor接口,所有因子都实现它
- Term表示一项,里面有系数和多个因子
- Expression表示整个表达式,里面有多个Term
- 用递归下降的方法解析字符串,这样括号嵌套就好处理了
- 求导用链式法则,每个因子自己知道怎么求导
这次重写花了我两天时间,写的时候觉得好麻烦,但写完再看确实比第一次好多了。
代码规模
第二次作业代码规模如下图:
(放SourceMonitor截图,总行数511行,9个类,38个方法)
代码量一下子涨到511行,是第一次的三倍多。类也从3个变成了9个,我把功能拆分得更细了。
类图
第二次作业类图如下:

这个图看起来复杂多了。关键点是:
- Factor接口统一了所有因子,求导的时候可以多态调用
- Parser用递归下降处理括号嵌套
- Expression和Term形成了树状结构,跟多项式的数学结构是对应的
复杂度分析
第二次作业的复杂度分析如下:
(放SourceMonitor的复杂度数据)
方法复杂度(挑几个高的):
| 类 | 方法 | v(G) | 说人话 |
|---|---|---|---|
| Parser | parseFactor | 12 | 要判断下一个token是啥,分好多种情况 |
| Parser | parseTerm | 9 | 解析一项,也有不少分支 |
| Expression | toString | 14 | 输出化简,又是各种if else |
| Term | toString | 11 | 输出化简,也是一堆判断 |
类复杂度:
| 类 | OCavg | WMC |
|---|---|---|
| Parser | 5.6 | 39 |
| Expression | 3.8 | 23 |
| Term | 4.2 | 21 |
| 各种Factor类 | <2 | <8 |
从数据能看出来:
-
Parser类的总复杂度39**,和第一次作业的Main类差不多。递归下降虽然思路清晰,但写出来还是有很多分支判断。
-
parseFactor方法的v(G)=12**,因为它要决定创建哪种因子:幂函数、sin、cos、常数、括号表达式等等,switch case写了一大堆。
-
toString方法复杂度还是高**,14和11,和第一次作业一样,输出化简的逻辑就是复杂。
-
各种Factor类的复杂度很低**,说明把不同的因子分开写是对的,每个只做自己的事,不乱搞。
Bug分析
公测
公测全过了,性能分也拿满了。这次我在输出上多花了些心思,没再出现漏掉化简的情况。
互测
互测被人发现了一个bug:sin(0)或cos(0)化简不彻底。比如sin(0)求导应该是cos(0)0=0,但我只化简到了"0cos(0)",没有继续化简成"0"。虽然数学上没错,但不符合同学的预期。
还有一个问题:括号嵌套太深会栈溢出。我的解析器是递归的,每层括号就多一层递归。有同学构造了100多层嵌套,我的程序直接崩了。虽然指导书没说支持这么深,但互测里确实会被人搞。
互测中我hack了11个人。发现的bug主要是:
- 三角函数里面不能有空格,有人写了"sin( x )"就被判非法了
- 括号匹配没处理好
- 求导公式记错了,sin求导还是sin(忘了变cos)
测试方法
第二次作业我沿用了第一次的测试脚本,但做了改进:
- 写了个随机生成函数,能生成带嵌套括号和三角函数的表达式
- 用sympy的expand和simplify让结果更规范,方便对比
- 加了性能测试,看每个用例跑多久
第三次作业
作业要求
在第二次的基础上,增加了自定义函数(比如定义f(x)=x+1)和求导算子dx()。还支持函数调用和嵌套。这是最难的一次,看到题目我第一反应是"这怎么写"。
我是怎么写的
这次我没有重写,而是在第二次作业的基础上加东西:
- 写个Function类,存函数名、参数、函数体
- 写个FunctionCallFactor类,表示调用函数
- 写个DerivativeFactor类,表示dx()
- 改Parser,让他能认识函数定义和dx()语法
- 函数调用求导时,先把参数代进去,再求导
这次写起来比第二次顺手多了,可能是有经验了吧。
代码规模
第三次作业代码规模如下图:
(放SourceMonitor截图,总行数777行,13个类,63个方法)
代码777行,比第二次又多了50%。类有13个,新增的都是跟函数相关的。
类图
第三次作业类图(只看新增的):

新增的这些东西:
- FunctionManager是个单例(其实就是个静态Map),管理所有自定义函数
- Function存函数的信息,call方法做参数替换
- FunctionCallFactor表示调用函数,求导时先展开再求导
- DerivativeFactor表示dx(),这个的求导规则我想了好久
复杂度分析
第三次作业的复杂度分析如下:
(放SourceMonitor的复杂度数据)
方法复杂度(挑几个高的):
| 类 | 方法 | v(G) | 说人话 |
|---|---|---|---|
| Parser | parseFactor | 15 | 因子类型又多了函数调用和dx(),分支更多了 |
| Parser | parseFunctionDef | 8 | 解析函数定义 |
| Function | call | 10 | 参数替换,要遍历整个函数体 |
| FunctionCallFactor | derivative | 8 | 函数调用求导,逻辑比较复杂 |
类复杂度:
| 类 | OCavg | WMC |
|---|---|---|
| Parser | 6.2 | 62 |
| Expression | 3.8 | 27 |
| Term | 4.2 | 25 |
| Function | 3.5 | 14 |
| 其他类 | ❤️ | <10 |
从数据看:
-
Parser类的总复杂度涨到62了,parseFactor的v(G)=15,因子类型越来越多,这个类越来越臃肿。
-
Function.call方法的v(G)=10,参数替换要处理很多种情况,嵌套调用的时候还要递归替换。
-
各个基础Factor类还是保持低复杂度**,这说明接口设计得还行,加新功能不影响老代码。
Bug分析
公测
公测大部分过了,但有一个性能点没拿满。问题是函数嵌套调用时,我没有化简中间结果,导致输出很长。比如f(x)=x+x,调用f(sin(x)),我输出"sin(x)+sin(x)",但更简单的写法是"2*sin(x)"。
互测
互测被人发现了两个bug:
第一个:自定义函数递归调用自己会无限递归。比如f(x)=f(x)+1,我的程序一直调用自己,最后栈溢出了。题目应该是不让递归的,但我没做检查。
第二个:函数参数在函数体里没用上时会出错。比如f(x)=y(y是外面定义的变量),我的替换算法会把y也错误地替换掉。
互测中我hack了8个人,发现的bug:
- 函数作用域搞不清楚,参数和外部变量混了
- dx()嵌套求导算不对
- 多参数函数解析有问题
测试方法
第三次作业继续升级测试:
- 写脚本随机生成函数定义和函数调用
- 专门测试嵌套调用和递归的边界情况
- 和几个同学互跑程序,发现不一致就互相通知,这个方法帮我找到了好几个自己没想到的bug
改进建议
第一次作业的改进
-
多用正则,少自己写判断。我那个checkFormat方法用了一堆charAt,换成正则表达式会简洁很多。
-
把输出单独写一个类。printPoly方法太复杂了,可以单独写个类负责输出化简。
-
早点想以后要加啥功能。第一次作业虽然简单,但如果稍微注意一下设计,第二次也不至于全重写。
第二次作业的改进
-
parseFactor方法太长了,拆小点。可以拆成parsePowerFactor、parseTrigFactor这些小方法。
-
把化简单独弄个模块。toString里的化简逻辑和输出混在一起,分开会更好维护。
-
考虑用访问者模式。现在求导和输出都是Factor接口的方法,以后要加新操作就得改所有Factor类。
第三次作业的改进
-
加递归深度保护。防止无限递归导致栈溢出。
-
加个缓存。同一个函数用同样的参数调用多次的话,可以缓存结果,不用每次都重新算。
-
完善符号表。现在的函数是全局的,如果以后要支持函数里定义函数,就需要更复杂的符号表管理了。
总结
通过这三次作业,我学到了不少东西:
学到的:
- 面向对象设计。从第一次把所有代码塞Main里,到第三次有13个类、清晰的接口继承。虽然过程痛苦,但确实是进步了。
- 递归下降解析。学会了用递归的方式处理嵌套结构,对编译原理有了初步认识。
- 设计模式。实际用了接口、组合模式、单例模式,比光看书理解深多了。
- 自动化测试。搭了一个Python测试框架,能自动生成用例和验证结果,这个以后还能用。
还需要学的:
- 代码重构。知道代码有问题,但有时候不知道怎么改最好,需要多学学。
- 更多设计模式。访问者模式、解释器模式这些只是听说过,还没真正用过。
- 性能分析。这几次只关心对不对,没管性能,以后要学学。
对课程的建议:
- 希望不要讲的这么快,我们对这个编程没那么了解,上课快,只能靠自学,但是精力又有限,还有不懂得,作业难度和量也大。
浙公网安备 33010602011771号