BUAA_OO_Unit1单元总结

魏东霖-22371041 学生 2024-03-21 11:11:59

Unit1 单元总结博客

前言

首先必须承认,OO课程还是蛮有难度和强度的。在第一次作业刚下发的那几天,由于从未听说过与使用过递归下降解析法,我被完成作业的思路折磨了很久。在实验代码和训练代码的提示下,终于开始动笔,但是过程也是非常坎坷,对于Term解析的返回,我又挣扎了一天左右。最终在那周的周六下午完成了基本的测试,忐忑的交上了第一次作业。

经历第一次作业的完整过程后,我发现自己对于自底向上的结构化建模似乎掌握了不少。面对第二三次的迭代需求,都能够先行分析出每一步大概要完成什么,向上提供了什么接口,上层又需要额外完成什么样的功能。这应该是我完成第一单元后对于“面向对象”这一概念感受最深的地方。第一次作业时的我还停留在曾经的过程式编程阶段,而这种概念很快被面向对象所刷新,这应该就是我这一单元收获最大的地方。

在本次博客中,我会先按照需求-实现的逻辑梳理我的每一次作业的完成思路,再给出复杂度分析和代码规模,最后做一个小总结。

第一次作业

需求:完成单层括号单变量表达式展开。

根据文法的定义,我们有Expression -> Term -> Factor这样的层次。而Factor最终又嵌套回Expression。使用递归下降解析法的好处是可以自然的根据文法解析所有的类型,同时支持多层括号。在实现上我没有做任何语义上的预处理,仅仅将所有的空白字符去除,剩下的交给递归下降法处理。

实现:基本单元的设定 + 递归下降解析

本次作业中,我的基本思路是:递归下降法,边乘边展开,基本项乘法,表达式加减

因此,我将Term作为基本项进行处理,其构造是coefficient系数+factors单项式集合。刚开始设计时考虑可能会有多变量,因此做了一些可扩展的处理。设计完基本项后,运算方法和合并方法就都可以实现了,可以说实现基本项是最重要、最关键的一步。另外一点,我在解析Term后不返回Term而是全部返回Expression,当时我的考虑是由于因子种类众多,方便上层统一处理,后续的迭代证实了这是非常方便、扩展性非常高的设计。

Term的设计如下:

img

使用Factor接口,使得在很多地方的处理上(解析返回时,储存时等)比较方便,同时也支持了后续的扩展。在递归下降的同时进行运算,保证每一步运算后的表达式都是最简的形式,可以简化后续的工作量。故我将乘方视为表达式的乘法,这样各模块的职责已经形成,如下所示:

Expression: 合并新解析的Term并储存
|-- Term: 解析过程中处理乘法(基本单元)
|-- Factors(Number, Monomial): 构成Term

UML类图:

img

可见,Term类中的方法主要是判断同类项与实现项的乘法的方法,其余的类中实现自己的乘法。这种实现降低了各类之间的耦合,维护和debug时比较方便。另外,使用重写的toString()方法可以方便的实现输出,也方便在调试时一目了然解析的结果。

各类的规模:

img

各类的复杂度统计:

img

各方法的复杂度:

img


img


img

可见,代码行数和复杂度主要还是集中在ParserTerm中。Parser类中的方法由于递归调用和较复杂的分支判断导致认知复杂度很高,不利于阅读和理解。这也促使了我在第二次作业中对于Parser类结构的重构。

架构设计的优缺点:

优点:简单直接,合并同类项时直接遍历搜索,乘法时全部使用基本项,思路上非常清晰。

缺点:优化不足。首先是复杂度上,使用ArrayList储存同类项的信息需要遍历,O(n)的复杂度,不如使用HashMap进行键值对的储存高效。且输出时没有做正项提前等优化,导致性能分受损失。

Bug情况:

强测正确性全部通过,性能分有些微损失。互测没有被hack,也没能hack到别人。联系代码复杂度和规模,本次作业中代码结构仍然比较清晰,且方法的数量较少,相对不容易出现bug。

测试方面,使用了自动评测和手动评测相结合的方式。手写了一些比较边界的测试点(重点放在包含1,0这样的数据,比如(0)^+0, (0*x)^+1这样的针对性数据),同时使用自动测试覆盖性的做了一些测试,利用数据的随机性对程序的正确性做检验。

第二次作业

需求:自定义函数解析+因子增加指数函数,实现:字符串替换+基本项完善

本次作业没有大规模重构。简单的将Parser类进行了结构优化,发现第一次作业的结构仍然可用。使用字符串替换的方式完成函数的解析的好处在于计算部分仍可以使用第一次作业的内容,只需要增加指数函数的乘法和修改同类项的判断。

基于这些思考,我新增了Preprocessor类,完成自定义函数的储存,字符串的替换工作。实际上的内容与Parser相近,只是解析的每一部分全部返回String即可。在字符串替换的方式中,有一个部分需要额外注意,就是对于exp这个token的保护问题。同时,考虑到形参的出现顺序不固定,因此必须将x替换为t进行保护。因此,本次作业的一个关键在于怎么有效的储存自定义函数。我新建了Function类,储存函数的名称,形参以及对应的表达式。在读取到对应的函数后,使用正则表达式匹配以上几项内容,同时将表达式中的exp替换为espx替换为t

