OO第一单元总结
一、基于度量的程序结构分析
1.1、程序结构整体分析
1.2、第一次作业
为解析表达式字符串,在参考讨论区同学分享的思路后,决定将表达式拆解为表达式、项和变量三种不同层次的结构。表达式由项通过加减号连接,项由因子通过乘号连接。在建立三种类并实现各自的拆解功能后,即可通过类之间的递归调用完成表达式解析。
由于门槛较低,选择了字符串分析的方法拆解表达式和项,这也为后面出现bug埋下了隐患
由于在拆解项时,项中存在表达式和变量两种因子,所以建立接口Factor,根据工厂模式,在确定拆解为何种因子后再赋予具体类,这样便于项统一管理。
度量
类复杂度
Class | OCavg | OCmax | WMC |
---|---|---|---|
Expr | 3.25 | 7 | 13 |
Main | 9 | 9 | 9 |
Pre | 10 | 28 | 30 |
Term | 3.5 | 8 | 21 |
Variable | 2.4 | 7 | 24 |
方法复杂度
Method | CogC | ev(G) | iv(G) | v(G) |
---|---|---|---|---|
Expr.calcExpression() | 6 | 1 | 4 | 4 |
Expr.getExprVar() | 0 | 1 | 1 | 1 |
Expr.setExpr(String) | 0 | 1 | 1 | 1 |
Expr.setTerm() | 14 | 1 | 8 | 10 |
Main.main(String[]) | 15 | 3 | 8 | 9 |
Pre.Pre(String) | 0 | 1 | 1 | 1 |
Pre.getExpr() | 0 | 1 | 1 | 1 |
Pre.process() | 98 | 15 | 32 | 38 |
Term.calcTerm() | 20 | 1 | 7 | 7 |
Term.getSign() | 0 | 1 | 1 | 1 |
Term.getVars() | 0 | 1 | 1 | 1 |
Term.setSign() | 4 | 1 | 3 | 3 |
Term.setTerm(String) | 0 | 1 | 1 | 1 |
Term.turnToExpr() | 19 | 1 | 8 | 10 |
Variable.Variable(BigInteger, int) | 0 | 1 | 1 | 1 |
Variable.Variable(String) | 13 | 1 | 7 | 7 |
Variable.getCoefficient() | 0 | 1 | 1 | 1 |
Variable.getExponent() | 0 | 1 | 1 | 1 |
Variable.multiply(Variable) | 0 | 1 | 1 | 1 |
Variable.multiplyArray(ArrayList<Variable>, ArrayList<Variable>) | 3 | 1 | 3 | 3 |
Variable.negative() | 0 | 1 | 1 | 1 |
Variable.setCoe(BigInteger) | 0 | 1 | 1 | 1 |
Variable.setIndex(int) | 0 | 1 | 1 | 1 |
Variable.setVar(String) | 13 | 1 | 7 | 7 |
Pre中的process方法认知复杂度过高,这是由于方法中含有多个功能,对表达式进行了反复处理,还有代码复用的情况。当时没能贯彻模块化封装思想,代码前后逻辑关联不强,功能分散,是显著问题。这也导致设计复杂度和圈复杂度居高不下。
由于输出部分直接在Main中实现,预处理未分解全部在Pre中实现,导致这两个类的平均复杂度极高。
类图描述(为突出重点,访问属性等过于简单的基础方法未进行展示)
Pre类的设计,主要是为了便于存放预处理的方法实现,通过调用Pre类来集中完成去空格、去前导零、幂函数展开等操作,更好地满足模块化架构。
Expr类用于储存表达式,同时实现把表达式切割为项的功能。是递归解析的重要部分。核心在于遍历字符串,查找到括号外的加减号时进行切割,得到项,对项进行处理。在整个字符串处理结束后得到一串Variable。
Variable类用于储存最小单元,存放变量的系数和指数,是表达式解析的最终形式。同时两组Variable对象相乘的方法在Variable类中实现。
Factor接口便于Term统一管理组成Term的Expr和Variable对象。
Term类用于储存项,是由表达式拆解为变量的过渡阶段。实现了把项切割为变量和表达式的功能。核心在于遍历字符串,查找到括号外的乘号进行切割,根据有无括号得到表达式因子和变量因子。在解析结束后调用Variable类中的方法进行连乘。
优缺点分析
由于在假期中没能很好的完成pre的练习,导致开学第一周花了大量的时间自学Java,最后磕磕绊绊一知半解,对面向对象的封装理解非常浅显。向同学请教了大量思路,但最后因为时间原因还是没能发现书写时的bug,导致没能通过中测。
优点在于通过结构解析,建立了表达式,项和变量类,为后期迭代打下了基础,这种简单的结构可以支持完成后面的两次作业,避免了大量重构。
缺点在于代码的封装性不好,有部分函数没有封装到类中实现,同时有代码的复用和冗余,也有定义的变量但是没有用到。这说明在动笔之前没有进行完整的构思和思考,对于表达式解析也没有理解透彻。其次,通过字符串解析的方法来分割解析表达式也为后续出现意料之外的情况导致bug留下了隐患。
1.3第二次作业
度量
类复杂度
Class | OCavg | OCmax | WMC |
---|---|---|---|
Expr | 3.75 | 8 | 15 |
Function | 2.71 | 6 | 19 |
Main | 3.33 | 7 | 10 |
Pre | 4.71 | 17 | 66 |
SumFunction | 2 | 4 | 6 |
Term | 3.83 | 10 | 23 |
Variable | 4.37 | 21 | 83 |
方法复杂度
Method | CogC | ev(G) | iv(G) | v(G) |
---|---|---|---|---|
Expr.Expr(String) | 1 | 1 | 2 | 2 |
Expr.calcExpression() | 6 | 1 | 4 | 4 |
Expr.getExprVar() | 0 | 1 | 1 | 1 |
Expr.setTerm() | 17 | 1 | 8 | 11 |
Function.Function(String) | 0 | 1 | 1 | 1 |
Function.divideFormalParameter(String) | 2 | 1 | 3 | 3 |
Function.divideStandard(String) | 3 | 3 | 3 | 3 |
Function.getName() | 0 | 1 | 1 | 1 |
Function.getString(String) | 0 | 1 | 1 | 1 |
Function.replace2(String) | 3 | 1 | 4 | 4 |
Function.setP(String) | 7 | 1 | 6 | 6 |
Main.checkExtraPar(String) | 21 | 5 | 7 | 10 |
Main.deleteExtraPar(String) | 0 | 1 | 1 | 1 |
Main.main(String[]) | 1 | 1 | 2 | 2 |
Pre.Pre(String) | 0 | 1 | 1 | 1 |
Pre.Pre(String, int) | 0 | 1 | 1 | 1 |
Pre.addPar() | 6 | 1 | 5 | 5 |
Pre.deleteAddExponent() | 0 | 1 | 1 | 1 |
Pre.deleteAddSub() | 0 | 1 | 1 | 1 |
Pre.deleteBlank() | 0 | 1 | 1 | 1 |
Pre.deleteLeadingZero() | 17 | 1 | 9 | 11 |
Pre.deleteZeroExponent() | 55 | 9 | 17 | 22 |
Pre.expandExponent() | 67 | 9 | 18 | 23 |
Pre.functionProcess() | 0 | 1 | 1 | 1 |
Pre.getExpr() | 0 | 1 | 1 | 1 |
Pre.process() | 0 | 1 | 1 | 1 |
Pre.replaceFunction() | 26 | 5 | 6 | 9 |
Pre.replaceSum() | 19 | 4 | 5 | 8 |
SumFunction.SumFunction(String) | 0 | 1 | 1 | 1 |
SumFunction.getString() | 4 | 2 | 3 | 4 |
SumFunction.set(String) | 0 | 1 | 1 | 1 |
Term.Term(String) | 0 | 1 | 1 | 1 |
Term.calcTerm() | 20 | 1 | 7 | 7 |
Term.getSign() | 0 | 1 | 1 | 1 |
Term.getVars() | 0 | 1 | 1 | 1 |
Term.setSign() | 4 | 1 | 3 | 3 |
Term.turnToExpr() | 27 | 1 | 10 | 13 |
Variable.Variable() | 0 | 1 | 1 | 1 |
Variable.Variable(BigInteger, int) | 0 | 1 | 1 | 1 |
Variable.Variable(BigInteger, int, HashMap<String, Integer>, HashMap<String, Integer>) | 0 | 1 | 1 | 1 |
Variable.getCoefficient() | 0 | 1 | 1 | 1 |
Variable.getCos() | 0 | 1 | 1 | 1 |
Variable.getExponent() | 0 | 1 | 1 | 1 |
Variable.getSin() | 0 | 1 | 1 | 1 |
Variable.multiply(Variable) | 10 | 1 | 7 | 7 |
Variable.multiplyArray(ArrayList<Variable>, ArrayList<Variable>) | 3 | 1 | 3 | 3 |
Variable.negative() | 0 | 1 | 1 | 1 |
Variable.print(ArrayList<Variable>) | 48 | 7 | 31 | 33 |
Variable.print2(ArrayList<Variable>) | 44 | 3 | 17 | 18 |
Variable.printSinCos(Variable) | 31 | 5 | 10 | 16 |
Variable.setCoe(BigInteger) | 0 | 1 | 1 | 1 |
Variable.setConst(String) | 13 | 1 | 7 | 7 |
Variable.setCos(String) | 1 | 1 | 2 | 2 |
Variable.setIndex(int) | 0 | 1 | 1 | 1 |
Variable.setSin(String) | 1 | 1 | 2 | 2 |
Variable.unite(ArrayList<Variable>) | 7 | 1 | 6 | 6 |
为了保证能够处理情况众多的幂函数,Pre.deleteZeroExponent()和Pre.expandExponent()这两个方法中有大量的条件判断和字符串处理。所以导致难以阅读,设计复杂度高。与字符串处理相关的方法,可读性较差,复杂度较高,诸如Term.turnToExpr(),Pre.replaceSum(),Pre.deleteLeadingZero(),Pre.replaceFunction()等,我认为这是字符串处理不可避免的问题,也是字符串解析的巨大弊端。输出函数由于功能嵌套,也相对更加复杂。
可以看到,经过方法封装,Main与Pre的操作复杂度得到了极大下降。
类图描述
新增类SumFunction,为处理求和函数而设置。在预处理中发现求和函数,则调用SumFunction来完成求和函数的解析,使其展开为表达式形式。
新增类Function,为处理自定义函数而设置。类似SumFunction,发现自定义函数时调用Function来完成替换,展开为表达式形式。
将Pre类中大量处理过程拆解,通过不同方法实现,确保结构清晰。
优缺点分析
优点在于层次清晰,架构简单,所以迭代的工作量并不大。
缺点在于为了实现这样的简单结构,使用了大量的字符串处理,非常容易遗漏特殊情况导致错误,而且字符串分析的代码逻辑非常繁琐。同时,没有为幂函数单独建类,导致在迭代中对幂函数的字符串处理愈发复杂,书写有诸多不便。应该及时重构,增设幂函数类。
1.4第三次作业
度量
类复杂度
Class | OCavg | OCmax | WMC |
---|---|---|---|
Expr | 3.75 | 8 | 15 |
Function | 4.88 | 18 | 39 |
Main | 3 | 7 | 12 |
Pre | 5.36 | 18 | 75 |
SumFunction | 2 | 4 | 6 |
Term | 3.83 | 10 | 23 |
Variable | 3.8 | 14 | 76 |
方法复杂度
Method | CogC | ev(G) | iv(G) | v(G) |
---|---|---|---|---|
Expr.Expr(String) | 1 | 1 | 2 | 2 |
Expr.calcExpression() | 6 | 1 | 4 | 4 |
Expr.getExprVar() | 0 | 1 | 1 | 1 |
Expr.setTerm() | 17 | 1 | 8 | 11 |
Function.Function(String) | 0 | 1 | 1 | 1 |
Function.divideFormalParameter(String) | 2 | 1 | 3 | 3 |
Function.divideStandard(String) | 3 | 3 | 3 | 3 |
Function.getName() | 0 | 1 | 1 | 1 |
Function.getString(String) | 0 | 1 | 1 | 1 |
Function.replace2(String) | 3 | 1 | 4 | 4 |
Function.replace3(String) | 22 | 1 | 11 | 11 |
Function.setP(String) | 38 | 9 | 11 | 20 |
Main.checkExtraPar(String) | 21 | 5 | 7 | 10 |
Main.deleteExtraPar(String) | 0 | 1 | 1 | 1 |
Main.getFunctions() | 0 | 1 | 1 | 1 |
Main.main(String[]) | 3 | 1 | 3 | 3 |
Pre.Pre(String) | 0 | 1 | 1 | 1 |
Pre.Pre(String, int) | 0 | 1 | 1 | 1 |
Pre.addPar() | 38 | 9 | 7 | 15 |
Pre.deleteAddExponent() | 0 | 1 | 1 | 1 |
Pre.deleteAddSub() | 0 | 1 | 1 | 1 |
Pre.deleteBlank() | 0 | 1 | 1 | 1 |
Pre.deleteLeadingZero() | 17 | 1 | 9 | 11 |
Pre.deleteZeroExponent() | 55 | 9 | 17 | 22 |
Pre.expandExponent() | 74 | 9 | 22 | 27 |
Pre.functionProcess() | 0 | 1 | 1 | 1 |
Pre.getExpr() | 0 | 1 | 1 | 1 |
Pre.process() | 0 | 1 | 1 | 1 |
Pre.replaceFunction() | 26 | 5 | 6 | 9 |
Pre.replaceSum() | 19 | 4 | 5 | 8 |
SumFunction.SumFunction(String) | 0 | 1 | 1 | 1 |
SumFunction.getString() | 4 | 2 | 3 | 4 |
SumFunction.set(String) | 0 | 1 | 1 | 1 |
Term.Term(String) | 0 | 1 | 1 | 1 |
Term.calcTerm() | 20 | 1 | 7 | 7 |
Term.getSign() | 0 | 1 | 1 | 1 |
Term.getVars() | 0 | 1 | 1 | 1 |
Term.setSign() | 4 | 1 | 3 | 3 |
Term.turnToExpr() | 27 | 1 | 10 | 13 |
Variable.Variable() | 0 | 1 | 1 | 1 |
Variable.Variable(BigInteger, int) | 0 | 1 | 1 | 1 |
Variable.Variable(BigInteger, int, HashMap<String, Integer>, HashMap<String, Integer>) | 0 | 1 | 1 | 1 |
Variable.getCoefficient() | 0 | 1 | 1 | 1 |
Variable.getCos() | 0 | 1 | 1 | 1 |
Variable.getExponent() | 0 | 1 | 1 | 1 |
Variable.getSin() | 0 | 1 | 1 | 1 |
Variable.judge(String) | 26 | 8 | 16 | 23 |
Variable.multiply(Variable) | 10 | 1 | 7 | 7 |
Variable.multiplyArray(ArrayList<Variable>, ArrayList<Variable>) | 3 | 1 | 3 | 3 |
Variable.negative() | 0 | 1 | 1 | 1 |
Variable.setCoe(BigInteger) | 0 | 1 | 1 | 1 |
Variable.setConst(String) | 13 | 1 | 7 | 7 |
Variable.setCos(String) | 1 | 1 | 2 | 2 |
Variable.setIndex(int) | 0 | 1 | 1 | 1 |
Variable.setSin(String) | 1 | 1 | 2 | 2 |
Variable.setSinCosZero(ArrayList<Variable>) | 11 | 6 | 5 | 6 |
Variable.toSinCosString(Variable, StringBuilder) | 17 | 5 | 8 | 10 |
Variable.toString(ArrayList<Variable>) | 47 | 5 | 20 | 20 |
Variable.unite(ArrayList<Variable>) | 7 | 1 | 6 | 6 |
方法的复杂度延续了第二次作业,由于路径依赖,为了完成任务依然沿用了第二次的大部分代码,没有进行重构,所以字符串处理进一步复杂,复杂度问题也没能改善。
由于预处理环节的增加和对于幂函数求解问题的改进,Pre的平均操作复杂度又上升了。这是没有建构幂函数类的根本问题,很遗憾现在才深刻认识到这一点(被抓了太多bug)。
类图描述
本次迭代不需要新增类,将sin,cos的String部分进行表达式的迭代解析即可。
优缺点分析
第三次作业进行了少量改动,以完成功能为主,同时进行了少量优化。优缺点继承自第二次作业,并无显著差异。
二、bug分析
2.1 第一次作业
由于时间问题没能充分调试,最后没有通过中测。后来经过检查,是少考虑了负数的输出问题,导致无法将Variable类的符号输出,只能输出正号。同时,输出功能实在Main中实现,层次非常混乱,也给调试bug带来了麻烦。
2.2 第二次作业
优化输出时忘记特判cos()最后一项后面不加*,导致sin,cos连乘会多输出一个乘号。该输出方法printSinCos()在Variable类中实现。
读取指数时通过字符读入,只能读入一位,所以无法处理x**11等情况导致错误。该处理方法expandExponent()在Pre类中实现。
特判当项为0时跳过不输出,导致当表达式为0时没有输出。该输出方法print2()在Variable类中实现。
他人bug:
进行随机测试,用几个易错数据进行了检测。
2.3第三次作业
不慎将cos(0)当做0进行了优化。
对于-x和-1的幂次处理函数出了问题,导致无法正确截取进行相乘,出现了符号错误。依然是Pre.expandExponent()的问题。未能建立幂函数类统一处理给我的测试、迭代都造成了极大的困扰,代码复杂度和可读性也深受影响。但是最后也没下决心推到重来,还是依赖了已有路径。起初贪图简单给后面造成了巨大的麻烦,以后一定要贯彻模块化思维,合理建类,加以封装。
他人bug:
检查了其他同学的代码输出优化部分,发现了sin(-1)的边界情况。但没能进一步检查发现更多bug。
三、心得体会
1、关于面向对象思想。经过一次构筑与两次迭代,我对于面向对象的思想有了初步的认识。在上第一堂课时完全不理解面向对象和面向过程有何区别,面对第一次作业完全没有头绪,连同学讨论区的帖子都完全看不懂。经过反复询问和摸索,终于意识到构筑合适的类来进行解析的重要性。将一个事物抽象出来,赋予属性和方法,再通过调用对象来完成任务,这是我从未想过的编程方法。
2、关于模块化与封装。对比第一次和第二次作业,可以很明显地感觉到封装的重要性。第一次作业由于完全不理解面向对象为何物,将很多方法放在了主类中,同时将很多功能放到类的一个方法里。导致功能错杂,架构混乱,常常无法理解上下文的联系,对某段代码的功能认知含混不清,更有甚者连起点和终点的位置都分辨不清。同时有一个功能重复实现的情况。在第二次作业中很好的改善了封装问题。将功能拆分,封装到各个方法中分别实现,调用和修改时方便了很多。一个高内聚低耦合的程序设计可以方便我进行迭代和功能修改,而不必大改架构或者在多个地方调整函数调用。同时,这样的结构也便于阅读分析,封装明确的方法能避免混淆、错用,误修改数据。