BUAA OO第一单元总结

范兴堃-22371426 学生 2024-03-22 09:47:51

OO第一单元总结

第一次作业

本次作业需要完成的任务为:读入一个包含加、减、乘、乘方以及括号(其中括号的深度至多为 1 层)的单变量表达式,输出恒等变形展开所有括号后的表达式。支持前导0和前导正负号

如何看待表达式

在实验预习中,已经给了我们提示,可以将表达式看成如下形式:

$$表达式 = 项 + 项 + 项 ...$$

$$项 = 因子 * 因子 * 因子 * ...$$

因子共有三种情况:数字因子(单独一个数)、变量因子(**$x^{指数}$**)、表达式因子($(表达式)^{指数}$)。其中指数用^表示,如x^5(x+1)^2

可以发现:最顶层的表达式和最底层的因子(表达式因子)有着非常强的关系,这也是递归下降的含义。

以下将表达式(因子)称为Expr、项称为Term、因子称为Factor、数字因子称为Number、变量因子称为Variable。

这里需要注意的一点是表达式的构成中,项之间只是相加的,而没有相减——这是因为我给每一个元素(Expr,Term、Factor)都加了一个symbol属性,如果是减法,直接在其属性上体现出来。——这一点我在最开始的时候没有考虑到,导致第一个版本出现了很多的bug,为了给其中一些地方打补丁,我甚至做了这样的替换:凡是输入中出现-的地方,都替换为-1*——但是这样以来,又会出现一堆其他问题,需要一堆特判,代码写得越来越长,越来越复杂,于是在中测已经过了的情况下,第一次作业就进行了一个小重构。——但也正是这个symbol属性的不正确使用,给我的第三次作业带来了巨大的灾难,这里先埋下一个伏笔。

思路分析

以下是本次作业的UML类图:

img

我将这个解析表达式的任务分成三层业务逻辑:

  1. 解析输入,返回一个Token的数组以便后面进行处理——为此设计了Lexer类;
  2. 根据tokenList,解析表达式,得到表达式树——为此设计了Parser类;
  3. 对表达式树进行合并于计算,以便能得到我们最后要输出的结果。

至于类的设计,让ExprVariableNumber实现Factor接口,Term中包含一个Factor的容器,Expr中有一个Term的容器,同时有一个exp属性代表表达式整体的指数。

Lexer——解析输入

这部分的目的是将读入的表达式(String类型)转化为Token的数组。

Token的意思是最小语法单元,我认为在这一步有如下几种:

  1. 数字(并且是解析好的数据——即去除了前导0,前导正负号);
  2. 变量x
  3. 各种符号:*+-()^

前两种都已经实现了相应的类,所以我又单独创建了一个Operator类,用来记录各种符号的内容。

但是这样做还有一个问题,那就是上面这三种数据类型是无法放到同一个容器中的——所以我在最顶层添加了Token接口,让Factor接口继承Token接口,并让Operator类实现Token接口。

这一步额外需要进行一下预处理,我主要做了两点:

  1. 删除所有原始输入中的空白字符——显然在表达式中空白字符并没有什么特殊含义;
  2. 将连续的正负号相合并,这里主要用到栈的想法,当读到一个+-时,判断当前tokenList的最后一个元素是不是+-,如果是就合并——这样就保证了经过处理后的tokenList中不会出现连续的正负号,后面解析时会减少很多不必要的思考。

在这一步之后,我们就得到了Token的数组tokenList

Parser——解析tokenList

本阶段的任务是以上一步得到的tokenList为输入,输出一个表达式对象——由于会有层次嵌套,实际上这是一个树形结构。具体的实现层次如下:

img

这也是递归下降的含义。

