OO第一单元总结
第一次作业
思路
使用正则表达式不断读取因子(本次作业中只有幂函数因子和常数因子),若干项的乘积为系数和指数描述的单项式。单项式集合为多项式,内部用<指数,系数>的hashmap存储。求导时直接对每一项求导后加和。
UML结构

在第一次作业中我使用表达式(多项式)-单项式-因子的层次结构设计类。分别对应了代码中的Polynomial类,Monomial类和Factor类。同时设计了两个构造类(参考pre作业中的工厂方法),分别用于构造Factor和Monomial。我认为在我的第一次作业中已经有了递归下降法的雏形,即一步一步分解读取表达式。本以为两个构造器类可以用于之后的作业,构造不同的项和因子,具有一定的可拓展性,但最后还是重写使用递归下降完成。
复杂度分析

可以发现我的代码中比较复杂的有tostring方法和两个构造方法,前者复杂度较大的原因是我在toString的同时还进行了输出的优化,如不输出前导1等,逻辑路径相对复杂。后两个方法中为了解析字符串,使用了很多的条件语句判断输入格式,造成复杂度较大。本次作业类较少就不分析类复杂度了。
优缺点
第一次作业的代码较为简单,我的代码可以很好的完成任务,并且由于架构简单,可以方便的完成求导,优化等操作。但是由于简单的将表达式建模为了<系数,指数>的形式,导致我的代码可拓展性很差,在第二次作业直接重写。
Bug分析
本次作业比较简单,在强测与互测中均没有发现bug。
第二次作业
思路
本次作业的parser部分我采用递归下降的方式完成,依次调用了readExpression,readTerm,readFactor逐层解析表达式。在解析后,我的每一个Term用系数,x幂次,cos幂次,sin幂次,表达式因子的hashmap存储。而表达式由Term的hashmap构成。求导时我对每一项分别求导后求和,在对特定Term求导时,利用了形如
的公式进行运算。
UML结构

第二次作业中我的类设计和第一次比有了较大变化,设计了Factor类作为抽象类,同时有若干具体的因子类作为Factor类的实现。Reader类负责创建读取并创建各个因子,通过他们构成Term和Expression,这是一个比较符合递归下降分析的类的结构。
复杂度分析

本次作业中我最复杂的是Reader中的readFactor和readTerm函数,其次是Term中的toString和derive函数。在后面的不足之处会仔细分析问题所在。

类的复杂度方面比较明显的是Term和Reader类复杂度最大,其他的类在我的设计中只能算个”花瓶“,几乎不起作用。
优缺点
本次代码唯一的”优点“是使用了面向对象的继承机制,构造了多个Factor类完成对term的建模。
本次代码有两个很大的问题:
- 没有真正的应用继承的方式,优点中构造的Factor类大部分都是象征性的,只有20多行代码,完全没有起到应有的功能,ReadFacotr方法中直接硬编码了多种因子的读取过程,造成复杂度过高,ReadTerm中进行了多层的条件判断。而在Term的构建中基本抛弃了各种因子类型,使用x指数 ,cos指数,sin指数作为替代,在第二次中也许有效,但是在第三次中面对三角函数括号内嵌套因子就会无能为力。同时由于我的Term绕过了各个因子的抽象,在求导和输出时只能包办它们的逻辑,导致我的Term方法长达180行,占整个项目的40%
- 使用了错误的数据结构,本次作业我使用了hashmap来存储项。但由于没有将项中系数和因子部分分离,造成项的合并非常困难,同样的问题还存在在项中表达式因子的合并上。因此造成本次作业几乎没有做什么优化。
Bug分析
本次作业没有做什么优化,在强测中没有产生bug。
在互测中我发现了同屋同学的几个bug。有两个同学是输出格式上的小错,和项中的负号有关。另外一个同学在同一个因子多次乘积处理时产生了bug,另外一个在做\(sin^2(x)+cos^2(x)\)合并优化的时候出了较大偏差。所有的bug均是通过随机生成评测发现的。
第三次作业
思路
第三次作业继续采用第二次作业的递归下降方式完成表达式解析。除此之外为了支持wrong format识别,我还添加了一个FormatExcption类,每当无法读取到期望部分时就抛出该异常供外层函数处理。在数据存储部分,我的每一个Term由<Factor,指数>的hashmap构成,每一个表达式由<term因子列表,系数>的hashmap构成,自然完成了合并同类项。在优化部分,我完成了表达式因子升级,由于不能很好的用高级算法进行智能的拆括号,我选择将全部拆括号结果和未拆括号结果进行对比,选取较短的一个。在输出中进行了自然的优化。
UML类图

