OO第一单元——解析表达式总结

文章详细分析了程序中类和方法的度量,包括内聚性和耦合性,并探讨了类的复杂度。在架构设计部分,介绍了如何使用递归下降解析表达式,以及如何处理函数定义、调用和三角函数因子。在后续作业中,增加了函数调用其他函数和表达式求导的功能,但导致了类间耦合度的增加和复杂度的提高。作者还分享了发现和修复bug的心得体会。
摘要由CSDN通过智能技术生成

度量分析程序结构

类和方法的相关度量

类名属性个数方法个数类总代码规模(行)
Calculate06160
Data514263
Differentiation04136
Expr1793
Factor0342
Term1322
TriFunc3576
Var1321
Func3576
Lexer3351
Main0129
Parser17205
方法名方法规模控制分支数目
Calculate.add314
Calculate.multyplyTwo110
Caiculate.multiply90
Calculate.unite506
Calculate.changeSign90
Calculate.simplify1331
Data.isSameData234
Data.isSimpleFactor30
Data.isNum50
Data.isVar100
Data.isTriFunc50
Data.convertedToOne335
Data.addData40
Data.minusData40
Data.mulData60
Data.abs20
Data.toString458
Data.buildVar329
Differentiation.diff514
Differentiation.diffCommon516
Differentiation.makeExpr80
Differentiation.setData111
Expr.mulExpr100
Expr.addExpr100
Expr.addExpr90
Expr.diffExpr60
Expr.addTerm30
Expr.getArrayList50
Expr.toString182
Factor.isSameFactor305
Term.addFactor30
Term.getArrayList50
TriFunc.isSameTriFunc82
TriFunc.isPartlySameTriFunc82
TriFunc.toString110
Var.getArrayList30
Var.toString20
Func.setter292
Func.calculateFunc60
Func.replaceFuncToExpr232
Func.searchFunc173
Func.replaceInput80
Lexer.next172
Lexer.getNumber120
Lexer.point30
Lexer.isExpNext30
Main.main170
Parser.parseExpr181
Parser.parseTerm161
Parser.parseFactor515
Parser.parseVar314
Parser.parseTriFunc517
Parser.parseDiff80

分析类的内聚耦合情况

利用IDEA的Calculate Metrics中的Class Metrics对类的复杂度进行分析:

  • OCavg(Average opearation complexity):平均操作复杂度
  • OCmax(Maximum operation complexity):最大操作复杂度
  • WMC(Weighted method complexity):加权方法复杂度
ClassOCavgOCmaxWMC
expr.Calculate5.714.034.0
expr.Data2.62513.063.0
expr.Differentiation6.2514.025.0
expr.Expr1.96.017.0
expr.Factor10.010.010.0
expr.Term1.01.03.0
expr.TriFunc1.64.018.0
expr.Var1.01.03.0
Func5.07.025.0
Lexer2.66.013.0
Main2.02.02.0
Parser5.612.039.0
Total252.0
Average3.197.521.0
  1. 这次作业的类复杂度很高,部分类之间耦合程度高。但是继承较少,多数类的功能互不影响;
  2. 对于Calculate类的函数,主要是用于合并同类项的一些运算以及性能优化部分,所以不仅连续嵌套循环,还要不断地调用其他类中的函数,比如Data、TriFunc等等。这就导致在计算和化简时,类之间反复调用,圈复杂度很高,也导致了耦合度大;
  3. Differentiation类有求导函数。求导函数在使用链式法则和乘法法则时反复调用自己,也出现了较大复杂度;
  4. Factor类是平均最复杂的类。作为一个抽象类,它只有一个函数:判断两因子是否相等。这其中又包括判断各因子中的项是否一一相等,循环多调用多,出现了很高的平均复杂度;加权复杂度最高的Data同理,在判断两项是否相等时,要判断内部三角函数是否一一相等;
  5. Term,Expr,TriFunc,Var等类与其他类间几乎没有干扰,作为表达式的“容器”完整地实现自身功能,可以说实现了一部分的“高内聚”原则。

架构设计

第一次作业

第一次作业的目标是给出表达式,得到拆开括号后的结果。

