BUAA_OO_Unit1 总结

1. 程序结构分析

根据第一单元hw3代码分析本单元程序结构

1.1 架构总览

(包括每个类的属性和方法)
在这里插入图片描述

1.2 类图与类介绍

在这里插入图片描述

1.2.1 MainClass

主类,是整个程序的入口,通过它来调用各个不同类以及类中的方法,从而实现规划的以下功能:

  1. 接收标准输入,包括自定义函数和待处理表达式;
  2. 对自定义函数进行形参和定义式的提取;
  3. 对待解析表达式进行预处理;
  4. 递归下降,实现层次化解析;
  5. 对解析后得到的表达式进行处理;
  6. 打印输出;

MainClass提纲挈领,根据以上的功能,以封装的思想(即只关心输入与输出)为指导思想,来规划进行整个project的设计。
代码规模:30行。

1.2.2 SelfDefineFunc

自定义函数类,接收自定义函数的声明,并对函数进行形参和定义式的分离提取。
代码规模:47行。

属性功能
definition记录函数的具体定义式
mapping记录函数形参及其在形参中所处的位置
方法规模控制分支数目功能
addFunc202加入新的自定义函数
replacePara132用实参替换自定义函数定义式中的形参
1.2.3 PreProcess

预处理类,对待解析的表达式进行预处理,包括:

  • 空白符去除;
  • 替换表达式中的自定义函数;
  • 将连续的’+‘和’-‘替换为一个’+‘和’-';

代码规模:101行

属性功能
input待解析的表达式
方法规模控制分支数目功能
simplify141去除空白符,并将连续的’+‘和’-‘替换为一个’+‘和’-’
generateNewInput288解析输入的表达式
replaceFunc347获得替换自定义函数后的表达式
getInput31获得预处理后的表示式
1.2.4 Lexer

词法分析类,控制光标的移动,解析当前的字符。
代码规模:70行。

属性功能
input待解析的表达式
pos光标所在字符串的位置
curToken目前解析得到的字符串
方法规模控制分支数目功能
peek31获得curToken
getNumber103解析数字字符串,将其转化为数字
next4415控制光标的移动,并修改curToken的值
1.2.5 Parser

语法解析类,递归下降,对表达式进行解析。
代码规模:221行。

属性功能
lexer传入的构造的词法解析对象
方法规模控制分支数目功能
parseExpr336解析表达式
parseTerm317解析项
parseFactor529解析因子
repeatAdd338遇到’^'时,转化为多次添加该因子
repeatAddExprFactor4913表达式遇到’^'时,转化为多次添加该因子,此外该方法还对只含一个Term的表达式因子进行了优化
1.2.6 Factor

因子接口类。
代码规模:7行。

方法控制分支数目功能
deepClone1深拷贝
derive1求导
1.2.7 Expr

表达式类。
代码规模:270行。

属性功能
hasProcessed标记该因子是否处理过
result记录表达式处理后的结果
terms该表达式所拥有的Term
方法规模控制分支数目功能
getResult31获得result这个属性
getTerms31获得terms这个属性
addTerm52向属性terms中加入元素
deepClone72深拷贝
derive132求导
process92对解析完的表达式进行处理
unfoldBracket62展开括号
containSimilar83判断是否result中是否包含类似基础元
deleteZero93删除系数为0的基础元
roughSame93粗略判断基础元相同
printNotOne165基础元系数不是1和-1的打印情况
printExpFunc216打印指数函数
uniteSimilar215合并类似基础元
compareExpFunc5320粗略判断基础元所包含的指数函数相同
printBasicEle5414打印基础元
1.2.8 Term

项类。
代码规模:127行。

属性功能
symbol标记是加上还是减去该项
factors记录该项所拥有的因子
basicElements记录该项所拥有的基础元
方法规模控制分支数目功能
addFactor31向属性factors中加入因子
getBasicElements31获得basicElements这个属性
getFactors31获得factors这个属性
setSymbol31修改属性symbol
deepClone102深拷贝
derive184求导
open295展开括号
combine247合并常数与合并自变量
1.2.9 Constant

常数因子类。
代码规模:28行。

属性功能
num该常数因子的值
方法规模控制分支数目功能
deepClone41深拷贝
derive51求导
getvalue31获得该常数因子的值
1.2.10 Variable

变量因子类。
代码规模:25行。

属性功能
num该变量因子x的幂次
方法规模控制分支数目功能
deepClone41深拷贝
derive92求导
getvalue31获得该变量因子的幂次
1.2.11 Exponential

指数函数类。
代码规模:23行。

属性功能
factor该指数函数括号中的因子
方法规模控制分支数目功能
deepClone41深拷贝
derive91求导
getFactor31获得该指数函数括号中的因子
1.2.12 BasicElement

基础元类,在该project中是最小的单元,任何一个表达式最终都可以转化为符合输出要求的若干基础元之和。
代码规模:66行。

属性功能
constant该基础元的系数
power该基础元的变量幂次
expFunc该基础元的指数函数的括号中的因子
方法规模控制分支数目功能
getConstant31获得constant这个属性
getPower31获得power这个属性
getExpFunc31获得expFunc这个属性
uniteExpFunc3613将expFunc中的因子合并为一个因子

1.3 类的内聚及耦合情况分析

使用IDEA中的MetricsReloaded插件进行度量,度量指标的含义如下:

  • average operation complexity:平均操作复杂度;
  • Maximum operation complexity:最大操作复杂性;
  • Weighted method complexity:加权方法复杂性;
classOCavgOCmaxWMC
expr.BasicElement2.48.012.0
expr.Constant1.01.04.0
expr.Exponential1.01.04.0
expr.Expr4.0615.065.0
expr.Term2.47.024.0
expr.Variable1.252.05.0
Lexer3.7511.015.0
MainClass2.02.02.0
Parser7.1713.043.0
PreProcess3.26.016.0
SelfDefineFunc1.672.05.0
Total195.0
Average3.156.1817.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中,加入了自定义函数和指数函数:

  • 针对自定义函数,我设计了相应的类来存储,同时建立预处理类,通过字符串替换解决自定义函数问题;
  • 针对指数函数,只需新增指数函数因子类即可,同时在lexerparser类的递归下降解析过程中加入新的分支;

此外,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数据的时候按照由简到繁的策略,并且由于是迭代作业,仍然测试前几次作业的样例:

  1. 测试不同因子的功能,例如只输入指数函数因子、常数因子等;
  2. 测试不同因子的组合,例如各种不同因子相乘;
  3. 测试嵌套括号的实现,包括exp(<因子>)的因子中嵌套括号的实现;
  4. 测试加入自定义函数后的预处理,重点测试有3个形参且实参为各种因子的情况;
  5. 针对求导的测试,仍然先测试每个因子的求导功能,之后测试不同因子的组合的求导,最后测试嵌套求导因子的情况。

在设计测试样例时我没有结合被测程序,原因在于同学们的思路都存在一定差异,同时属性和方法的命名习惯不同,加上一些同学的代码缺少注释,阅读起来较为困难。

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修复开始的时间。
  • 17
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值