BUAA-OO-U1

Dawn

随橙想呢,本人在U1结束之后才弄懂U1在做什么……

自我点评

扪心自问本人的架构非常差劲,解析和输出耦合在一起,导致低内聚高耦合、超级巨类、重复计算等问题。类图和具体迭代过程将在下一板块展开,此处仅介绍最终设计中每个类的设计目的。

  • MainClass :进入程序,处理输入输出逻辑
  • Parser :语法分析,实现递归下降
  • Lexer : 词法分析,实现读入和跳过
  • Expr : 存储、化简项和表达式
  • ExprOps : 因为checkstyle原因不能超出500行所以分出的方法类
  • ExprPrinter : 结果输出逻辑
  • Variable : 枚举类型,处理多变量
  • Factor : 接口,统一管理Factor的求导、化简行为
  • DerivativeFactor : 管理求导因子
  • ExpFactor : 管理指数函数因子
  • ExprFactor : 管理表达式因子
  • FunctionCallFactor : 管理自定义函数因子
  • NumberFactor : 管理数字因子
  • PowerFactor : 管理幂函数因子
  • SeleFactor : 管理选择表达式因子
  • VariableFactor : 管理xy变量
  • FunctionDef : 处理函数调用操作
  • SimpleFucntionDef : 普通函数定义
  • RecursiveFuntionDef : 递归函数定义
  • RecurrenceTemplate : 递归函数存储化简
  • NormalizeContext : 化简操作
  • MonomialKey : 管理项

架构设计体验

逐步成型?逐步毁灭!

第一次作业

前文已经提到本人此次作业的架构非常差劲,而差劲的根源从第一次作业就埋下了。

在第一次作业中,本人未能深入理解AST树和真正的代码实现的关系、轻率地使用AST树的架构给我的类进行区分。所以本人并没有使用 Mono 类和 Poly 类,而是将它们全部用 Expr 存储。

图片8

因为第一次作业比较简单,所以这样的架构也没出大问题。

第二次作业

第二次作业在第一次的基础上增加了自定义函数、指数函数因子和选择式因子。

图片7

在第一次作业本人曾用HashMap存储项,将因子的指数作为键、系数作为值。增加了指数函数后这种方法随即失效,所以本人增加了一个类来管理项,即 TermKeyTermKey 中有两个成员变量,分别用BigInteger存储 x 的指数部分和Expr存储exp内部因子,再将 TermKey 作为键、系数作为值来存储项。

其实这个时候本人就开始困惑:我的Factor并没有参与数据的管理,所有的数据都存在TermKey和Expr里,那Factor存在的意义是什么?我的Term似乎没有任何作用,这样真的对吗?但当时本人并没有细究,这种不求甚解的态度再次为第三次作业埋下败笔。

第三次作业

第三次作业增加了新的自变量、自定义递推函数和求导运算。

图片6

看得出,这设计有种为管理而管理,为分层而分层的意味。复杂而冗余的设计让人无法debug,所以最后强测被肘进了o房。

值得一提的是在实现链式法则时本人override了一堆Factor类的方法,竟然让xxxFactor的设计显得不那么冗余,甚至还能自圆其说xxxFactor存在的意义是为了分发求导操作。

新的迭代情景

设想若增加三角函数,在现有架构下大致需要修改:

  1. 增加 SinFactorCosFactor
  2. MonomialKey 中增加相应存储、比较逻辑
  3. Parser 中增加相应匹配逻辑
  4. Expr 中加入相应 toString 逻辑

但是现有架构太过庞杂,极其容易失手,事实也证明确实失手了。仔细分析后发现之前的数据存储逻辑其实应该变成数据处理逻辑。因此所有 xxxFactor 类都是不必需的,上述类图中绿色的部分都可以删掉,所以最终得出的架构应该长这样。

重构,在单元结束之后

图片9

这个设计与我之前设计的区别在于:

  1. 把原来由 Factor 管理的数据统一交给 TermKey ,即 TermKey 中管理自变量的指数和exp内部因子。
  2. 把原来由 Factor 进行的操作统一交给 Poly ,即语法树的建立体现在 Poly 的成员方法中。
  3. 对函数的处理在 MainClass 读入时就完成,不建立多余的类。

这样的层次结构更清晰,更利于测试和维护。

自己的bug

第二次作业

描述

第二次作业的两个bug都出在对选择式因子的处理上。

  1. 对Factor没有实现统一管理,在bug层面体现在会将 (x-x)0 判成不等。
  2. 先解析四个因子再进行判断选择,会造成TLE。

ev(G)=6