本次代码的类图和上次比较类似,同样为五种子Factor类继承Factor类,Main函数调用Reader生成Term和Expression,随后调用表达式的求导和优化函数,一层一层的递归到表达式底部执行,本次作业在整体架构上只是比上次添加了ForamtException类。
复杂度分析

本次代码中有几个方法的复杂度过大,其他的方法控制的还可以。最复杂的方法是Term项的toString方法,在该方法中我进行了一些输出优化,如\(cos(0)\)跳过(变为乘1),\(sin(0)\)直接不输出这一项等。其中进行了过多情况的特判,可以考虑将这一部分载荷下放到各因子部分,让在Term中直接完成字符串拼接工作。剩下来的两个方法是进行因子升级和表达式展开的两个方法,这两个方法由于采用了多层嵌套循环,控制流比较复杂。由于优化牺牲了一定的架构,我认为未必是可取的。

我的Term类还是承担了过多的载荷,其中toString,expand,upgrade贡献了较高的复杂度和行数,可以考虑进一步向底层转移这些方法。或者更改底层数据结构支持更便捷的优化操作。
优缺点
本次代码在重构后比较好的利用了继承的机制,将第二次作业中很多Term完成的工作迁移到了各个因子中,降低了平均复杂度,除此之外还进行了力所能及的优化,性能分相比第二次有所提升。
但我的代码可以说成也优化败也优化,其中优化部分代码量并不大,但在复杂代码中的测试调试消耗了我较多时间。同时为了优化写了一些很丑的方法,破坏了代码的整体美观架构,也在优化中埋下了一个小bug。同时不同的类由于相互依赖,耦合度很高,进一步拓展时会有阻力。
bug分析
本次在强测和互测中都各出现了1个bug,简直心在滴血。在强测中,由于第三次作业的指数限制,我直接使Integer类型读取并存储指数,但却忽略了wrong format中的指数可以是任意数的限制。同时在互测中,我的upgrade方法也有了小问题,我的判断条件是项中只有一个因子且该因子是表达式因子时,就将这个因子提取出来。但由于有指数,故在处理\((x+1)^5\)这种类型的数据时会发生bug。
本次互测中我共发现了4个bug,其中两个都是由于同学没有注意format,将正确的格式输出错误,这警示我们要认真读题。另外还有两个同学在求导的时候犯了小错误,分别别是同一项多次幂输出有误(和我被一个数据点hack)以及三角优化时出了锅。本次测试我同样采用的是随机生成+反复测试的策略。
重构
在第一到第二次作业之间我并没有进行重构,直接进行了重写。而在第二次作业和第三次作业间,我进行了一次重构。重构的最直接动力来源就是进行优化时非常吃力,我在周日下午开始进行表达式因子升级优化,在coding2h和debug3h后最后被迫放弃,罪恶的根源是上文所述的数据结构问题。优化前后的UML类图如作业2和作业3中所示,在作业3中去掉两个优化方法后,除了toString和Reader复杂度较大,其他方法的复杂度得到了降低。比较有代表的是其中的derive类,该类在重构前实现具体因子如三角因子,幂因子的求导方法。在重构后简单调用了各个因子的求导方法,降低复杂度又提高了可拓展性。
这次重构经验又让我想起了第一次研讨课时一位学长分享的重构时机之一——当添加新功能时觉得困难时。一旦在开发过程中遇到了困难,不应该考虑如何在自己的屎山代码上一错再错,而应该及时纠正底层架构,为后续拓展铺平道路。
感受
- 好的架构比coding更重要,第二次作业的两大错误让我写代码时无比痛苦面具,最后写了半天的优化也无法解决所有的bug,第三次作业周二晚上用了2.5h就完成了一次重构,在重构后处理各种项和优化都得心应手。
- 缺乏设计模式的概念,如不同因子的构建适用工厂方法,但我却在readFactor部分使用了过多的if else语句。
- 需要提高抽象能力,像在第二次作业中虽然有因子类的抽象,但其子类根本没排上用途。第三次作业中,这些子类有了更多的负荷,但是抽象类Factor的方法还不够具有代表性,同样的,SinFactor和CosFactor两个类中有80%的代码都是相似的,可以考虑设计一层抽象
- 需要提高code review能力,本单元的互测部分我均依赖于评测机进行,没有好好阅读他人代码。他山之石,可以攻玉,学习他人优秀代码也是一种提高。

浙公网安备 33010602011771号