2024ooUnit1

杨汇一-22371376 学生 2024-03-21 17:58:39

一、基于度量分析程序结构

1.度量类的属性个数、方法个数、每个方法规模、每个方法的控制分支数目、类总代码规模

img

img

2.分析类的内聚和相互间的耦合情况

img

img

img

图中CogC代表认知复杂度,衡量代码是否容易理解;ev(G)衡量非结构化程度,ev(G)高意味着难以模块化和维护 ;iv(G)代表模块设计复杂度,用来衡量模块之间的调用关系;v(G) 代表圈复杂度,用来判定结构的复杂程度。

我的getString()方法比较复杂,原因是在处理输出时,我对系数为-1,0,1,指数为0和1的情况进行了特判,导致分支较多。

3.类图

img

首先先用类Pre预处理字符串,然后用Lexer词法分析Parser语法分析。分析后的语法树自顶向下为ExprTermFactor类,Factor一共有6种,用一个接口将他们统一管理起来,其中,Invoke类专门用来实现自定义函数的定义和调用。语法树的每个部分都有toPoly方法,转换为多项式polyPoly类中用一个Hashmap管理单项式中的系数,变量指数和exp()里的poly,将变量指数和exp()里的poly封装成类Atom,将Atom和系数封装成Mono。最后用Output类对得到的Poly输出。

优点:建立语法树时用接口Factor统一管理,在新增因子时扩展性强;有专门的输入处理类Pre和输出处理类Output

缺点:Mono类显得比较多余,这是代码多次迭代后的遗留产物。

二、架构设计体验

1.第一次作业

架构设计

利用递归下降的思想,建立一个表达式树。

img

从顶层向下层分别是ExprTermFactorExpr是由Term相加得到的,Term是由Factor相乘得到的,而Factor又分为VarFactorNumberFactorExprFactor,利用一个接口Factor对这三个类进行抽象管理。其中,ExprFactor这个类里面储存着括号里的表达式expr和指数exp。建立一个Lexer类,用一个move()方法实现在输入的字符串中遍历得到一个个的token。然后建立一个Parser类,对字符串自顶向下进行语法分析。首先实现解析Expr,若遇到"+",则将解析的Term加入Expr中的Arraylist<Term> terms。再实现解析Term,若遇到"*",则将解析的Factor加入Term中的Arraylist<Factor> factors。最后实现解析Factor,如果lexer返回的token数字,则返回解析到的数字;如果是字母,则返回解析到的变量以及指数值(若没有指数则设指数为1);如果是括号,则先将括号里的expr解析出来,再解析指数的值,最后返回expr和指数结合起来的ExprFactor

经过思考,发现任何表达式展开括号后都可能写成Σa*x^b(单项式相加)的形式。所以我新建了Poly类和Mono类,在Poly类中新建Arraylist<Mono> monos,在Mono类里存储系数a和指数b。在ExprTermFactor中新建toPoly()方法,将它们都转换为多项式。其中,ExprtoPoly是将termstermpoly相加,Term中的toPoly是将factorsfactorpoly相乘,ExprFactor是将exprpoly和指数做乘方运算。我在Poly类里实现了多项式之间的加法乘法乘方运算。在主函数中,我对解析后得到的expr调用toPoly()方法就能得到整个式子的多项式。最后新建一个Output类,实现getString方法,专门对输出做处理,并且对monolist为空(最后结果为0),mono系数为0、1、-1指数为0、1的情况进行了特判。

其中有四点需要注意:

第一点是合并同类项:我们必须保证在每一次poly的运算后都对其进行同类项合并,否则遇到诸如(1+x)^8的情况很容易超时。由于对迭代器的用法不熟,我在实现此功能时用了一个十分笨拙的方法。就是双重遍历monos容器,两个mono的指数相同时,则将前一个mono的系数增加一倍,后一个mono的系数置为0,最后在遍历一次将系数为0的mono移除。可以看到一共需要遍历至少三次,这也增加了时间复杂度,为我之后的代码重构埋下了伏笔。
        