基本项的处理上,Term中增加了一个数据域,用来储存指数函数的相关信息,同时增加对应的乘法法则(实际上就是表达式的加法)。

在增加支持指数函数的乘法后,一个问题在于合并同类项。判断指数的表达式是否完全相同的方法复杂度较高,且涉及最后的优化问题。由于没有想到一个非常简洁的优化方法,最终只是完成了合并同类项,没有进行更多。

UML类图如下:

img

新增的方法主要集中在Term类和Preprocessor两类中,用于判断是否是同类项,以及指数函数的指数部分是否完全相同。所有新增的方法以斜体显示。

代码的规模如下:

img

相比第一次作业,增加的行数集中在Term类中。一个原因是判断同类项方法和合并时需要特判和遍历,比较繁琐。

复杂度分析如下:

img

img

img

可见,除了与上次作业中大致相同的Parser类中的高复杂度方法以外,Expression.isIdenticalExpression方法是新增的高复杂度方法。原因和上面说的一致:需要遍历表达式的所有项,复杂度很高。但是由于没有更好的解决方法,优化就暂时搁置了。

架构设计的优缺点:

优点:简单直接,思路上非常清晰。包括乘法实现、字符串替换处理函数,以及合并同类项,且各处向上返回时都保留了上一层的接口,扩展性高。同时可以支持求导的需求。

缺点:时间复杂度高,特别是判断同类项时。另外,优化缺失,没有探索最优的输出方案,导致最终输出长度长,性能分较低。

Bug分析:

强测挂一点,互测被hack两次。原因是我优化不当,导致形如exp((-x))的输出为exp(-x),不符合形式化定义。互测中也是挂在这里,很遗憾。对于优化与正确性两者的关系,有待更深层次的思考。

测试方面:

使用自动测试与手动测试。手动测试主要以构造包含0和1的数据和包含0的嵌套的自定义函数为主,重点测试乘0和幂0两种情况。自动测试为覆盖性测试,使用大量随机数据来测试程序的正确性。

第三次作业

需求:定义时的自定义函数嵌套+求导,实现:基本项求导+多次字符串替换

在上次作业的基础上,我使用了一种比较简单粗暴的方法处理定义时函数嵌套——多次替换。考虑到自定义函数出现的个数最多为3,因此最多只需要替换三次,就能将表达式替换为不带有嵌套的自定义函数的情况。

最终,我的Term基本项表达如下:

img

在求导方面,需要处理好基本项的求导法则(乘法法则)和表达式的求导法则(加法法则)。这时全部返回表达式的Term解析方法就非常方便,求导后本身就是表达式,无需再对架构做任何修改。于是,完成本次作业只需要加上5个derive()方法即可。

UML类图如下:

img

可见,多出的方法只有derive()。在处理嵌套定义的自定义函数方面,甚至不需要在Main类外做任何改动。

代码规模如下:

img

整体的代码规模也没有什么变化,只有100行左右。主要分散在各个类新增的derive()方法。

代码复杂度分析:

img

img

img

img

复杂度较高的方法与第二次作业基本相同,还集中在判断同类项和解析表达式上。注意到两个toString()方法复杂度也很高,这是我优化不当的缘故,在输出时有非常多的特判(对于符号是否要输出,系数如何表示,是否需要乘号连接等),导致认知复杂度较高。

Bug分析:

强测正确性通过,互测没有被hack。没什么特别要注意的,在保证上一次作业正确性的基础上,只要认真写好求导方法即可。这时Term基本项中包含带符号的系数非常重要,可以保证在求导时的符号正确性,是这个架构的另一个优点。同时由于上次优化失误导致强测出现问题,这次我选择优先以正确性为主,不强求性能方面的优化。

架构的优点和缺点:

优点:整体的功能类数量比较少,核心比较精简。同时可扩展性强,各接口的设计保证了增加新的需求时无需大改架构。

缺点:优化仍然欠缺。最终的输出没有对于指数函数的指数部分做任何优化,导致性能分受损。

总结

正如在前言中提到的,本单元给我的提升可能不在于这1000行代码,更多的在于这种设计面向对象程序的方法。无论是递归下降解析的应用还是分模块形成基本项再进行操作的思想,再到每次都有去考虑的各层次的接口设计,实际上是我从面向过程编程向面向对象编程的重要转变。尽管这三周的过程有一些痛苦,(可能第一周尤为令人记忆深刻)但是完整经历过后,会真正觉得自己在设计代码结构的能力上前进了一大步。

若谈及如何改进第一单元,我想可能在第一次作业时多给一些架构上的提示会更好。可能对于我这种代码能力较弱的人来说,从没有接触过递归下降到完整写出递归下降解析表达式的程序,还是需要一定的时间去理解的,不过好在有训练代码和实验代码()

致谢

感谢我的舍友们,在第一周我面对空白的IDEA完全手足无措时给了我很多思路上的帮助。同时感谢助教们,解决了一些可能逾越我个人能力的问题。感谢每一个在这三周时间里或多或少帮助过我的人。

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

301

社区成员

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

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