第一单元总结与反思
基于度量分析程序结构
最终作业UML类图
任务拆分
在我的最终作业中,我将整体表达式化简任务拆分为四个子任务,分别为:
- 预处理自定义函数
- 利用CFfactory类分派构建自定义函数CF类构造工作
- 在构建自定义函数CF时,将自定义函数的变量按顺序修改为a、b、c(不修改exp中的x)
- 解析表达式并生成树
- 利用输入的表达式生成词法分析Lexer
- 借助词法分析Lexer,进行语法分析Parser
- 计算表达式树
- 使用Compute类自底而上计算解析好的表达式树
- 优化输出
- 借助计算出的多项式类Polynomial的toStr方法,以及output类中的优化方法,将计算好的多项式按照格式输出
每个类的设计考虑
- Main类:主函数
- CFfactory类:分派构建自定义函数CF类
- CF类:构建自定义函数,将函数中的变量进行预处理
- Lexer类:进行词法分析
- Parser类:进行语法分析
- Factor类:表达式中的因子父类
- Expr类:表达式因子
- Num类:数字因子
- Variable类:变量因子
- Exp类:指数因子
- CFtrue类:自定义函数因子
- Fx类:求导类
- Term类:表达式中的项
- Compute类:用于计算解析好的表达式树(Expr)
- Formula类:计算结果的父类
- Base类:计算后的基础项(
a*x^k*exp(Expr)
) - Polynomial类:完整的计算结果,主要属性由多个Base构成
- Operation类:实现加法、乘法、乘方、求导等计算功能
- Output类:完成对输出的优化
优缺点分析
本单元的整体架构较为清晰,拆分后的任务简明,且具有较高的可拓展性(只增加了几个方法便能够完成第三次作业),但在实现具体任务的过程中,功能类实现较为复杂,耦合度较高,在下面复杂度分析中,大型功能类的复杂度较高。
程序规模与复杂度分析
类名 | 方法数 | 代码总行数 | OCavg | OCmax | WMC |
---|---|---|---|---|---|
Base | 8 | 83 | 2.5 | 11 | 20 |
CF | 3 | 31 | 2 | 4 | 6 |
CFfactory | 1 | 13 | 2 | 2 | 2 |
CFtrue | 6 | 38 | 1.17 | 2 | 7 |
Compute | 3 | 55 | 4 | 8 | 12 |
Exp | 3 | 25 | 1 | 1 | 3 |
Expr | 5 | 24 | 1 | 1 | 5 |
Factor | 1 | 6 | 1.5 | 2 | 6 |
Formula | 1 | 5 | 1 | 1 | 1 |
Fx | 2 | 15 | 1 | 1 | 2 |
Lexer | 4 | 32 | 2 | 4 | 8 |
Main | 1 | 20 | 2 | 2 | 2 |
Num | 3 | 18 | 1 | 1 | 3 |
Operation | 8 | 176 | 5.43 | 13 | 38 |
Output | 2 | 55 | 4.5 | 6 | 9 |
Parser | 8 | 148 | 3.88 | 8 | 31 |
Polynomial | 14 | 163 | 3.14 | 12 | 44 |
Term | 3 | 13 | 1 | 1 | 3 |
Variable | 2 | 15 | 1 | 1 | 2 |
在复杂度方面,大部分类的表现都能在良好的范围内,但承担了计算、分析等的主类的方法平均循环复杂度、类的总循环复杂度较高,如Operation、Output、Parser、Polynomial、Compute等。主要原因可能有以下几点:
- Polynomial类中存在判断是否可优化exp、以及提取出exp内多项式因子的方法,在判断与提取过程中,分支判断与循环较多,复杂度较高
- Output与Polynomial的耦合度较高,Compute与Operation的耦合度较高
- 在Operation类中,合并同类项时循环较多,且条件判断较多,且存在向下递归判断的过程
框架设计体验
第一次框架设计
在第一次作业中,我并没有仔细拆分任务,而是将向下递归建树与计算融为一体,在向下递归到底部时,进行计算,边计算边返回。同时我未将Expr、Term、Factor拆分,而是统一为Polynomial类。整体框架如下图所示。
第二次作业重构
在加入自定义函数与exp后,原本的将解析与计算混在一起的框架不能够满足复杂解析计算的要求,因此我选择了重构。重构后的框架图与最终作业一致(第三次作业只增加了几个方法)
相比较重构前,重构后的框架对任务拆分的更加明确,在不同任务之间耦合度很低,能够分任务修bug,且可拓展性得到了极大的提高。
重构让我的代码架构更加清晰,但由于我将任务拆开,需要二次递归(第一次递归建树,第二次递归计算)。重构是考虑方面主要为如何将任务拆分清晰,个人认为,任务拆分清晰是代码具有良好拓展性的基础。
第三次作业拓展
在第二次作业重构后,重构完的框架可以几乎完全匹配第三次作业的要求,因此我选择在第二次作业的基础上进行迭代。
主要思路如下:
- 对自定义函数嵌套定义方面,由于我本身对自定义函数的处理为字符串替换,即将调用自定义函数时给的参数,添加括号后放入对应位置,将得到的字符串作为一个整体再次进行Expr的解析,因此其本身便能够处理嵌套定义。
- 在求导方面,由于我将任务细分化,在解析阶段,我新增了Fx类,保留求导内部的信息,并递归解析求导内部的Expr,在计算阶段再进行求导运算。而在运算方面,我选择对每一个Base求导,并将每一个Base的求导结果加起来,得到对整个Fx的计算。
再次迭代分析
如果增加需求,如增加三角函数(不考虑化简)等,只需要解析阶段增加三角函数因子,以及计算阶段在Operation类中添加三角函数的计算方法就能够实现简单的三角函数计算,因此程序在拓展性方面表现良好。
Bug分析
在本单元的作业中,由于在本地进行了大量的测试,因此在公测与互测中均为发现bug。
在本地的测试中,在第二次作业调试过程中发现了,调试结果与直接输出结果不一致的情况,具体表现如下:
- 直接运行结果
- 调试结果
而在查询了大量资料与询问助教后,我找到了问题的根源——我在复写方法toString时修改了该变量,而在调试过程中,每调试一步,都会运行一次toString。因此我的变量在调试过程中一直在被修改。
但由于在重构完成并添加完第二次作业要求后,我的代码规模已经较为庞大,我在尝试不在toString中修改变量的过程中,程序不断出错,我最终放弃了复写toString方法,而采用了toStr
如何发现别人的bug
在本单元作业中,我一共发现了如下几个bug点:
- 在对输出优化时,将
exp((-2*x))
输出为exp(-2*x)
或者将exp((-x))
输出为exp(-x)
- 在对输出优化时,将
exp((-2*x))
输出为exp(x)^-2
- 解析函数时,未考虑到表达式因子,忽略了表达式因子后面的指数
个人认为,在发现别人bug方面,更多的依赖于自己代码曾出现过的bug,以及自动测评机的测试。
我在hw2时,自身的代码在未给数据点的测试点出错,在思考问题期间,发现了自己代码存在上述两个问题并成功修改,并用这两个易错点hack了两位同学;而在hw3期间,则是借助同学所搭建的测评机,发现了另一位同学未考虑表达式因子这一错误。
优化分析
在本单元的作业中,虽然侥幸在公测与互测中未被发现bug,但我的代码整体仍较为臃肿,复杂度较高,类之间的耦合度也较高,虽然可拓展,但拓展后代码会越来越臃肿,复杂度会不断增加。因此如果进行优化,则需要对每个较为臃肿的任务进行重构,如优化输出任务等。
同时在输出方面,个人认为应该重写输出方法,在输出方法中拒绝更改变量,使用命令-查询分离原则,杜绝各种莫名的“闹鬼”事件。
心得体会与未来方向
OO的第一单元正式落下帷幕,整体来说,个人认为即使是课改之后的OO,难度依旧不低,一次作业的代码量能够与数据结构的期末大作业相比(甚至更大),但这也确实极大的提升了个人能力。而在本单元中收获最大的可能便是hw3讨论区中同学所分享的命令-查询分离原则,这一原则能够极大的解决我的代码中的大部分bug,向我展示了真正的面向对象。同时,给我留下深刻影响的还有紧张刺激的互测,额,就真的很刺激,可能一段时间没看,房间里大家的成功率都高的可怕。希望自己未来能够落实命令-查询分离原则,杜绝各种奇怪bug与“闹鬼”现象。