第二点是对指数为0的情况的处理:在对poly进行乘方运算时,若出现指数为0的情况,则直接返回一个值为1的poly

第三点是对"-"的处理:我的思路是把"-"看作一个numberfactor,再解析factor时遇到"-"就返回值为-1的numberfactor。这样做虽然思维难度简单,但很容易出现问题,因为有可能将"-"视为-1后,其前后没有"*"或者"+",我对此专门做出了预处理

第四点是预处理:在进行词法分析前,我用了一个Pre类对字符串预处理。主要任务是将其中的空格符\t去除,然后再将连续的符号化简为单个,然后将"*","^","("后的"+"处理掉,最后是将数字、变量、括号与在之前的"-"间添加"*",将数字、变量、反括号与在之后的"-"间添加"+"。这样做的目的是降低耦合度,实现单一职责原则

可扩展性

可扩展性较差原因主要是在合并同类项时,我没有考虑到后续添加多种类型因子的情况。若添加exp()或者三角函数时,就不能简单地对指数进行相等判断,还要对exp()或三角函数里的expr是否相等进行判断。因此,Poly并不能简单地用Arraylist存储Mono,而应该用自带equals方法判断的hashmap

优化

第一次作业的优化主要是针对第一个单项式系数是负数的情况,将第一个系数是正数的单项式挪到开头即可解决。

Bug分析

第一次作业中未出现Bug

2.第二次作业

架构设计

img

对指数因子:

由于指数因子exp(a)^b=exp(a*b)exp(a)*exp(b)=exp(a+b)的特性,任何多项式都可以写成Σa*x^b*exp(poly)(单项式相加)的形式。我的第一想法是在mono类里添加一个存放exp里的poly。我新建了一个ExpFactor类,里面存放括号里的factor以及指数exponent,在toPoly方法内,我新建一个mono,系数设为1,指数设为0,poly设为exp里的factorpolyexponentpoly,最后返回一个加入了该monopoly;在实现多项式相乘时,对mono里的poly相加。

我主要是在合并同类项时遇到了问题。我原先的做法是将mono里面存放exp()factor信息的poly先用getString方法得到展开后的字符串,然后在合并同类项时增加两个mono字符串相等的条件,但用这种方法的效率非常慢,遇到嵌套指数和exp()里的factor特别复杂的情况时,极有可能出现超时的bug。

经过思索后,我重构了代码。既然合并同类项要判断mono的指数和poly,所以我又新建了一个类Atommono的指数和poly封装起来,然后将Poly类里存放mono的容器arraylist改为hashmap<Atom, BigInteger>,这样做的目的是利用hashmapput()contains()方法,在addMono的时候会自动寻找与其atom相等的键值对,从而达到合并同类项的目的。只需要重写Mono, Atom, Polyhashcode()equals()方法,就可以比较轻易地实现。在addMono中,如果map的键中没有传入monoatom,则put这个mono;若存在,则将找到的atom对应的系数与传入mono的系数相加,若结果为0,则remove掉这个键值对,否则更新系数。

对自定义函数因子:

新建了FuncFacter类,里面存放自定义函数调用实参后的字符串,以及该字符串解析后的表达式。新增Invoke类,用HashMap<String, String>存放函数名对应的表达式HashMap<String, ArrayList<String>>存放函数名对应的形参,用静态addFunc()方法实现定义自定义函数时的功能,用静态invokeFunc()方法实现调用自定义函数的功能,通过实参替换形参实现。在parser解析到"f","g"或"h"时,继续解析的表达式,直到遇到")",将实参解析出来与函数名一并传入invokeFunc方法,得到代入实参后的函数字符串后就可以新建一个Funfactor并返回了。

invokeFunc中,有几点要注意:

