BUAA2024春-OOUnit1总结

​ 第一单元的主题是表达式展开,要求依据文法定义对输入的表达式进行解析,进行必要的计算后输出。第一次作业要求对表达式进行括号展开;第二次作业新增指数函数自定义函数的展开和运算,并允许括号嵌套;第三次作业新增求导因子,并允许“自定义函数的定义式中出现已定义的其他自定义函数”。

程序架构分析

uml类图

三次作业后最终的架构如下。

在这里插入图片描述

  • 表达式解析类

    • Parser类和Lexer类负责通过递归下降法对输入的表达式进行解析。Lexer获取输入的字符串,像一个“指针“沿着字符串移动,返回读取的信息;Parser接受信息,依据文法解析生成不同表达式成分的对象。
    • ExprTermBasicFactor为表达式中不同层次组成成分的类。除指导书中表述的表达式、项、因子三个层次外,我在项和因子间插入了基本项(Basic)层次,对应幂次运算符^,而将因子定义为不含指数;这样就可以根据运算划分表达式层次,每个层次对应一种运算符:表达式(Expr)可以看作是项(Term)的集合,项之间通过+-连接;项看作基本项(Basic)的集合,基本项之间通过*连接;基本项的形式是因子^因子
    • 因子(Factor)定义为接口,其中定义了计算(toPoly())和代入实参(substitute())方法。每种因子都实现这个接口,从而实现了对多种因子的统一管理。
    • FunctionDefine为静态类,负责解析自定义函数的定义式。Process负责对表达式字符串进行处理,包括去除空白符,合并连续±号,去除指数前的+号等。
  • 表达式计算类

    • Unit类为计算时的最小单元,形式为:
      a ∗ x b ∗ e x p ( P o l y ) a*x^b*exp(Poly) axbexp(Poly)

      • 由于指数函数运算的特殊性,多个exp()相乘可以合并成一项,因此可以将Unit定义成如上简洁且方便运算的形式。
    • Polyno类负责多项式运算,通过ArrayList管理组成它的Unit,实现了加减、乘法、幂次、求导运算,并在运算过程中完成合并同类项。

优缺点分析

  • 优点:我的架构基本符合高内聚,低耦合的思想,从而有较好的鲁棒性和可扩展性。
    • 解析和计算解耦:
      • 在解析和建立表达式树时,并不关心计算的问题,只是忠实地记录读取到的信息。
      • 在计算时,所有表达式成分都是Poly,依据不同的运算规则完成运算即可。
    • 计算的高内聚:
      • 我将自定义函数代参求导也视作一种计算,在顶层只需要调用toPoly()方法计算即可,不需要关心都需要进行哪些运算。
  • 缺点:
    • 一些方法的实现比较啰嗦
    • toString()方法直接打印x的字符串,如果需要输出多变量表达式则可扩展性很差
    • 基本没做优化

度量分析

  • Method Metrics(只展示复杂度高的方法)

    在这里插入图片描述

    • UnittoString()方法中要对系数、指数的各种情况进行复杂的特判从而实现输出优化,因而复杂度飙升。
    • ParserparseFactor()方法要对各种不同的因子类型进行判断,通过不同的方法构建各种因子的对象,因而复杂度非常高。
    • 除此之外,其他方法都较为简洁。
  • 代码量

    在这里插入图片描述

架构设计体验

  • 第一次作业
    • 初步实现了解析与计算的解耦,并将这一思想沿用至后两次作业;
    • 解析表达式时采取递归下降算法,天然支持括号嵌套,并在需要加入对更多类型的因子的解析时有很好的可扩展性;
    • 计算表达式时,Polyno类中我选用HashMap<BigInteger,BigInteger>管理运算的对象,以指数作为key,系数作为value。这样在合并同类项时非常方便,但无法支持更多种类多项式的运算(如后面加入的指数函数),可扩展性较差。
  • 第二次作业
    • 由于指数函数的加入,原先HashMap的数据结构已经无法完成计算,因此引入了Unit类,将Polyno看作Unit的集合。
    • 在实现指数函数的运算时,主要在UnitPolynoequals()方法上遇到了问题:
      • Unit中指数函数的指数部分存储为一个Polyno类对象,而Polyno类又是Unit类对象的集合;在判断Unit相等时要判断Polyno相等,在判断Polyno相等时又要判断每一个Unit相等,无穷地递归下去。解决方法是定义Polyno中存储UnitArrayList为空时值为0,并将其作为递归的终点。
      • 解决了无穷递归,在合并同类项时又出现了问题,原因是混淆了UnitPolynoequals()的定义。在我的代码中,Unit.equals()用于判断两个Unit是否可以合并,而Polyno.equals()则是判断两个多项式是否完全相等。如果在Polyno.equals()中调用每个Unitequals()方法就会出现错误。将判断两个Polyno相等的方法改为计算二者相减后是否等于0就解决了这个问题。
    • 在自定义函数的处理上,我没有采用字符串替换的方式,而是进行对象层面的替换。我的想法是,将“代入实参”视作是自定义函数因子计算行为,在toPoly()方法中实现,这样就达到了行为上的统一。
      • toPoly()方法流程:
        • 解析形参表达式
        • 调用substitute()方法,传入形参实参映射表作为参数,通过递归层层往下,再层层返回
          • 返回一个Polyno类对象
          • Expr返回所有Term.substitute(…)相加的结果
          • Term返回所有Basic.substitute(…)相乘的结果
          • Basic返回Factor.substitue(…)再幂次的结果
          • 对于Factor:
            • 常数直接返回自己
            • 变量返回根据映射表得到的实参的toPoly()得到的结果
            • 指数Factor返回它的指数部分上的factor调用substitute()得到的结果

