度量分析程序结构
类和方法的相关度量
类名 | 属性个数 | 方法个数 | 类总代码规模(行) |
---|---|---|---|
Calculate | 0 | 6 | 160 |
Data | 5 | 14 | 263 |
Differentiation | 0 | 4 | 136 |
Expr | 1 | 7 | 93 |
Factor | 0 | 3 | 42 |
Term | 1 | 3 | 22 |
TriFunc | 3 | 5 | 76 |
Var | 1 | 3 | 21 |
Func | 3 | 5 | 76 |
Lexer | 3 | 3 | 51 |
Main | 0 | 1 | 29 |
Parser | 1 | 7 | 205 |
方法名 | 方法规模 | 控制分支数目 |
---|---|---|
Calculate.add | 31 | 4 |
Calculate.multyplyTwo | 11 | 0 |
Caiculate.multiply | 9 | 0 |
Calculate.unite | 50 | 6 |
Calculate.changeSign | 9 | 0 |
Calculate.simplify1 | 33 | 1 |
Data.isSameData | 23 | 4 |
Data.isSimpleFactor | 3 | 0 |
Data.isNum | 5 | 0 |
Data.isVar | 10 | 0 |
Data.isTriFunc | 5 | 0 |
Data.convertedToOne | 33 | 5 |
Data.addData | 4 | 0 |
Data.minusData | 4 | 0 |
Data.mulData | 6 | 0 |
Data.abs | 2 | 0 |
Data.toString | 45 | 8 |
Data.buildVar | 32 | 9 |
Differentiation.diff | 51 | 4 |
Differentiation.diffCommon | 51 | 6 |
Differentiation.makeExpr | 8 | 0 |
Differentiation.setData | 11 | 1 |
Expr.mulExpr | 10 | 0 |
Expr.addExpr | 10 | 0 |
Expr.addExpr | 9 | 0 |
Expr.diffExpr | 6 | 0 |
Expr.addTerm | 3 | 0 |
Expr.getArrayList | 5 | 0 |
Expr.toString | 18 | 2 |
Factor.isSameFactor | 30 | 5 |
Term.addFactor | 3 | 0 |
Term.getArrayList | 5 | 0 |
TriFunc.isSameTriFunc | 8 | 2 |
TriFunc.isPartlySameTriFunc | 8 | 2 |
TriFunc.toString | 11 | 0 |
Var.getArrayList | 3 | 0 |
Var.toString | 2 | 0 |
Func.setter | 29 | 2 |
Func.calculateFunc | 6 | 0 |
Func.replaceFuncToExpr | 23 | 2 |
Func.searchFunc | 17 | 3 |
Func.replaceInput | 8 | 0 |
Lexer.next | 17 | 2 |
Lexer.getNumber | 12 | 0 |
Lexer.point | 3 | 0 |
Lexer.isExpNext | 3 | 0 |
Main.main | 17 | 0 |
Parser.parseExpr | 18 | 1 |
Parser.parseTerm | 16 | 1 |
Parser.parseFactor | 51 | 5 |
Parser.parseVar | 31 | 4 |
Parser.parseTriFunc | 51 | 7 |
Parser.parseDiff | 8 | 0 |
分析类的内聚耦合情况
利用IDEA的Calculate Metrics中的Class Metrics对类的复杂度进行分析:
- OCavg(Average opearation complexity):平均操作复杂度
- OCmax(Maximum operation complexity):最大操作复杂度
- WMC(Weighted method complexity):加权方法复杂度
Class | OCavg | OCmax | WMC |
---|---|---|---|
expr.Calculate | 5.7 | 14.0 | 34.0 |
expr.Data | 2.625 | 13.0 | 63.0 |
expr.Differentiation | 6.25 | 14.0 | 25.0 |
expr.Expr | 1.9 | 6.0 | 17.0 |
expr.Factor | 10.0 | 10.0 | 10.0 |
expr.Term | 1.0 | 1.0 | 3.0 |
expr.TriFunc | 1.6 | 4.0 | 18.0 |
expr.Var | 1.0 | 1.0 | 3.0 |
Func | 5.0 | 7.0 | 25.0 |
Lexer | 2.6 | 6.0 | 13.0 |
Main | 2.0 | 2.0 | 2.0 |
Parser | 5.6 | 12.0 | 39.0 |
Total | 252.0 | ||
Average | 3.19 | 7.5 | 21.0 |
- 这次作业的类复杂度很高,部分类之间耦合程度高。但是继承较少,多数类的功能互不影响;
- 对于Calculate类的函数,主要是用于合并同类项的一些运算以及性能优化部分,所以不仅连续嵌套循环,还要不断地调用其他类中的函数,比如Data、TriFunc等等。这就导致在计算和化简时,类之间反复调用,圈复杂度很高,也导致了耦合度大;
- Differentiation类有求导函数。求导函数在使用链式法则和乘法法则时反复调用自己,也出现了较大复杂度;
- Factor类是平均最复杂的类。作为一个抽象类,它只有一个函数:判断两因子是否相等。这其中又包括判断各因子中的项是否一一相等,循环多调用多,出现了很高的平均复杂度;加权复杂度最高的Data同理,在判断两项是否相等时,要判断内部三角函数是否一一相等;
- Term,Expr,TriFunc,Var等类与其他类间几乎没有干扰,作为表达式的“容器”完整地实现自身功能,可以说实现了一部分的“高内聚”原则。
架构设计
第一次作业
第一次作业的目标是给出表达式,得到拆开括号后的结果。
首先说明递归下降的方法。递归下降通过对给定的文法进行严谨的分析,能够逐层获得对应部分。再对该部分进行解析计算,就可以逐步将整个表达式化简。以解析表达式为例:
所以需要构造出能够分析表达式字符的词法解析器Lexer,以及根据文法实行递归下降、解析出各个部分的文法解析器Parser。
根据文法,需要解析三个部分:表达式、项和因子,所以Parser类中三个函数分别用来解析上述三部分;而因子又分三种:数字因子、变量因子和表达式因子,因此可以让Var(数字和幂因子类)和Expr(表达式因子类)继承一个抽象出来的Factor类,同时解析因子的函数也要分为三部分来执行各自的功能。
解析表达式的方法有了,下一步就是为每一个部分设置对应的“容器”,保存解析出来的部分。
从最基本的构成谈起,任何表达式、项、因子都可以表示成如下形式:
∑
i
=
1
n
a
i
x
b
i
y
c
i
z
d
i
\sum \limits _{i=1}^na_{i}x^{b_{i}}y^{c_{i}}z^{d_{i}}
i=1∑naixbiycizdi
也就是说,知道了系数、xyz的指数,就能确定最基本的形式。所以不妨将Data类的属性设置为上述四个数,再用Data去构建其他所有的部分即可。
随后需要定义Data的运算,以便于合并同类项。定义同类项判断、加法、乘法。之后定义ArrayList<Data>间的加法、乘法运算,即若干个Data间相加或相乘,为随后的表达式、项的加、乘法做准备。
因子统一为ArrayList<Data>形式,可以存储三种因子;Term定义为ArrayList<ArrayList<Data>>形式,保存构成的因子,各元素间是相乘的关系,而获取该term时格式简化为ArrayList<Data>;Expr类定义为ArrayList<ArrayList<Data>>形式,保存构成的项,各元素间是相加的关系,而获取该expr时格式简化为ArrayList<Data>。至此,表达式各部分的容器也搭建完成。
最后是性能优化。除了将同类项合并外,在Data中重写toString函数,分析系数是否为0、1或-1,xyz指数是否分别大于0,从而构建出符合数学公式的最短字符串(将x**2简化为x*x)。在Expr中重写toString函数,选取一个正项先输出省去开头负号,再依次按照各个Data的顺序逐一输出。该性能优化可以实现输出字符最少。
将解析、装填、运算三部分结合起来,即可完美实现对应功能,在强测和互测中并未出现bug。第一次作业类图如下:
继承关系明确,方法清晰,类与类之间耦合情况并不严重,设计得非常好。
第二次作业
本次作业增量开发难度很大,新增三个功能:嵌套括号化简、实现函数定义及调用、新增三角函数因子。
因为第一次作业使用递归下降,所以嵌套括号自然而然可以实现。主要分析另两个功能。
函数定义及调用
首先是处理定义函数。对读入的函数表达式去空白后,应该替换形参。防止之后在调用时出现形参与实参同名,导致替换错误的情况。所以先用正则表达式提取函数名、形参列表和定义式,再根据形参顺序,依次地将定义式中的形参替换为arg{i}。例如:f(z,x,y) = (x+y)**2-z处理为f(arg0,arg1,arg2)=(arg1+arg2)**2-arg0。替换后的式子保存为属性。
其次就是将表达式中函数调用替换为带入函数后的式子。遍历表达式,寻找fgh符号。找到后根据成对括号数量确定作用域,利用逗号分隔各实参,按照顺序分别将arg{i}替换为实参。值得注意的是,在带入实参以及返回替换后的式子时,外层需要加一层括号,避免改变运算顺序。
三角函数因子
对于词法解析器,需要识别sin、cos等符号;对于文法解析器,需要在解析因子部分增加三角函数的文法解析。
重新定义Data类。对于任意的表达式、项、因子都可以表示成如下形式:
∑
i
=
1
n
a
i
x
b
i
y
c
i
z
d
i
∏
j
=
1
p
s
i
n
j
(
f
a
c
t
o
r
j
)
∏
k
=
1
q
c
o
s
k
(
f
a
c
t
o
r
k
)
\sum \limits _{i=1}^na_{i}x^{b_{i}}y^{c_{i}}z^{d_{i}} \prod \limits _{j=1}^psin^j(factor_j)\prod \limits _{k=1}^qcos^k(factor_k)
i=1∑naixbiycizdij=1∏psinj(factorj)k=1∏qcosk(factork)
为了统一管理,将sin和cos设置为三角函数类TriFunc,设置种类(1为sin,2为cos)、指数、内部因子三个属性。在Data类新增ArrayList<TriFunc>存储三角函数。
重新定义运算,包括Data相关的所有加法、乘法。Data的同类项定义为xyz指数分别相等且两Data内部三角函数分别等价;
定义三角函数的等价关系:同名、同次数、因子等价;
定义因子的等价关系:所含Data分别等价,即Data系数相同且是同类项。
在自反性、对称性、等价性的作用下,三个等价关系形成一个闭环,从而可以应对三角函数内部因子任何不同情况。如sin((x+(cos(sin(x)))))等情况,该闭环可保证解析、计算无误。
至此,三角函数的解析、计算与存储也完毕了。在性能优化部分,TriFunc中重写toString方法,输出符合格式的三角函数字符串。在Data中额外判断是否存在三角函数,并按照三角函数的输出方法顺序输出即可。此外,本次作业还优化了诱导公式中sin(-x)=-sin(x)、cos(-x)=cos(x)的部分,以及sin(x)**2+cos(x)**2=1、2*sin(x)*cos(x)=sin((2*x))的部分。对三角函数内部因子为数字、幂因子的单层括号问题也进行了优化。
将上述功能统一起来,便完成了第二次作业。类图如下:
在这次作业中由于新增功能的原因,类之间耦合性变大,尤其是Data、TriFunc和Factor类的互相调用,使得圈复杂度较高。此外,三角函数化简表达式较为繁琐,使得Data类出现大量化简代码,使类复杂度也增加了不少。
在测试过程中出现了两个bug:函数没有删除空白符,导致函数作用域识别错误,文法解析器无法识别抛出异常;
另一个是三角函数内部因子是否为数字、幂因子识别错误。我采取的方法为计算内部因子Data列中数字、幂因子的个数,只要为1就套单层括号。然而强测中频频出现的形如cos((1+sin(x)**2))的因子也只含一个数字因子,却要加双层括号。因此,我将判断条件改为Data列元素个数为1,且为数字、幂因子,则套单层括号。
对于出现bug的Data.toString函数,由于控制分支过多,代码45行长度,所以复杂度远高于其他方法:
method | CogC | ev(G) | iv(G) | v(G) |
---|---|---|---|---|
expr.Data.toString() | 27.0 | 2.0 | 13.0 | 15.0 |
所以方法要尽量减少控制分支,或者写入不同方法中,才可能完备地考虑所有情况!
第三次作业
本次作业开发量较少,只新增两个功能:函数定义允许调用其他函数、表达式新增求导因子。
对于前者,只需要将每个函数定义式看作是需要化简的表达式即可。对于函数定义式中出现求导因子,需要将式子求导后再进行保存。在Func类中设置一个parser,解析、计算该函数定义式,将所得Expr调用toString函数得到一个String,就是该函数求导、代入其他函数调用并且合并同类项之后的定义式了。
对于后者,需要使Lexer识别“d”从而判断出求导因子,Parser类中也要在解析因子时新增对求导因子的识别。
前面提到,任何表达式、项、因子都可以表示为若干Data类相加。而对每个Data类求导则得到Expr类。也就是说,对表达式、项、因子求导,得到的部分都可以统一地表示成ArrayList<Expr>。为了方便起见,不妨将Var类、TriFunc类都继承Expr类,在求导时类型转换十分方便。
求导的基本元素是Expr,所以先定义Expr的一些运算,包括Expr间的加法、乘法、求导,为之后的求导函数所需运算做铺垫。
新建Differentiation类,具备求导方法。第一个方法是diff,用于设置递归出口:
-
若Data不含该求导变量,返回0
-
若Data只是该变量的幂因子,按照如下法则返回:
( x n ) ′ = n x n − 1 (x^n)'=nx^{n-1} (xn)′=nxn−1 -
若Data是单个三角函数,按照如下法则返回:
[ c o s k ( f ( x ) ) ] ′ = − k c o s k − 1 ( f ( x ) ) ⋅ s i n ( f ( x ) ) ⋅ f ′ ( x ) [cos^k(f(x))]'=-kcos^{k-1}(f(x))\cdot sin(f(x))\cdot f'(x) [cosk(f(x))]′=−kcosk−1(f(x))⋅sin(f(x))⋅f′(x)[ s i n k ( f ( x ) ) ] ′ = k s i n k − 1 ( f ( x ) ) ⋅ c o s ( f ( x ) ) ⋅ f ′ ( x ) [sin^k(f(x))]'=ksin^{k-1}(f(x))\cdot cos(f(x))\cdot f'(x) [sink(f(x))]′=ksink−1(f(x))⋅cos(f(x))⋅f′(x)
return Expr.mulExpr(expr2, expr, Expr.diffExpr(expr1, arg));//其中expr2为系数,expr为三角函数,expr1为内部因子,该语句意为上述三者相乘
其他表达式进入diffCommon中,进行普遍形式的求导。
分两种情况:
-
当Data中含有系数、该变量幂因子和可能存在的三角函数时,将系数、幂因子、三角函数列分别封装为Expr(若不存在三角函数,Expr为常数1),按照如下法则返回:
( a x k ∏ i = 1 n s i n p i ( f a c t o r i ) ∏ j = 1 n c o s q i ( f a c t o r j ) ′ = a k x k − 1 ( ∏ i = 1 n s i n p i ( f a c t o r i ) ∏ j = 1 n c o s q i ( f a c t o r j ) ) ′ (ax^k\prod \limits _{i=1}^nsin^{p_i}(factor_i)\prod\limits_{j=1}^ncos^{q_i}(factor_j)'=akx^{k-1}(\prod \limits _{i=1}^nsin^{p_i}(factor_i)\prod\limits_{j=1}^ncos^{q_i}(factor_j))' (axki=1∏nsinpi(factori)j=1∏ncosqi(factorj)′=akxk−1(i=1∏nsinpi(factori)j=1∏ncosqi(factorj))′ -
当Data中含有系数、三角函数时,将系数、第一个三角函数、剩余三角函数列分别封装为Expr(若不存在第二个三角函数,则Expr为常数1),按照如下法则返回:
( a ∏ i = 1 n s i n p i ( f a c t o r i ) ∏ j = 1 n c o s q i ( f a c t o r j ) ) ′ = a [ c o s q 1 ( f a c t o r 1 ) ] ′ ⋅ ( ∏ i = 1 n s i n p i ( f a c t o r i ) ∏ j = 2 n c o s q i ( f a c t o r j ) ) ′ (a\prod \limits _{i=1}^nsin^{p_i}(factor_i)\prod\limits_{j=1}^ncos^{q_i}(factor_j))'=a[cos^{q_1}(factor_1)]'\cdot (\prod \limits _{i=1}^nsin^{p_i}(factor_i)\prod\limits_{j=2}^ncos^{q_i}(factor_j))' (ai=1∏nsinpi(factori)j=1∏ncosqi(factorj))′=a[cosq1(factor1)]′⋅(i=1∏nsinpi(factori)j=2∏ncosqi(factorj))′( a ∏ i = 1 n s i n p i ( f a c t o r i ) ∏ j = 1 n c o s q i ( f a c t o r j ) ) ′ = a [ s i n p 1 ( f a c t o r 1 ) ] ′ ⋅ ( ∏ i = 2 n s i n p i ( f a c t o r i ) ∏ j = 1 n c o s q i ( f a c t o r j ) ) ′ (a\prod \limits _{i=1}^nsin^{p_i}(factor_i)\prod\limits_{j=1}^ncos^{q_i}(factor_j))'=a[sin^{p_1}(factor_1)]'\cdot (\prod \limits _{i=2}^nsin^{p_i}(factor_i)\prod\limits_{j=1}^ncos^{q_i}(factor_j))' (ai=1∏nsinpi(factori)j=1∏ncosqi(factorj))′=a[sinp1(factor1)]′⋅(i=2∏nsinpi(factori)j=1∏ncosqi(factorj))′
return Expr.mulExpr(unnec, Expr.addExpr(Expr.mulExpr(Expr.diffExpr(expr1, arg), expr2),
Expr.mulExpr(expr1, Expr.diffExpr(expr2, arg))));
至此,所有表达式的求导情况便已涵盖,通过递归便可得到任意表达式的导数。
第三次作业功能新增完毕,结构有稍微的变动。类图如下:
虽然完成了需要的功能,但是类之间的耦合程度进一步加大,尤其是Expr类和Differentiation类的互相调用。而一些主要类的方法复杂度也增大不少,并不利于再进行修改。
在本次互测、强测中,除了爆内存的诸如f(x)=dx((((x+y)**8)**8)**8)之外,并没有出现bug。
发现他人bug策略
针对文法中或方法中容易被忽略的地方进行hack。比如:
- 函数定义式或表达式中可能出现的空白项:f(x) = x +1
- 表达式、项前可能出现的+、-号:–cos(x)±x
- 常数因子的正负号、指数的正号、前导零:-+cos(x)* -0003
- 三角函数的多层嵌套:cos((cos(x)±1)**3)±1
- 函数的多层嵌套:f(f(f(x)))
- 零次幂:(0)**0,(sin(sin(y)))**0
- 性能优化漏洞:sin((sin(x)**2+cos(x)**2-sin(0)**+0)),sin((-x-y))+sin((x+y)),2*sin(x)**2*cos(x)**2
- 求导函数:dx(f(g(x),g(x**2)))
由此构造出复杂数据,如
1
f(z,x)=cos(cos((x*z**3)**2))
z+dz(f(f(z,sin(z))**3,+1)**3)+z**3
有效性一般。因为并没有结合被测程序的代码设计结构来设计测试用例,只是将代码放入对拍器,然后比对sympy化简的表达式和输出的表达式是否相同,所以并无很高的针对性。
心得体会
经过三次作业迭代,我认为自己出色地完成了本单元任务。虽然在第二次作业强测中,因为括号的判定问题错误数据较多,但是问题不大。而且其他的两次作业都几乎是满分,我十分满意。
满意之余,我重新看了看实验课给的代码。类与类之间并没有冗余的联系,而且继承关系特别明确。比如求导,根据求导对象的不同,进入不同子类的求导函数中,真正地体现出来了面向对象的设计。
我的代码相对而言更偏重于实现当前的功能,而忽略了将该功能分散到不同的子类。比如,还是求导,我的想法是单独创建求导类,将所有对象包装成Expr类,用一个类完成求导的所有计算。这样就会使方法复杂度骤增,且不利于之后的维护和进一步的改动。
或许这就是刚入门的时候先入为主的思路吧。我阅读了同一互测房其他人的代码,似乎也并不能清晰地看出面向对象的设计思想。所幸的是,在研讨课上,组员们提供了宝贵的思路改进方法,也指出了我很多bug。我不禁想问,我自己的分享是否也有意无意地帮助了其他人克服困难?或者至少让一名组员从一头雾水,开始有了一些思路?总之十分感谢研讨课的出现,让一个相对难度较大的项目有了他人的援助,也打破了计算机专业课传统的“自己的项目自己做”的旧规定吧。必要的合作,也是学习过程中不可或缺的一部分,而oo课为我们做了榜样。
不敢说之后的作业还能像第一次作业这样,付出时间长,也有很好的收获。以后的作业要求更复杂呢?新知识点让人更难以理解呢?付出时间可能更长了,但收获会更大吗?我不得不抱着悲观的心态看待以后的艰险,但是有一点是毋庸置疑的:随着练习一次又一次进行,只在实验课出现过的“教科书式”的代码也不再是一种幻想。说不定哪次作业,这看似完美代码也会是我git push上去的呢?
最后以我喜爱的一首诗作结吧,作为我第一单元的总结:
莫言下岭便无难,赚得行人错喜欢。
政入万山围子里,一山放出一山拦。