首先说明递归下降的方法。递归下降通过对给定的文法进行严谨的分析,能够逐层获得对应部分。再对该部分进行解析计算,就可以逐步将整个表达式化简。以解析表达式为例:

img

所以需要构造出能够分析表达式字符的词法解析器Lexer,以及根据文法实行递归下降、解析出各个部分的文法解析器Parser。

根据文法,需要解析三个部分:表达式、项和因子,所以Parser类中三个函数分别用来解析上述三部分;而因子又分三种:数字因子、变量因子和表达式因子,因此可以让Var(数字和幂因子类)和Expr(表达式因子类)继承一个抽象出来的Factor类,同时解析因子的函数也要分为三部分来执行各自的功能。

Parser类

解析表达式的方法有了,下一步就是为每一个部分设置对应的“容器”,保存解析出来的部分。

从最基本的构成谈起,任何表达式、项、因子都可以表示成如下形式:
∑ 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=1naixbiycizdi
也就是说,知道了系数、xyz的指数,就能确定最基本的形式。所以不妨将Data类的属性设置为上述四个数,再用Data去构建其他所有的部分即可。

随后需要定义Data的运算,以便于合并同类项。定义同类项判断、加法、乘法。之后定义ArrayList<Data>间的加法、乘法运算,即若干个Data间相加或相乘,为随后的表达式、项的加、乘法做准备。

Data类

Calculate类

因子统一为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}替换为实参。值得注意的是,在带入实参以及返回替换后的式子时,外层需要加一层括号,避免改变运算顺序。

Func类

三角函数因子

对于词法解析器,需要识别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=1naixbiycizdij=1psinj(factorj)k=1qcosk(factork)
为了统一管理,将sin和cos设置为三角函数类TriFunc,设置种类(1为sin,2为cos)、指数、内部因子三个属性。在Data类新增ArrayList<TriFunc>存储三角函数。

重新定义运算,包括Data相关的所有加法、乘法。Data的同类项定义为xyz指数分别相等且两Data内部三角函数分别等价;

定义三角函数的等价关系:同名、同次数、因子等价;

定义因子的等价关系:所含Data分别等价,即Data系数相同且是同类项。

在自反性、对称性、等价性的作用下,三个等价关系形成一个闭环,从而可以应对三角函数内部因子任何不同情况。如sin((x+(cos(sin(x)))))等情况,该闭环可保证解析、计算无误。

TriFunc类

至此,三角函数的解析、计算与存储也完毕了。在性能优化部分,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行长度,所以复杂度远高于其他方法:

methodCogCev(G)iv(G)v(G)
expr.Data.toString()27.02.013.015.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)=nxn1

  • 若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))]=kcosk1(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))]=ksink1(f(x))cos(f(x))f(x)

return Expr.mulExpr(expr2, expr, Expr.diffExpr(expr1, arg));//其中expr2为系数,expr为三角函数,expr1为内部因子,该语句意为上述三者相乘

其他表达式进入diffCommon中,进行普遍形式的求导。

分两种情况:

  1. 当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=1nsinpi(factori)j=1ncosqi(factorj)=akxk1(i=1nsinpi(factori)j=1ncosqi(factorj))

  2. 当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=1nsinpi(factori)j=1ncosqi(factorj))=a[cosq1(factor1)](i=1nsinpi(factori)j=2ncosqi(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=1nsinpi(factori)j=1ncosqi(factorj))=a[sinp1(factor1)](i=2nsinpi(factori)j=1ncosqi(factorj))

return Expr.mulExpr(unnec, Expr.addExpr(Expr.mulExpr(Expr.diffExpr(expr1, arg), expr2),
    Expr.mulExpr(expr1, Expr.diffExpr(expr2, arg))));

至此,所有表达式的求导情况便已涵盖,通过递归便可得到任意表达式的导数。

Differentiation类

第三次作业功能新增完毕,结构有稍微的变动。类图如下:

第三次作业类图

虽然完成了需要的功能,但是类之间的耦合程度进一步加大,尤其是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上去的呢?

最后以我喜爱的一首诗作结吧,作为我第一单元的总结:

莫言下岭便无难,赚得行人错喜欢。
政入万山围子里,一山放出一山拦。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值