1. 程序结构分析
根据第一单元hw3代码分析本单元程序结构
1.1 架构总览
(包括每个类的属性和方法)
1.2 类图与类介绍
1.2.1 MainClass
主类,是整个程序的入口,通过它来调用各个不同类以及类中的方法,从而实现规划的以下功能:
- 接收标准输入,包括自定义函数和待处理表达式;
- 对自定义函数进行形参和定义式的提取;
- 对待解析表达式进行预处理;
- 递归下降,实现层次化解析;
- 对解析后得到的表达式进行处理;
- 打印输出;
MainClass提纲挈领,根据以上的功能,以封装的思想(即只关心输入与输出)为指导思想,来规划进行整个project的设计。
代码规模:30行。
1.2.2 SelfDefineFunc
自定义函数类,接收自定义函数的声明,并对函数进行形参和定义式的分离提取。
代码规模:47行。
属性 | 功能 |
---|---|
definition | 记录函数的具体定义式 |
mapping | 记录函数形参及其在形参中所处的位置 |
方法 | 规模 | 控制分支数目 | 功能 |
---|---|---|---|
addFunc | 20 | 2 | 加入新的自定义函数 |
replacePara | 13 | 2 | 用实参替换自定义函数定义式中的形参 |
1.2.3 PreProcess
预处理类,对待解析的表达式进行预处理,包括:
- 空白符去除;
- 替换表达式中的自定义函数;
- 将连续的’+‘和’-‘替换为一个’+‘和’-';
代码规模:101行
属性 | 功能 |
---|---|
input | 待解析的表达式 |
方法 | 规模 | 控制分支数目 | 功能 |
---|---|---|---|
simplify | 14 | 1 | 去除空白符,并将连续的’+‘和’-‘替换为一个’+‘和’-’ |
generateNewInput | 28 | 8 | 解析输入的表达式 |
replaceFunc | 34 | 7 | 获得替换自定义函数后的表达式 |
getInput | 3 | 1 | 获得预处理后的表示式 |
1.2.4 Lexer
词法分析类,控制光标的移动,解析当前的字符。
代码规模:70行。
属性 | 功能 |
---|---|
input | 待解析的表达式 |
pos | 光标所在字符串的位置 |
curToken | 目前解析得到的字符串 |
方法 | 规模 | 控制分支数目 | 功能 |
---|---|---|---|
peek | 3 | 1 | 获得curToken |
getNumber | 10 | 3 | 解析数字字符串,将其转化为数字 |
next | 44 | 15 | 控制光标的移动,并修改curToken的值 |
1.2.5 Parser
语法解析类,递归下降,对表达式进行解析。
代码规模:221行。
属性 | 功能 |
---|---|
lexer | 传入的构造的词法解析对象 |
方法 | 规模 | 控制分支数目 | 功能 |
---|---|---|---|
parseExpr | 33 | 6 | 解析表达式 |
parseTerm | 31 | 7 | 解析项 |
parseFactor | 52 | 9 | 解析因子 |
repeatAdd | 33 | 8 | 遇到’^'时,转化为多次添加该因子 |
repeatAddExprFactor | 49 | 13 | 表达式遇到’^'时,转化为多次添加该因子,此外该方法还对只含一个Term的表达式因子进行了优化 |
1.2.6 Factor
因子接口类。
代码规模:7行。
方法 | 控制分支数目 | 功能 |
---|---|---|
deepClone | 1 | 深拷贝 |
derive | 1 | 求导 |
1.2.7 Expr
表达式类。
代码规模:270行。
属性 | 功能 |
---|---|
hasProcessed | 标记该因子是否处理过 |
result | 记录表达式处理后的结果 |
terms | 该表达式所拥有的Term |
方法 | 规模 | 控制分支数目 | 功能 |
---|---|---|---|
getResult | 3 | 1 | 获得result这个属性 |
getTerms | 3 | 1 | 获得terms这个属性 |
addTerm | 5 | 2 | 向属性terms中加入元素 |
deepClone | 7 | 2 | 深拷贝 |
derive | 13 | 2 | 求导 |
process | 9 | 2 | 对解析完的表达式进行处理 |
unfoldBracket | 6 | 2 | 展开括号 |
containSimilar | 8 | 3 | 判断是否result中是否包含类似基础元 |
deleteZero | 9 | 3 | 删除系数为0的基础元 |
roughSame | 9 | 3 | 粗略判断基础元相同 |
printNotOne | 16 | 5 | 基础元系数不是1和-1的打印情况 |
printExpFunc | 21 | 6 | 打印指数函数 |
uniteSimilar | 21 | 5 | 合并类似基础元 |
compareExpFunc | 53 | 20 | 粗略判断基础元所包含的指数函数相同 |
printBasicEle | 54 | 14 | 打印基础元 |
1.2.8 Term
项类。
代码规模:127行。
属性 | 功能 |
---|---|
symbol | 标记是加上还是减去该项 |
factors | 记录该项所拥有的因子 |
basicElements | 记录该项所拥有的基础元 |
方法 | 规模 | 控制分支数目 | 功能 |
---|---|---|---|
addFactor | 3 | 1 | 向属性factors中加入因子 |
getBasicElements | 3 | 1 | 获得basicElements这个属性 |
getFactors | 3 | 1 | 获得factors这个属性 |
setSymbol | 3 | 1 | 修改属性symbol |
deepClone | 10 | 2 | 深拷贝 |
derive | 18 | 4 | 求导 |
open | 29 | 5 | 展开括号 |
combine | 24 | 7 | 合并常数与合并自变量 |
1.2.9 Constant
常数因子类。
代码规模:28行。
属性 | 功能 |
---|---|
num | 该常数因子的值 |
方法 | 规模 | 控制分支数目 | 功能 |
---|---|---|---|
deepClone | 4 | 1 | 深拷贝 |
derive | 5 | 1 | 求导 |
getvalue | 3 | 1 | 获得该常数因子的值 |
1.2.10 Variable
变量因子类。
代码规模:25行。
属性 | 功能 |
---|---|
num | 该变量因子x的幂次 |
方法 | 规模 | 控制分支数目 | 功能 |
---|---|---|---|
deepClone | 4 | 1 | 深拷贝 |
derive | 9 | 2 | 求导 |
getvalue | 3 | 1 | 获得该变量因子的幂次 |
1.2.11 Exponential
指数函数类。
代码规模:23行。
属性 | 功能 |
---|---|
factor | 该指数函数括号中的因子 |
方法 | 规模 | 控制分支数目 | 功能 |
---|---|---|---|
deepClone | 4 | 1 | 深拷贝 |
derive | 9 | 1 | 求导 |
getFactor | 3 | 1 | 获得该指数函数括号中的因子 |
1.2.12 BasicElement
基础元类,在该project中是最小的单元,任何一个表达式最终都可以转化为符合输出要求的若干基础元之和。
代码规模:66行。
属性 | 功能 |
---|---|
constant | 该基础元的系数 |
power | 该基础元的变量幂次 |
expFunc | 该基础元的指数函数的括号中的因子 |
方法 | 规模 | 控制分支数目 | 功能 |
---|---|---|---|
getConstant | 3 | 1 | 获得constant这个属性 |
getPower | 3 | 1 | 获得power这个属性 |
getExpFunc | 3 | 1 | 获得expFunc这个属性 |
uniteExpFunc | 36 | 13 | 将expFunc中的因子合并为一个因子 |
1.3 类的内聚及耦合情况分析
使用IDEA中的MetricsReloaded插件进行度量,度量指标的含义如下:
- average operation complexity:平均操作复杂度;
- Maximum operation complexity:最大操作复杂性;
- Weighted method complexity:加权方法复杂性;
class | OCavg | OCmax | WMC |
---|---|---|---|
expr.BasicElement | 2.4 | 8.0 | 12.0 |
expr.Constant | 1.0 | 1.0 | 4.0 |
expr.Exponential | 1.0 | 1.0 | 4.0 |
expr.Expr | 4.06 | 15.0 | 65.0 |
expr.Term | 2.4 | 7.0 | 24.0 |
expr.Variable | 1.25 | 2.0 | 5.0 |
Lexer | 3.75 | 11.0 | 15.0 |
MainClass | 2.0 | 2.0 | 2.0 |
Parser | 7.17 | 13.0 | 43.0 |
PreProcess | 3.2 | 6.0 | 16.0 |
SelfDefineFunc | 1.67 | 2.0 | 5.0 |
Total | 195.0 | ||
Average | 3.15 | 6.18 | 17.73 |
由表格数据可知:
- BasicElement类、SelfDefineFunc类和除Expr外的因子类的内聚情况较好;
- PreProcess类调用了自定义函数类,所以耦合度较高;
- parser类调用了lexer类,同时在里面出现了所有类型因子的声明,所以耦合度较高;
- Expr类中包含了括号展开、基础元合并化简和最后的打印输出,调用了较多其他因子类和基础元类中的方法,因子耦合度较高。
1.4 自我点评
优点:
- 整体思路明确,将整个project清晰地划分为预处理、解析、展开括号、优化、打印输出这五部分;
- 封装性好,对重复出现的代码都进行了封装,抽象为方法;
- 注释完善,对大部分方法都进行了注释,说明了该方法的作用以及注意事项。
缺点:
- 个别类的功能分配不准确,例如将展开括号和优化放在了Expr类,而从逻辑上来看,Expr类应该是一个因子,不应该把对表达式的处理放在该类里面;
- 一些方法写的过于复杂,在整个project中有几个方法达到了50多行,这些方法内部使用了较多的if进行特判,很多分支之间其实出现了交叉,没有划分清楚;
- 优化较差,尤其是在判断两个
exp(<因子>)
是否相等时,我的处理是如果该因子是表达式因子就不进行判断,直接认为不相同,导致最后输出长度非常差,性能分非常低。
2 架构设计体验
2.1 hw1
在第一次作业的设计中,我参考了oolens公众号发布的递归下降思路,沿用了lexer和parser进行解析的思路.同时采用了基础元的设计,基础元由系数和自变量x的幂次组成,若不含自变量x,则幂次为0,这样基础元就是最小的元素,在解析完后对表达式进行处理,将其转化为若干个基础元之和,然后进行合并并输出。
2.2 hw2
在hw2中,加入了自定义函数和指数函数:
- 针对自定义函数,我设计了相应的类来存储,同时建立预处理类,通过字符串替换解决自定义函数问题;
- 针对指数函数,只需新增指数函数因子类即可,同时在
lexer
和parser
类的递归下降解析过程中加入新的分支;
此外,hw2中还允许嵌套括号,这一点的实现较为简单,只需要在解析到表达式因子时再次调用parseExpr方法即可。
2.3 hw3
在hw3中,支持了自定义函数嵌套定义和求导因子:
- 针对自定义函数的嵌套定义,我的实现是判断字符串中是否还含有
f、g、h
,如果有就再次调用替换自定义函数的方法,直到不含有自定义函数; - 针对求导因子,我沿用了第二次试验的设计思路,首先在每一个因子类中都重写求导方法,之后在parser类的解析过程,加入新的求导因子分支即可。
2.4 预设迭代情景
- 常数因子支持指数运算,例如
2^3
,在我的实现中已经支持了该种情况,因为我针对^
的处理是重复添加该符号前的因子的深拷贝; - 自定义函数的定义可以含有求导因子,这一点在我的实现中也已经支持,因为我是对自定义函数进行字符串替换,对替换后的、不含自定义函数的表达式进行解析,因此自定义函数的定义含有求导因子也可以支持。
3 分析程序的bug
3.1 hw1
bug1:根据表达式的层次化定义,允许出现有符号整数,当一个负数前面为*
例如5*-3
时,-3
无法正常读取,在lexer的词法分析时忽略了这一类情况,根本原因在于没有严格按照层次化定义进行解析,因此导致了情况的遗漏;
bug2:当减去一个Term时误操作为了加,原因在于处理加减的方法中存在较多重复代码,我在不同控制分支之间使用了多次复制粘贴,最终导致了在减的这一分支出现错误,要避免这一类错误应该对重复代码进行封装,只关心方法的输入与输出,这样能大幅避免这一类细节错误。
3.2 hw2
bug1:没有认真阅读自定义函数的层次化定义,忽略了自定义函数内也可以包含空白项;
bug2:时间性能差,具体改进在后文介绍。
3.3 分析
出现bug的都是规模较大、控制分支多的方法,因为情况较多,且不同情况只存在较小的例如加或者减、1或者-1的差异,导致在其中某一个控制分支中出现纰漏,要避免这一类bug,可以对不同控制分支的内容进行封装,严格测试封装方法否输入输出,这样可以极大地避免这一类bug,同时方法的复杂度也会降低。
4 分析hack别人程序的策略
我在构建hack数据的时候按照由简到繁的策略,并且由于是迭代作业,仍然测试前几次作业的样例:
- 测试不同因子的功能,例如只输入指数函数因子、常数因子等;
- 测试不同因子的组合,例如各种不同因子相乘;
- 测试嵌套括号的实现,包括exp(<因子>)的因子中嵌套括号的实现;
- 测试加入自定义函数后的预处理,重点测试有3个形参且实参为各种因子的情况;
- 针对求导的测试,仍然先测试每个因子的求导功能,之后测试不同因子的组合的求导,最后测试嵌套求导因子的情况。
在设计测试样例时我没有结合被测程序,原因在于同学们的思路都存在一定差异,同时属性和方法的命名习惯不同,加上一些同学的代码缺少注释,阅读起来较为困难。
5 优化
5.1 时间与空间性能
- 在第二次作业中,
(((((((((((x^8)^8)^8)^8)^8)^8)^8)^8)^8)^8)^8)^8
这一样例我出现了超内存,这是因为我对^
的处理是重复添加该因子,同时在这一样例中存在较多表达式因子,因此这一样例我使用的内存呈指数增长,针对这一点,我对表达式因子进行了优化,当表达式因子可以简化时例如(x^2)
可以简化为变量因子x^2,
后者使用的内存远远小于前者,这样就节省了大量的内存资源; - 当遇到系数
0
时,例如exp(x)*x^3*0*35*dx(x+6)
,之后我们其实并不再关心该Term,因为存在乘0,因此我在解析过程中加入判断,当遇到系数0
时,直接丢弃该Term,这样大大地节省了空间资源,并且加快了程序运行速度。
5.2 输出长度优化
- 当打印一个系数为
1
的基础元时,如果该基础元的变量幂次或者指数函数存在,则系数1
可以省略; - 变量x的幂次为
1
,即x^1
时简化为x
; - 当
exp(<因子>)
的因子是表达式因子时,判断该表达式因子是否可以简化,例如exp((x^3))
可以简化为exp(x^3)
; - 合并变量幂次和指数函数相同的基础元。
5.3 优化自评
在我的优化中,采用了较多的特判,即列举了所有的控制分支,并对每一个控制分支进行优化,这样导致我的代码在优化部分变得非常臃肿,我认为导致这一点的原因是我的设计出现问题,在我的设计中,解析与性能优化完全独立,在解析的过程中只关心结果是否正确,而忽略了解析的结果是否便于后续处理,最终得到的解析结果存在非常多的冗余项,例如exp(0)
等,因此我应该在对解析的设计中就考虑简化解析的结果,比如exp(0)
优化为1
,这样才能大大方便后续的处理,而不需要再采用特判来进行优化。
6 心得体会
- 上学期的OOpre确实对本学期的OO产生了极大的帮助,如果上学期没上OOpre,对git的使用、java的基础语法等将不熟悉,从hw1开始就将寸步难行;
- 第一单元的作业我进行了大量的封装,将重复的代码都封装成了方法,这样下来在整个第一单元我都没有遇到上学期OOpre经常遇到的方法超过60行的风格错误,并且由于进行了封装,代码的可读性也大大提高,在几次迭代debug过程中也能快速的定位到bug;
- 对代码的迭代应该侧重于增加而非修改,例如在hw2新增了自定义函数,我选择在预处理过程中就对自定义函数进行替换,这样就不需要修改后续的词法和语法解析类,减少了因为修改带来bug的可能,同时出现新的bug也能知道就是出现在预处理类中,方便快速定位bug。
- 进行模块化设计,功能划分要明确清晰,例如这个类进行预处理,另外一个类进行解析等,这样极大地方便了我进行debug,在我出现bug时,我一般是对不同的模块独立进行测试,能够快速定位到是哪一个部分出现bug,之后再去进行准确定位。
7 未来方向
- 对一些修改量较大的迭代可以再hw1就进行提示,例如幂次可能超过int范围,这一点在hw1无需考虑,而从hw2开始需要考虑,这一点导致我在hw2中围绕int和BigInteger进行了大量的修改;
- 课程时间安排可以进行一些调整,因为在周一下午7点就发布了本周作业,而上周作业的bug修复还没开始,如果我知道了自己上周作业存在bug,总是希望在对bug进行了修复,确保前一次作业的正确之后再进行下一次作业的迭代,因此我建议可以适当提前bug修复开始的时间。