修复

  1. 弃用Java自带的equals方法,改为判断 FactorA-FactorB 是否 equals.zero
  2. 增加skip方法

第三次作业

描述

第三次WA了两个点,T了一个点。总结起来是两个问题。

  1. 函数调用的缓存没做好。最开始的逻辑是打印时调用 toString ,这部分没做好缓存导致同一 Expr 被反复计算,造成TLE。
  2. 函数定义时先读入 f{n}(x) 时先神秘读入一个符号,再读入表达式。此时如果表达式第一个符号是负号就会出错。ev(G)=4

修复

  1. 在Expr中加入一个 String 类型的成员变量 cachedToString 用来缓存表达式,在 toString 时先查有无缓存,无缓存则计算后存入缓存,有缓存则直接使用。
  2. 把读入符号那步删掉就好了。

能否通过更好的设计避免这样的问题?

假如本人按照上述架构(重构版)进行设计,应该不会出现第二次作业的第一个bug和第三次作业的第二个bug。在学习别人代码后,本人发现有一种很巧妙的思想,用到我的架构里来大概是在 MonoPoly 之间加一个 LazyPoly ,只标记不运算。在 Poly 中加入一个缓存层,被算过的表达式不重复计算。这样可以解决第三次作业的第一个bug。

部分思考题

测试策略及有效性

显然本人的测试做的不到位,否则不会出现如此低级的bug。

本人的测试方法是使用评测机测试代码,但我的数据生成器写得很弱,覆盖的范围不够广,以至于没有测出自己的问题。加上之前也不懂得根据度量结果设计数据生成的方式和规模,所以盲目相信评测机,没有对自己的代码进行足够的静态查错,最终出现了匪夷所思的bug。

本人的数据生成是根据文法做的,会分别生成因子,用因子组合成项,用项组合成表达式,但函数尤其是递归函数生成的不好。比如我第三次作业的bug出在 f{n}(x) 这一项的第三部分,而本人的数据生成器第三部分恰好之会生成0或1个项,所以完完全全没测到这个bug。

优化

本人在HW2的第一版中采取自认为相当激进的化简策略。当时使用了两种策略,一是遍历0到200的质数,尝试提取gcd;二是根据单项式前的系数部分提取公因式。比较两个策略得到的化简结果长度,输出较短的一个。但是这种化简结果在第二周周二晚上被测出有奇妙bug,当时我来不及改了,就只保留了第二部分。

HW3并没有进一步优化。

本人的化简在一定程度上保持了简洁,但在正确性上有所欠缺。简洁是因为我把所有化简逻辑放在一个类中,正确性欠缺是因为我代码能力暂时不支持我完成复杂逻辑操作。

思来想去感觉还是本人动手之前并没有明确自己想实现的功能,在优化过程中迷失了方向。在下一次作业中力求明确每个方法的功能与接口,正确写出真正简洁正确的代码。

大模型

  • 可以说本人大部分的代码是ai写的。本人的作用大概是给prompt,指明每个类需要储存什么数据、实现什么功能。性能优化同样是本人给优化方案,ai实现。
  • 用ai搭建了评测机,但ai只负责代码实现,本人来提供数据生成逻辑和指定输入输出接口
  • 大模型在代码层面的完成效果本人认为不好。ai生成的代码码量很大,review起来工作量也大。而且ai生成的代码往往会在很细微的地方出问题,当然也可能是因为本人提示词写得不好。
  • 因为本人已经删除当时下载的文件,没法对应到具体同学。ai写的代码一般非常严谨,即对输入合法性非常敏感。考虑到有佬会对输入合法性作出限制,所以如果测试中看到某位同学的代码对错误输入敏感且文件大小很大,本人一般会怀疑这位同学的代码出自ai之手。

心得体会

因为HW3出了两个严重的bug被肘进o房,我曾一度无法面对关于OO的所有。修复阶段增加一些逻辑就能解决的bug也让我困惑自己的架构到底是好是坏。但多亏怕什么来什么盼什么没什么,在充分学习了别人的架构后我终于认识到自己的问题所在。

一个好的架构应该是类各得其所,方法各司其职。这样的设计有助于程序员进行操作、修改、扩展,也能保持开发过程中心情的愉悦。

我最大的错误应该是在HW1的时候没有花心思理解区分语法层面和操作层面对字符串处理的区别,没有明确自己的处理逻辑。所以一头雾水地开始、写出了让人啼笑皆非的bug、最后在强测结果出来时悔不当初。

未来规划

总结我失败的第一单元,我的评价是尽量想清楚每个类的作用再开始写代码,如果最初没能做到,该重构也不要犹豫。

Comments