第一点:要先将自定义函数中的形参替换成"u","v","w"或其他变量,再将"u","v","w"替换成实参。否则会出现问题。例如对于f(x,y)=2*x-y,带展开的表达式为f(y,x),若直接先将"x"替换成"y",变为了2*y-y,再将"y"替换成"x",变为了2*x-x,最后结果为"x",显然不对。

第二点:要先将自定义函数中的"exp"替换成其他变量如"a",因为在替换时可能也会将"exp"中的"x"替换掉。

第三点:最好先将实参放进括号内将其当成一个整体再替换,否则也有可能出现问题。例如对于f(x)=2-x,带展开的表达式为f(-1),若实参"-1"不加括号,会替换为2--1,会出现问题,当然也可以再进行一遍预处理。

对负号:

考虑到将来有可能会加入多种因子,负号的位置可能会出现在各种意想不到的地方,所以我又进行了针对负号的重构。跟第一次作业中预处理负号不同,我在各种Factor的类中新增了sign属性,用来判断该Factor的正负情况,在Factor接口新增changeSign()方法,并在Poly类中新增changeSign()方法用来实现对多项式的取反。在parser中解析到"-"时就继续解析后面的factor,再调用changeSign并返回。

优化

主要针对指数函数去掉一层括号,我首先无脑在exp()里再叠一层括号,然后再用replaceAll方法将里面是正负数或者变量的exp(())去掉一层括号。

Bug分析

在重写Atomequals方法中,我在对BigInteger类型的指数exp判断相等时犯了错误,我直接用了相等判断符"==",但应该用equals判断。这导致我实现不了合并同类项的效果,在强测中几乎丧失了全部性能分,有一个点TLE,并且还在互测中被hack到了一个TLE的点。

0
(1+x+x^2+x^3)^6

3.第三次作业

架构设计

由于经历了前两次作业中的重构,代码扩展性良好,本次作业不需要太多的变动,只需增加求导因子的实现。语法树变为:

img

新增DeFactor类,里面储存括号内的表达式。并且在Factor接口和TermExpr中新增toDe()方法,返回对它们求导之后得到的Poly。根据求导法则具体实现:

img

其中,DeFactor类里面也要实现toDe()方法,因为可能会出现多层求导因子嵌套的情况,如dx(dx(……))。具体实现为:先将dx()括号内的expr解析为展开后的表达式字符串,再对这个字符串进行新的一轮词法和语法分析,将得到的表达式用toDe()得到求导后的poly后返回。

可扩展性

经过三次作业迭代后,我的代码扩展性良好。如果在将来新添对数因子,利用可以仿照指数因子,在Mono中新添Hashmap<Poly, BigInteger>用来存放对数的真数多项式和对应的指数,同时在Mono中实现addLog方法。但仍有不足之处,在自定义函数替换形参时,有可能用户的需求并不仅仅满足于有限的几个字母,这个时候不能简单地先将形参替换成"u","v","w",需要寻求更好的方法。

Bug分析

在第三次作业没有出现Bug

三、心得体会

在第一单元中,我收获颇丰,我通过replaceAll方法巩固了一些正则表达式的知识,还第一次学会了($1)($2)在正则表达式中的用法;通过重写hashcodeequals方法,我加深了对hashmap用途和优点的理解;通过将预处理和处理输入单独封装成类,我学会了单一职责原则可以降低耦合;通过Factor接口抽象管理不同因子,我加深了对接口的理解。最重要的是,我学会了递归下降,自顶向下的思想。

四、未来方向

希望每单元的训练提交后希望可以有个结果的反馈,这样能够更好掌握每单元的知识。

...全文
55 回复 打赏 收藏 转发到动态 举报
写回复
用AI写文章
回复
切换为时间正序
请发表友善的回复…
发表回复

301

社区成员

发帖
与我相关
我的任务
社区描述
2023年北航面向对象设计与构造
学习 高校
社区管理员
  • YannaZhang
  • CajZella
  • C_ecelia
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告
暂无公告

试试用AI创作助手写篇文章吧