​ 这样写完以后足以通过第二次作业,但其实有一个问题悬而未决:自定义函数因子作为一种表达式成 分,它也应该实现substitute()方法,但在此时我没有想出合理的做法,只是粗暴地将其和toPoly()方 法等同。这就为第三次作业支持形参中的自定义函数埋下了地雷。

  • 第三次作业

    • 求导的实现非常容易。我依旧贯彻“解析与计算解耦”的想法,并没有对每个表达式层次实现求导,而是直接在Unit类和Polyno类中实现求导方法,从而将求导也蕴含在了表达式的toPoly()过程中。

      (当然,这得益于本次作业中Unit的形式简单。如果像往年出现三角函数的累乘,这种做法就行不通了。)

    • 对于自定义函数,第三次作业新增自定义函数可以作为形参。由于我之前没有实现自定义函数因子类的substitute()方法,无法将形参表达式中出现的自定义函数进一步替换;虽然看上去只是个小问题,但真正着手去实现时却发现困难重重,转眼间距离重构的泥潭仅有一步之遥。为此我苦恼了很久,在参考了一些博客和讨论区的帖子后,最终决定不进行大规模的重构,而是将替换计算进一步解耦:

      • substitute()不再返回Polyno,而是返回void,只替换,不计算。

        (狠狠夸夸xmgg的讨论区帖子“命令-查询分离原则及其在接口设计中的应用”,醍醐灌顶,把我从前仅停留在感性直觉上的认识剖析得一清二楚)

      • 变量因子记录自己是否是形参(private boolean isFormal),如果是,则记录下对应的实参表达式(private Expr actualPara)。actualPara默认为空;如果不为空,且此时isFormal为真,则说明需要对actualPara进行进一步的替换。这就实现了对形参中出现的自定义函数的深层替换。

        (这里感谢yyb同学的讨论区帖子给我的启发)

​ 尽管期间付出了大把头发,最终的改动却只有寥寥数行,终于实现了这一要求。

  • 新迭代情景——多变量 (只要文法定义不变,解析部分就不需要大改,只需要更改计算部分)
    • Unit中需要储存每个变量的指数和系数,equals()方法也会更为复杂
    • toString()输出时不能直接输出字符串x,可能难以避免与变量因子类的耦合
    • 多变量下的偏导和全微分问题o_O

程序中的bug

​ 三次作业中,仅有第一次作业在强测和互测中被发现了bug,原因是Parser类中的parseExpr()方法在解析表达式时没有考虑表达式开头可能出现的符号。由于我对表达式进行了预处理(如果整个表达式以符号开头则在最前面插一个0),就忽视了这方面的处理,而在处理括号中的表达式因子时就会暴雷。

​ 其余两次作业强测和互测都没有发现错误。(虽然性能分被干碎了)

发现别人的bug

​ 由于代价函数的限制,互测中很难构造出压力测试(当然有大佬构造出来了orz),主要通过构造一些嵌套来增加数据的复杂度。第一次作业进了C房菜鸡互啄,在两位同学的代码中发现了输出优化导致的错误,针对这个问题构造了可以发现bug的测试数据。

优化

​ 能力所限,我只进行了一些最基本的优化,包括系数为1或-1时省略,指数为1时省略,为0时省略变量x等。对于指数函数没有做任何的优化(省略括号,提公因数等),一切以正确性为重

心得体会

​ 整篇总结遣词造句已尽可能精简,写到现在却不知不觉已经洋洋洒洒近三千字,仅聊以记录一个月来在oo作业上的思考。oo第一周的作业可谓当头一棒,尽管已经经历过oopre的洗礼,却还是被这一棒敲了个晕头转向。期间经历过无数个闭上眼还在脑内断点调试的夜晚——好在最终还是平稳落地了。有些遗憾的是在优化上没有多下功夫。

加深了对递归的理解。本次作业中大量用到了递归调用,使我进一步加深了对递归思想的认识。递归思想就是“相信后人的智慧”。

多多关注讨论区。讨论区很多同学的见解都非常有启发性,让我受益良多。

建议

​ 我认为第一单元的设计已经非常好了,从迭代设计到实验、训练、公众号文章的架构引导都足见课程组投入的心血。尽管hw1一上来,以及hw1到hw2之间的难度曲线较为陡峭,但经过一些努力依旧可以较好完成。

  • 18
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值