OO 第一单元总结
第一单元通过三次递进式作业,让我们实现了较复杂表达式的解析,在这三次作业中我也有很多收获,接下来我对三次作业分别进行总结回顾。
第一次作业
-
摘要:
本次作业需要完成的训练目标为:完成单变量多项式的括号展开,初步体会层次化设计的思想。具体要求是:读入一个包含加、减、乘、乘方以及括号(其中括号的深度至多为 1 层)的单变量表达式,输出恒等变形展开所有括号后的表达式。
-
设计思路
- 程序架构分析:UML图如下:
本次作业我注意到表达式可以抽象成如图结构:
表达式由项组成,项之间通过加减运算符连接;项由因子组成,因子之间通过乘运算符连接;因子有中常数因子和变量因子可以统一通过它们的项和系数唯一确定(常数因子是指数为0的变量因子),表达式因子是表达式类型,它同样可以再解析出项和因子的层次。
因此我首先建立Expr、Term、Variable类分别代表表达式、项、常数因子与幂函数因子三个层次,此外创建Factor类型的接口,让Expr和Variabl实现。Expr中terms存储每一个term,Term中每个factors存储Variable或Expr,我们最终的结果形式为Variable + Variable + … ,于是在Expr和Term中建立HashMap:variables,key代表指数,value代表该变量。
对于输入的表达式,首先进行预处理,即有正则匹配的方法去除所有空格和制表符,将多个‘+’、‘-’替换成成一个,将‘+-’、‘-+’替换为'-',将 ‘*+’ 替换为 ‘*’,这样一来就实现了只保留有效符号,方便了之后递归解析表达式。唯一要注意的是,’+-‘类型的替换要在多个’-‘类型的替换之前进行,否则会出现减号替换不全的问题。
之后我参照实验一代码的写法,建立Parse和Lexer类用递归下降的方法解析表达式,形成如下灰色部分的树状结构:
对于一个没有表达式因子(即没有括号)的项来说,经过计算之后应该得到一个Variable,即形如 “ 系数 * x ** 指数 ” 的结果。这种项的计算很容易实现,只要将该项存放的Variable进行系数相乘、指数相加即可。对于一个有表达式因子的项,把括号拆掉必然会得到一个多项式。因此,计算之后应该得到若干个Variable。其中,项中非表达式的因子可以参照上一种项的形式先行计算,使得项的语义变为: Expr * Expr * … * Variable。因此在Term类和Expr类中写cal()方法,Term类中的cal()方法将各Variable类型因子相乘,再将所得结果与Expr类型因子经过cal()得到的variables相乘,(模仿我们自己拆括号的行为:(A)*(B)*(C) -> (AB)*(C) -> ABC
所得结果存入对应的variabls中。Expr类中的cal()方法根据该因子的指数进行Expr*Expr的操作,同样模拟两个表达式相乘的过程,其中需要首先递归调用Term类的cal()方法得到其中各个terms的varibles。
合并同类项则是遍历表达式的每个Term,将每个term的存Variable的数组内容转存到expr,用以指数为key的HashMap来装,更方便合并,最后按照coefficient* x * power的形式输出每个variable,中间用加号连接即可。
优化时我做了正负得负的化简以及x**0替换成1的化简,是简单的字符串处理操作。
-
代码度量分析 :
总体上看,我的架构对表达式的划分层次正确,但在输入预处理和输出优化时字符出处理的代码过多,导致我的MainClass复杂度较高,此外Expr和Term类中的Cal()存在相互调用的递归嵌套,iv(G)较高未符合高内聚低耦合原则,debug的过程十分痛苦。
-
关于测评
我的第一次作业在长时间的debug的绝望中最终无效,因此未进行较细致的数据测试。之后我进行了反思,发现我的bug出现在cal()方法的递归过程中,模拟因子相乘时逻辑出现错误。之后需要细心分析输入的形式化描述,防止理解出现偏差。
第二次作业
-
摘要 :
本次作业要求读入一系列自定义函数的定义以及一个包含简单幂函数、简单三角函数、简单自定义函数调用以及求和函数的表达式,输出恒等变形展开所有括号后的表达式。
-
设计思路:
本次作业对比第一次作业,新增要求有:
- 增加三角函数(三角函数中的因子可以时除表达式因子外的其它)、自定义函数(表达式中允许除x外的其他变量且不超过3个)、sum函数(求和函数,增加变量因子i);
- 允许括号嵌套
- 幂函数的指数允许大于8
-
对新增需求的应对策略
-
对于第2、3点新需求,我延续了第一次作业的解析模式,即仍使用 lexer+parser递归下降的方法解析表达式,构造具有Expr->Term->AllFactor层次的表达式树,这种递归解析构造的过程可正确处理括号嵌套问题,对于幂函数的指数大于8的新要求,因为我用Integer类型变量存储每个幂函数的指数,有关指数的相关需求都通过Integer类的方法实现,所以对于我解析构造的过程没有影响,唯一的影响是我在优化时是用纯字符串替换的方法,在正则匹配时诸如x**1时会错误匹配到x**10的错误(因此我在优化时未进行x**1 = x的优化,造成性能分的损失);
-
本次作业的难点在于新增要求的第一点,首先对于新增的自定义函数和sum函数的处理,可以把它们都看作一种特殊的表达式因子按第一次作业的方法一样地解析。因此我的方法是新增TriaFunc、Function、Sum类,这三个类都偏向于方法类,里面的方法多是与字符串替换相关的,在预处理阶段新增函数调用方法call(),遇到对应类型的函数时,进行相应的函数调用展开。call()方法返回值为String,其本质还是字符串替换,即将在函数出现的地方将其替换为当它展开后的表达式。这样做的好处是可以延续第一次作业的解析构造方法解析表达式,缺点在于:纯字符串替换的方法易造成因考虑情况不周造成较多bug的情况,我在这一阶段的处理也花费了大量时间测试和debug。
-
关于三角函数的处理: 第一次作业,抽象出了Variable的形式为a* x ** b,本次作业,自然想到将Variable改为a * x ** b * sin(Expr) ** c * cos(Expr) ** d …,Variable类中的成员在coefficient和power基础上增加TriaFunc类型的ArrayList,存储每一个三角函数,TriaFunc类中String类型成员base存储三角函数除去指数部分的内容,Integer类型的pow存储三角函数的指数,重写TriaFunc类的equal方法,通过比较base判断两个三角函数对象是否相等,用于之后三角函数的同类项合并。
此外,还要修改Lexer,添加三角函数获取的方法getTria,模仿实验代码中的getNumber方法进行。在Parser类的parseFactor方法中添加检测到三角函数因子的判断。
关于每一个Variable的合并计算,其x部分和第一次作业相同,三角函数部分通过遍历ArrayList容器,将base相同的三角函数power相加,base不同的三角函数直接添加到结果Variable中的ArrayList中。当然,由于我只通过比较base字符串判断三角函数的相等关系,并不能完美覆盖所有可以合并的情况,比如sin(x**2 + x) 和sin(x+x**2)本可以合并但,字符串判断不相等则最终没有合并。
-
注意事项:在构造每一个Variable 和 Variable中的Triafun时都要用深拷贝的方式。可以通过实现clonable接口方法实现,我是在修改需要进行深拷贝的类的构造器,在其构造器中new出新成员,而不是简单的进行赋值操作。
-
度量分析(篇幅原因仅展示部分)
Expr | 2.2 | 12.0 | 22.0 |
---|---|---|---|
Function | 1.7142857142857142 | 4.0 | 12.0 |
Lexer | 3.0 | 7.0 | 15.0 |
MainClass | 5.2 | 12.0 | 26.0 |
Parser | 4.0 | 9.0 | 16.0 |
Sum | 3.6666666666666665 | 4.0 | 11.0 |
Term | 2.4545454545454546 | 14.0 | 27.0 |
TriaFunc | 1.2222222222222223 | 3.0 | 11.0 |
Variable | 1.3333333333333333 | 3.0 | 16.0 |
Total | 156.0 | ||
Average | 2.3636363636363638 | 7.555555555555555 | 17.333333333333332 |
从第二次作业的度量分析中可以看出,MainClass、Sum、Function类的圈复杂度较高,原因是其中存在大量对输入表达式进行字符串预处理的函数,我的对输出表达式的性能优化也在MainClass的simplify方法中,延续第一次作业,也是进行正则匹配和字符串处理的。
-
关于测评
-
我的测试思路
我在本次作业中首先着重测试了字符串预处理部分,单纯测试是否能将各种函数正确替换成表达式因子的过程,之后我测试第一次作业中的样例以及多层括号嵌套的情况,最后按照形式化构造的方法构造多种自定义函数和三角函数加入的表达式,观察化简前的输出结果,写完simplify函数后继续测试化简前的输出结果与化简后的输出结果是否等价。
-
中测强测互测结果
在中测和强测中通过了所有测试点,但在互测中被hack了一次,检查代码后发现我的bug是在字符串预处理阶段,对输入形如下图表达式
0 +-+241
的符号处理出错,我的程序当时并未将前面+-+改变成-,而是变为+-简单调整字符串处理符号的逻辑先后顺序后即可解决bug,归根到底,还是字符出处理的细节问题,应该是第一次作业中的残留bug。
第三次作业
-
摘要
读入一系列自定义函数的定义以及一个包含幂函数、三角函数、自定义函数调用以及求和函数的表达式,输出恒等变形展开所有括号后的表达式。
-
设计思路
-
新增需求分析及解决策略
本次作业的新增需求主要在于:增加多重因子的嵌套规则,比如函数中可以再嵌套函数,三角函数中可以嵌套表达式因子等。
解决策略:仔细阅读指导书后,按照我前两次的思路,新需求本质上只需再增加三角函数中内嵌表达式,对于函数的嵌套,我仍然采用预处理中将所有函数解析成表达式因子的思路,需要修改的地方是将原来的替换的方法改写成递归替换的方法,以应付函数嵌套的问题。
本次作业的新增需求主要在于:增加多重因子的嵌套规则,比如函数中可以再嵌套函数,三角函数中可以嵌套表达式因子等。
解决策略:仔细阅读指导书后,按照我前两次的思路,新需求本质上只需再增加三角函数中内嵌表达式,对于函数的嵌套,我仍然采用预处理中将所有函数解析成表达式因子的思路,需要修改的地方是将原来的替换的方法改写成递归替换的方法,以应付函数嵌套的问题。
-
UML图与程序框架结构如下:
本次作业的方法和成员通过UML图看与第二次作业几乎没有区别,表达式解析、建树方式和层次、Variable形式均与第二次作业相同,不同点在于TraFunc类中添加了新的parse方法,用于递归解析三角函数括号内的表达式因子,Function类中的call方法改成递归解析的写法即可。
-
-
复杂度度量分析
Expr | 2.2 | 12.0 | 22.0 |
---|---|---|---|
Function | 2.6666666666666665 | 10.0 | 32.0 |
Lexer | 3.0 | 7.0 | 15.0 |
MainClass | 4.0 | 12.0 | 28.0 |
Parser | 4.0 | 9.0 | 16.0 |
Sum | 4.0 | 4.0 | 12.0 |
Term | 2.4545454545454546 | 14.0 | 27.0 |
TriaFunc | 1.6 | 5.0 | 16.0 |
Variable | 1.3333333333333333 | 3.0 | 16.0 |
Total | 184.0 | ||
Average | 2.4864864864864864 | 8.444444444444445 | 20.444444444444443 |
本次作业只在第二次作业的基础上做了轻量修改,复杂度问题与第二次作业相同,这也是本设计的一个不足之处。
-
关于测评
本次作业的我的测试思路,同样是在代码编写的过程中边写边测试,首先我在写完表达式中函数调用的递归后进行测试,只为检查递归调用最终结果是否正确,之后先测试第二次作业中的测试点是否正确输出,检查无误后,着重测试三角函数因子中嵌套其他表达式因子的情况。
-
中测强测互测
本次作业不幸在强测中有一个点出现RunTimeError,结果发现bug原因是没有在Sum函数中考虑到求和上界大于下界直接返回0的情况。
在互测中被hack了5次,原因有两个,一个和强测出错原因相同,另一个是Sum中上下界过大爆Int,将Sum中所有int类型数据改为BigInteger类型即可轻松解决,之后我观察了房间中其他同学被hack的点,也都是Sum中爆int的问题。
三次作业总结
三次作业的迭代开发,训练目标各有侧重,最终完成了如图所示的一个表达式解析项目(黄绿红分别代表1,2,3次作业实现)
在三次作业中,对于表达式解析,我都采取了不变的思路,即先进行去空格,去多于符号预处理,递归下降解析表达式的方式,在层次抽象上保持Expr->Term->Fator的抽象层次,新增要求要原来的基础上进行继承或添加新功能,并未进行重构。对于表达式输出与化简,我都是死板地进行先按Variabl标准形式输出,再进行字符串处理的方式,在表示化简方面下的功夫不够。
第一单元心得感想
-
pre作业中能感受到课程组中的用心,作业中提到的容器、设计模式、接口继承语法和正则匹配等的介绍对第一单元作业的帮助很大;
-
对于自己在第一单元的表现我并不满意。写第一次作业时自己一直在闭门造车,思想上并没有重视OO作业,导致我在设计阶段就耽误了较多时间,知道周五晚上才真正开始写代码,导致我最终交出了无效作业;其次,三次作业中对表达式的优化我在设计阶段都没有进行考虑,只是对最终输出的答案字符串进行面向过程式的浅优化;最后,在测试阶段没有尽太大力,内心只觉得通过中测就万事大吉,互测阶段也没有花太多时间,导致我在互测中吃了大亏,下一单元一定好好研究如何进行高全面性、高覆盖性的测试以及如何构造数据hack他人;
-
独学而无友,孤陋而寡闻,继第一次无效作业后,我吸取教训,积极与身边同学讨论设计思路和实现,无论是与同学们的讨论还是通过课程网站的讨论区,我都获得了许多设计思路上的启发,我的第二三次作业的架构和debug都在不断和同学的讨论中取得较大进展;
-
设计阶段和实现阶段都不能偷懒,即使通过了中测也逃不过强测和互测,每一个阶段都要尽力去做,此外,设计阶段还应有前瞻性,每次设计时应该对之后的迭代也要有规划。
-
在oo研讨课上虽然听了其他同学的设计,但自己还仅限于听,并未思考是否能将课上的收获用于自己的设计中;
-
研讨课上同学介绍了SOLID设计原则在其第一次作业中运用的例子,在此进行简单回顾:
在程序设计领域, SOLID(单一功能、开闭原则、里氏替换、接口隔离以及依赖反转)指代了面向对象编程和面向对象设计的五个基本原则。当这些原则被一起应用时,它们使得一个程序员开发一个容易进行软件维护和扩展的系统变得更加可能。
希望今后能将这一设计原则用于自己的设计中。