各个类的结构
类的设计
类图
使用IDEA的插件PlantUML Integration,编写UML代码生成类图。
类的作用
Fac
因子类,用作构建抽象层次,利用多态对所有因子进行统一管理Cons
常数类Var
幂函数类Function
自定义函数类TrigFunc
三角函数类Term
项类Expr
表达式类Lexer
字符串读取器Parser
语法分析器Main
主体
复杂度
4种复杂度:
- 认知复杂度(
CogC
):
认知复杂度描述了理解一段代码的难度;认知复杂度越高,代码的线性程度越低,也越不容易理解。会增加认知复杂度的成分包括而不限于:
(1)打断线性逻辑的语句,比如控制流(如if
,while
)和连续使用的不同的逻辑运算符(如a||b&&c
)都会使认知复杂度++
(2)打断线性逻辑的嵌套语句。每嵌套一层,该层内的所有会增加认知复杂度的语句对认知复杂度的贡献会累加1
(3)递归 - 圈复杂度(
v(G)
):
用于描述一段代码结构的复杂程度,圈复杂度越高,程序维护起来越难。
计算公式:V(G)=e-n+2p
。其中,e表示控制流图中边的数量;n表示控制流图中节点的数量;p表示控制流图的分支数。
对比认知复杂度,圈复杂度完全依赖控制流图,其特点是switch-case语句提供很多路径,因此对圈复杂度的贡献很高。 - 设计复杂度(
iv(G)
):
模块设计复杂度是从模块流程图中移去那些不包含调用子模块的判定和循环结构后得出的圈复杂度;其描述的是模块间互相调用的多少,即模块间耦合程度,设计复杂度越高,模块间耦合度越高,维护起来越困难。 - 基本圈复杂度(
ev(G)
):
基本圈复杂度描述一段代码的非结构化程度,数值越高,代码越难维护。
各个方法的复杂度
利用IEDA的复杂度分析工具MatricsReloader对各个类的方法进行复杂度分析,其中高亮代表复杂度较高:
认知复杂度 | 基本圈复杂度 | 设计复杂度 | 圈复杂度 | |
---|---|---|---|---|
Method | CogC | ev(G) | iv(G) | v(G) |
Main.main(String[]) | 6 | 1 | 4 | 4 |
Main.simplifySign(String) | 0 | 1 | 1 | 1 |
factors.Cons.Cons(char, BigInteger) | 0 | 1 | 1 | 1 |
factors.Cons.calSign() | 0 | 1 | 1 | 1 |
factors.Cons.copy() | 0 | 1 | 1 | 1 |
factors.Cons.differentiate(char) | 0 | 1 | 1 | 1 |
factors.Cons.getNumber() | 0 | 1 | 1 | 1 |
factors.Cons.getSign() | 0 | 1 | 1 | 1 |
factors.Cons.isHomogeneous(Fac) | 2 | 2 | 1 | 2 |
factors.Cons.isequel(Fac) | 3 | 2 | 3 | 3 |
factors.Cons.multSign(char) | 4 | 1 | 1 | 3 |
factors.Cons.setNumber(BigInteger) | 0 | 1 | 1 | 1 |
factors.Cons.setSign(char) | 0 | 1 | 1 | 1 |
factors.Cons.toString() | 0 | 1 | 1 | 1 |
factors.Expr.Expr() | 0 | 1 | 1 | 1 |
factors.Expr.addOneTermOfExpr(Term) | 1 | 1 | 2 | 2 |
factors.Expr.addTerm(Term) | 42 | 9 | 9 | 19 |
factors.Expr.calSign() | 0 | 1 | 1 | 1 |
factors.Expr.copy() | 1 | 1 | 2 | 2 |
factors.Expr.differentiate(char) | 1 | 1 | 2 | 2 |
factors.Expr.getSign() | 0 | 1 | 1 | 1 |
factors.Expr.getTerms() | 0 | 1 | 1 | 1 |
factors.Expr.isHomogeneous(Fac) | 14 | 6 | 4 | 6 |
factors.Expr.isequel(Fac) | 14 | 6 | 4 | 6 |
factors.Expr.multSign(char) | 4 | 1 | 1 | 3 |
factors.Expr.setSign(char) | 0 | 1 | 1 | 1 |
factors.Expr.slwAddTerm(Term) | 0 | 1 | 1 | 1 |
factors.Expr.toString() | 1 | 1 | 2 | 2 |
factors.Function.Function() | 0 | 1 | 1 | 1 |
factors.Function.Function(char, char, String, ArrayList < Character >, ArrayList< Fac >, int) | 0 | 1 | 1 | 1 |
factors.Function.calSign() | 0 | 1 | 1 | 1 |
factors.Function.copy() | 0 | 1 | 1 | 1 |
factors.Function.differentiate(char) | 1 | 1 | 2 | 2 |
factors.Function.getName() | 0 | 1 | 1 | 1 |
factors.Function.getSign() | 0 | 1 | 1 | 1 |
factors.Function.isHomogeneous(Fac) | 0 | 1 | 1 | 1 |
factors.Function.isequel(Fac) | 0 | 1 | 1 | 1 |
factors.Function.multSign(char) | 4 | 1 | 1 | 3 |
factors.Function.setFactors(ArrayList< Fac >) | 0 | 1 | 1 | 1 |
factors.Function.setName(char) | 0 | 1 | 1 | 1 |
factors.Function.setPower(int) | 0 | 1 | 1 | 1 |
factors.Function.setSign(char) | 0 | 1 | 1 | 1 |
factors.Function.setVariables(ArrayList< Character >) | 0 | 1 | 1 | 1 |
factors.Function.toString() | 24 | 6 | 8 | 9 |
factors.Term.Term() | 0 | 1 | 1 | 1 |
factors.Term.Term(Term) | 1 | 1 | 2 | 2 |
factors.Term.addExpr(Fac) | 9 | 1 | 5 | 5 |
factors.Term.addFac(Fac) | 33 | 8 | 15 | 15 |
factors.Term.calSign() | 5 | 1 | 2 | 5 |
factors.Term.clrFac() | 0 | 1 | 1 | 1 |
factors.Term.copy() | 1 | 1 | 2 | 2 |
factors.Term.differentiate(char) | 6 | 1 | 4 | 4 |
factors.Term.getFacs() | 0 | 1 | 1 | 1 |
factors.Term.getSign() | 0 | 1 | 1 | 1 |
factors.Term.isHomogeneous(Term) | 44 | 14 | 10 | 19 |
factors.Term.isequel(Term) | 9 | 6 | 3 | 6 |
factors.Term.multSign(char) | 4 | 1 | 1 | 3 |
factors.Term.setSign(char) | 0 | 1 | 1 | 1 |
factors.Term.toString() | 38 | 5 | 13 | 15 |
factors.TrigFunc.TrigFunc() | 0 | 1 | 1 | 1 |
factors.TrigFunc.TrigFunc(char, String, Fac, int) | 0 | 1 | 1 | 1 |
factors.TrigFunc.calSign() | 0 | 1 | 1 | 1 |
factors.TrigFunc.copy() | 0 | 1 | 1 | 1 |
factors.TrigFunc.differentiate(char) | 5 | 2 | 4 | 5 |
factors.TrigFunc.getFac() | 0 | 1 | 1 | 1 |
factors.TrigFunc.getName() | 0 | 1 | 1 | 1 |
factors.TrigFunc.getPower() | 0 | 1 | 1 | 1 |
factors.TrigFunc.getSign() | 0 | 1 | 1 | 1 |
factors.TrigFunc.isHomogeneous(Fac) | 5 | 3 | 3 | 4 |
factors.TrigFunc.isequel(Fac) | 5 | 3 | 4 | 5 |
factors.TrigFunc.multSign(char) | 4 | 1 | 1 | 3 |
factors.TrigFunc.setFac(Fac) | 0 | 1 | 1 | 1 |
factors.TrigFunc.setName(String) | 0 | 1 | 1 | 1 |
factors.TrigFunc.setPower(int) | 0 | 1 | 1 | 1 |
factors.TrigFunc.setSign(char) | 0 | 1 | 1 | 1 |
factors.TrigFunc.toString() | 8 | 2 | 6 | 8 |
factors.Var.Var(char, char, int) | 0 | 1 | 1 | 1 |
factors.Var.calSign() | 0 | 1 | 1 | 1 |
factors.Var.copy() | 0 | 1 | 1 | 1 |
factors.Var.differentiate(char) | 3 | 1 | 2 | 3 |
factors.Var.getPower() | 0 | 1 | 1 | 1 |
factors.Var.getSign() | 0 | 1 | 1 | 1 |
factors.Var.getVariable() | 0 | 1 | 1 | 1 |
factors.Var.isHomogeneous(Fac) | 4 | 3 | 3 | 4 |
factors.Var.isequel(Fac) | 5 | 3 | 4 | 5 |
factors.Var.multSign(char) | 4 | 1 | 1 | 3 |
factors.Var.setPower(int) | 0 | 1 | 1 | 1 |
factors.Var.setSign(char) | 0 | 1 | 1 | 1 |
factors.Var.setVariable(char) | 0 | 1 | 1 | 1 |
factors.Var.toString() | 2 | 3 | 1 | 3 |
tools.Lexer.Lexer() | 0 | 1 | 1 | 1 |
tools.Lexer.Lexer(String) | 0 | 1 | 1 | 1 |
tools.Lexer.addFactors(ArrayList< Character >) | 0 | 1 | 1 | 1 |
tools.Lexer.addfDefination(String) | 0 | 1 | 1 | 1 |
tools.Lexer.addfName(Character) | 0 | 1 | 1 | 1 |
tools.Lexer.back() | 2 | 2 | 2 | 3 |
tools.Lexer.getCharNow() | 0 | 1 | 1 | 1 |
tools.Lexer.getFactors() | 0 | 1 | 1 | 1 |
tools.Lexer.getFdefinations() | 0 | 1 | 1 | 1 |
tools.Lexer.getFnames() | 0 | 1 | 1 | 1 |
tools.Lexer.getLen() | 0 | 1 | 1 | 1 |
tools.Lexer.getPos() | 0 | 1 | 1 | 1 |
tools.Lexer.next() | 2 | 1 | 2 | 3 |
tools.Lexer.resetPos() | 0 | 1 | 1 | 1 |
tools.Lexer.setInput(String) | 0 | 1 | 1 | 1 |
tools.Parser.Parser(Lexer) | 0 | 1 | 1 | 1 |
tools.Parser.assignBrackets(Term, char) | 27 | 4 | 10 | 10 |
tools.Parser.assignCons(char) | 9 | 3 | 4 | 7 |
tools.Parser.assignExpr() | 6 | 4 | 3 | 4 |
tools.Parser.assignFac(Term, char) | 23 | 8 | 11 | 12 |
tools.Parser.assignFunction(Term, char, char) | 8 | 3 | 6 | 6 |
tools.Parser.assignTerm() | 3 | 1 | 3 | 3 |
tools.Parser.assignTrig(Term, char, char) | 7 | 1 | 4 | 4 |
tools.Parser.assignVar(Term, char) | 9 | 4 | 4 | 4 |
tools.Parser.termMulExpr(Term, Expr) | 14 | 1 | 6 | 6 |
方法复杂度数据分析
复杂度较高的类
分析器 Parser
巨型方法
-
用于解析元素的方法
public Fac assignFac(Term term, char sign)
由于让包括表达式、自定义函数、三角函数、幂函数、常数在内的类都实现了Fac接口,也就是将其通通归类为元素,在利用抽象层次设计思想对每个元素进行解析的时候要调用统一的分析方法。
分析结构发现,由于该方法调用了大量方法,包括对表达式、自定义函数、三角函数等的解析方法,并包含了大量的if
语句导致程序流程图比较复杂,耦合度较高,所以不论是哪种复杂度都很高。 -
用于解析表达式的方法
public Fac assignBrackets(Term term, char sign)
该方法专门用来解析带括号的表达式,在
assignFac
中被调用。
该方法的圈复杂度不高,但是认知复杂度很高,原因是该方法中包含了很多嵌套的控制流语句,这就导致尽管程序流程图中的边不太多,但是执行逻辑的非线性程度很高。认知复杂度在一次又一次的嵌套中被叠加到很大。
这会导致在调试过程中,需要进行很多人工判断,来验证程序是否走进了正确的分支。这种需求提高了调试难度。
项 Term
巨型方法
-
判断是否是同类项的方法
public boolean isHomogeneous(Term orgTerm)
该方法的各种复杂度显著高于其他方法,以下对其进行分析。
超多嵌套:如果要判断同类项,就要遍历待判断的项的每一个元素,首先判断是否是同类元素,在找到同类元素后,再针对不同元素种类实施不同的判定方法。这一逻辑过程本身包含了四层嵌套:项自身的元素的for
循环、待判定项的元素的for
循环、判断同类元素的if
、判断同类元素满足同类项条件的if
。多层嵌套极大提高了认知复杂度。事实上,该方法的类的认知复杂度是所有方法中最高的。
执行逻辑的非线性程度十分大:上述的诸多判断和循环直接导致控制流图具有很多分支,换句话说,程序有很多可能的运行路径。复杂的控制流图意味着很高的圈复杂度。
各个类的复杂度
类平均圈复杂度 | 类最大圈复杂度 | 类总圈复杂度 | |
---|---|---|---|
Class | OCavg | OCmax | WMC |
Main | 2.5 | 4 | 5 |
factors.Cons | 1.33 | 3 | 16 |
factors.Expr | 3.21 | 16 | 45 |
factors.Function | 1.69 | 9 | 27 |
factors.Term | 4.93 | 17 | 74 |
factors.TrigFunc | 1.94 | 7 | 33 |
factors.Var | 1.64 | 3 | 23 |
tools.Lexer | 1.27 | 3 | 19 |
tools.Parser | 5.7 | 12 | 57 |
类复杂度数据分析
解析器Parser
类和项Term
类平均圈复杂度较大,控制流比较复杂,在调试的时候相对困难。
迭代体验 & debug记录
HW1
第一次作业遇到的最大问题是符号的传递。
我让每个因子类都拥有符号
属性,这样一来,在处理字符串时可以方便的存储符号信息。在这之后的处理上,我犯下了最大的错误:在向某个对象中添加因子对象时,不根据该对象的符号对因子的符号进行修改,而是直接将因子对象存进容器中;直到在将该对象转化为表达式,也就是调用tostring
方法时,才去修改其成员变量的符号。换句话说,我将调整符号的功能加入了tostring
方法中。
这给调试带来了很大的困难。
在IDEA中进行调试时,只要将某个对象add to watches
,每向下执行一步,IDEA都会调用该对象的tostring
方法。而由于我在tostring
方法中修改了成员变量的属性,每次向下执行一步,都会带来对监视区对象的修改,也就是说,监视区对象会经常受到来自于我的程序之外的修改。
进一步的,意外的修改直接导致IDEA的调试功能失效,让我只能在头脑中模拟程序的运行——肉眼debug。这极大地损害了debug效率。但我当时没有舍得抛弃辛辛苦苦写出来的一团乱麻,而是选择“缝缝补补又三年”,打几个补丁试试代码能不能跑。
塞翁失马,焉知非福。我花了一整天打补丁,勉强能让程序跑起来。过了中测,自以为万事大吉。但事实上,这种设计给我后两周的迭代,挖了一个超级大——坑。
HW2
第二次作业遇到的最大问题还是符号的传递。
第二次作业要求添加对多层括号的解析,并添加了三角函数和自定义函数因子。其中,实现多层括号的解析并不需要做多少修改,毕竟第一次作业使用了递归下降法,其本身就已经能解析嵌套的括号了。工作量主要集中在两个函数的解析上。
刚开始我是这样想的,但实际上…
又在符号传递上花了超过大量的精力。
由于在tostring
方法中修改其因子符号,同时在合并同类项等处也修改了符号,一旦符号的传递出现问题,根本不知道是在哪个地方出bug,再加上递归下降的层数很多,增加了调试难度,我曾一度尝试自己构建方法减少递归结果的深度进而辅助debug。但是新构建的方法本身隐藏了bug。于是乎,bug越de越多,调试越来越困难。最后只好放弃减小深度,在每个修改符号的地方设置断点,头脑模拟程序运行,找到出问题的地方,再去修改代码。
HW3
第三次作业遇到的问题竟然还是符号的传递…
最后一次迭代要求添加求导功能,以及自定义函数的嵌套定义。由于求导因子可能在任何地方出现,包括自定义函数的定义,所以在读入函数定义时需要进行表达式的解析。
cos求导后变符号,以及求导的规则,包括链式法则和乘法法则,都给符号的调整提出了很大的要求,但这恰恰是我的程序的薄弱环节。在debug的过程中,我需要同时监视解析过程和转化为字符串的过程。事实上,我的程序在这两种地方都出了问题。
总结
千万不要在tostring
方法中对任何对象的任何属性进行任何修改!!!
本来可以在第一次作业,或者第二次作业开头时,对第一次的架构进行重构,但是当时偷懒,打打补丁就凑合用了,实际上最后debug花的时间可能不比重构花的时间少。
一个好的架构对后续迭代以及调试真的很重要,下次一旦发现整体架构出了问题,只要时间允许,立马重构。当然,在动手码代码之前,也要做好充分的设计:首先猜测可能出现的问题的点,之后对这些点进行分析,以求在码代码时能避免架构上的问题。
此外,分析圈复杂度发现,当时觉得难以调试的方法,其复杂度大多比较高,控制流比较复杂,不方便头脑模拟程序运行。可见,减少不必要的控制流和方法调用也是提高debug效率的好办法。
最后一点,多多借鉴别人的经验,并总结自身经验也是很必要的。
hack策略
由于没有写测评机,在hack别人的时候只能手捏数据点。再加上读别人的代码很困难,于是我直接使用我在debug时出错的数据来hack别人。
我在debug时,会构造边缘数据来测试程序,并将跑出问题的数据记录下来,一点一点缩减它,记录缩减过程中的每个版本,直到bug消失,其前一个版本就是最能指向bug所在地的数据。
在hack别人的时候,会将原始版本的易错数据拿来测试别人的代码。我发现,在有些点上,我容易出错,别人也容易出问题。于是,这些debug的辅助工具能在hack中派上用场。
的。
hack策略
由于没有写测评机,在hack别人的时候只能手捏数据点。再加上读别人的代码很困难,于是我直接使用我在debug时出错的数据来hack别人。
我在debug时,会构造边缘数据来测试程序,并将跑出问题的数据记录下来,一点一点缩减它,记录缩减过程中的每个版本,直到bug消失,其前一个版本就是最能指向bug所在地的数据。
在hack别人的时候,会将原始版本的易错数据拿来测试别人的代码。我发现,在有些点上,我容易出错,别人也容易出问题。于是,这些debug的辅助工具能在hack中派上用场。