这里有一点要注意:当解析表达式因子时(读到(),有两种方法:

  1. 找到匹配的右括号,然后看看后面是否有指数,将指数记录下来后再回来解析表达式;
  2. 直接调用parseExpr,然后再用setExp修改得到的Expr对象的指数。

第一种方法要注意的一点是:在第一次作业中并没有嵌套括号,所以“匹配的右括号”就是下一个右括号,但是下一次作业支持嵌套括号时,这种方法就要改成压栈的方式去找匹配的括号了。

Poly——统一多项式结构

现在我们得到了表达式树,但是各类在数据上并没有什么相似(这也是为什么我们实现了接口而没有用继承的原因),很难在此基础上进行运算合并。

其实无论是哪种因子或者项,其本质都是一个多项式,我选择新建一个Poly类代表多项式,内置加法乘法等方法。并给每一个类都写一个toPoly方法——这样对最顶层的Expr调用后,这也将递归下去进行合并。

如此,便得到了我本次作业的架构,也即上面的UML类图。

架构特点

如上所述,我的这种架构将题目的业务逻辑严格分成了三层,层与层直接几乎没有多余的关联(Lexer的输出作为Parser的输入,而Parser解析的结果作为输入调用toPoly方法去构造Poly对象,最后调用PolytoString方法进行输出)——最直接的结果就是:后两次迭代过程中,每一个新加的业务逻辑都能精确地对应到这三层上面来,只需要做很小的改动(有的层不需改动),就可以实现新增的业务要求——可拓展性非常好。

输出优化

第一次作业比较简单,输出优化也很容易想到,主要有两点:

  1. 以1或-1为系数时,可省略系数;
  2. 可以将系数为正的项提到第一项输出,这样可以优化掉一个-

bug相关

一开始出了一堆bug,主要原因是最开始的设计里没有synbol这个属性,我在Lexer里面做了很多自认为很“天才”的设计去避免这个问题,导致一直在打补丁,而且越打越多,重构成现在这个版本后,全程几乎就没怎么出bug了。由于第一次作业比较简单,我们room里也没有成功hack到彼此。

第二次作业

本次作业的UML类图如下:

img

新增需求与解决策略

相比第一次作业,本次作业主要增加了三个点:

  1. 允许嵌套括号;
  2. 新增自定义函数;
  3. 新增指数函数因子

对第一点,由于采用递归下降的设计,已经自然而然被解决了,唯一需要注意的一点是我在第一次作业中强调的有的设计里压栈匹配括号的问题 。

对第二点,我的思路是单独设计一个Function类,存放自定义函数,然后在Lexer做处理时,先对所有自定义函数进行替换。这里有两点需要注意的地方:

  1. 替换时不要吝啬加括号,替换后的整体也要加,防止出现意外;
  2. 对x的替换要特殊一点,因为exp中也有x,所以可以考虑把所有的exp换成e,再把所有的x换成随便一个字符(因为函数的形参是什么字符并不重要),再把exp换回来即可。

对第三点,要改动如下的位置:

  1. Lexer解析时,要将exp看成一个整体,作为Operatorcontent属性去构造;
  2. 构造表达式时,要多去考虑expenont类的解析;
  3. 由于单项的一般形式变成了$coefficient * x^{variableExp} * exp(exponentContent)$,所以直接用一层HashMap不管用了,我就单独创建了一个单项式类Monomial,将多项式看成多个单项式相加。同时,为了合并可以合并的项,还要判断两个单项式是否可合并,多项式是否相等——需要重写equals方法和HashCode方法。

可见:由于第一次的架构逻辑非常清晰,本次迭代所改动的地方相对较少而且逻辑很清晰。

bug相关

本次的hack策略主要是尝试不同的新增组合,比如exp(f(x))这种。

第三次作业

本次作业的UML类图如下:

img

新增业务

本次增加了两个业务需求:自定义函数中调用自定义函数,以及求导因子。

对第一点:我的策略还是能尽量少修改就少修改,所以我在每次读完一个自定义函数后,直接对其调用Function类的静态方法expandFunction——直接将其展开——这样,在后面的所有设计都不需要修改的情况下,就完成了这个任务,总共修改不超过10行。

对第二点:显然Lexer解析Token时要特判一下dx的情况,将其看成整体;同时要对各类添加derive方法进行求导运算,在上机课给了提示的情况下,也没有什么难度。

bug相关——私有的含义

本次我在调试时出现了“灵异事件”——单步调试的结果和直接运行输出不一样,后来知道是单步调试时自动直接调用了toString方法——由于我为了方便调试,其实给每一个类都写了toString方法,而为了方便,我的ExprTerm类都有mergeSymbol方法,而且还有setPositivesetSymbol方法——虽然属性是私有的,但是对外的接口这么多已经导致他根本就表现得像是公有的一样。

为了方便,我在第一次作业里频繁地使用了mergeSymbol方法,但是在本次求导时,由于要判断各项的正负,所以symbol属性是相当重要的,但是频繁地修改,导致出了一堆bug,最后我非常谨慎地删掉了一堆调用。

这次的debug经历也让我认识到了mergeSymbol这种改变了多个对象的属性的方法的危险性——会直接把“私有”的含义破坏掉——这可以算是本单元作业的最大收获了吧。

同时,本次测试中,着重测试了替换函数的顺序——上次要把x换成一个不相关的字符,本次y和z也同理——因为函数中可以调用自定义函数了,有的同学这个地方欠考虑导致出错了。

第二次和第三次作业优化

在第一次基础上,主要有两点:

  1. exp里面的括号有可能会去掉,有这三种情况:里面就是一个数字、系数为1的变量、单独的指数函数
  2. exp里面的多项式可能会提出公因子——考虑将他们的最大公因子提出来。

可能还有别的优化方向,但是我没有卷那个性能分,就做了这些最简单的优化。

指标分析

img

img

img

img

img

img

学习体会

开学初就体验到了OO课程的巨大压力,尤其是第二次作业写得是真要崩溃了,好在最后写完了并且强测都没挂过。

通过本单元的作业,算是初步地体会到了面向对象编程的思想,尤其是看到自己最后的架构画出来的UML类图,不禁感叹原来自己写的程序看起来也有点复杂了。

我觉得最大的教训/收获还是第三层次作业的那个mergeSymbol方法上——这一点算是让我深刻地理解了为什么面向对象的设计里要有privatepublic,真是惨痛的教训,以后再写程序的时候也坚决不会再图方便随便设计改变属性的接口了。

总体而言,这三周的任务量还算可以接受,回过头一看,没想到自己写了1300多行代码,也没想到这么多代码却只实现了这么小的一个功能,算是初步体会到了OO的强度与刺激吧。

未来方向

感觉互测环节设计得有些多余了——直接强测把bug都测出来就好了。本身写完OO就到周末了,还要为了互测耗费一些时间,感觉没有什么必要。建议把互测这个环节取消掉,节约一些大家的时间。

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

301

社区成员

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

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