BUAA_OO_Unit1阶段总结

本文详细概述了作者在OOUnit1项目中关于表达式计算的实现、遇到的bug(如深浅拷贝、括号层数和性能问题)、架构设计优化,以及如何通过hack策略和性能优化提升项目。作者反思了设计决策并提出了未来改进方向。
摘要由CSDN通过智能技术生成

OO Unit1阶段总结

前言

在OO课程第一个大项目中,我们聚焦于“表达式计算”这一专题,并在三次迭代中逐步完善我们的项目,添加新的功能。在这个过程中体会“面向对象”的设计思想,进一步提高自己的代码质量和水平。

程序概况

先上本人项目中各个类的数据总体的类图以及各个方法的复杂度
各个类的数据量概况
简略的类图
由于我在动手开始写代码时并没有参考课程组的代码,因此我的项目架构和其他同学的架构也许区别较大。
方法复杂度
由于方法数目过多,这里我只给出了数据表现较差的几个方法的数据。其中前三个方法分别为标准化输出,解析表达式,简化指数函数。这三个函数在实现上只能通过大量的if语句实现分支控制,因此数据表现较差。

表达式解析方面

表达式解析由三个类负责。它们分别是Token类,Lexer类以及Parser类。
其中,Token类表示输入字符串中进行解析的最小单元,例如加号"+“,变量"x”,常数"123",自定义函数标志"f",左括号“(”,求导因子"dx"等。
Lexer类则负责具体的解析过程,它接受一个字符串类型的输入,并以此形成一个Token的列表。
Parser类负责进一步的对表达式,项,因子的识别。它接受一个Lexer类型的对象作为输入。调用者可以通过"getExpr"方法获取解析后的表达式。

表达式计算方面

如类图所示,Expr类中具有ArrayList<Term>类型的属性terms,表示表达式中的各个项。
Term类中具有属性ArrayList<Factor>类型的属性Factors,表示项中的各个因子。
Factor为一个接口,其中实现的方法如下
Factor类概况
共有Num,Var,Expr,Exp,Func五个类实现了这个接口,分别表示常数,变量,表达式,指数函数,自定义函数

个人分析

由于我个人认为计算这一任务应当是由客观主体完成的,而不是某个Factor的固有属性,因此我将创建了类Cal,并将所有的计算方法计算的流程控制都放在了这个类中。这是我Cal类的代码量独占鳌头的原因,也是我的架构与其他许多同学不同的根本所在。
这样的设计导致我的项目内聚和耦合程度较低,每当加入了新的计算需求时我都需要在Cal类中新建一个计算方法,并对各个类型不同的Factor设计出一个独自的计算方法。如下图所示
计算方法示例
现在重新回顾我的代码,我认为其他许多同学的“将计算抽象为Factor对象的方法”的设计比较好,可读性与可拓展性都比较高。主要是hw1完成后,想着本来也就三次迭代,就懒地改架构了
但我认为这样的设计也有一定的优点,那就是主要都在Cal.java文件中进行项目迭代,定位Bug和新增需求比较方便。

程序bug分析

由于本人在每次作业完成后都会与他人合作开发评测机,并使用该评测机对项目进行大量测试,因此实际上在强测和互测阶段出现的Bug并不多,这里我将我在评测过程中遇见的Bug和在强测中遭遇的Bug进行统一分析。

深浅拷贝产生的Bug

在本次项目的三次迭代中,因深浅拷贝产生的Bug应该占据了所有Bug的百分之八十以上,而且此类Bug非常的“玄学”(?)。经常出现的情况就是:我在某个方法中改变了某个对象的属性,但是这一改变却导致某个看似与其八竿子打不着的对象的属性发生了变化,从而导致程序的崩溃或者输出错误。然而我在hw2中通过对部分方法进行重构很大程度上解决了这个问题,参见下文的项目迭代分析。

exp括号层数产生的Bug

在第二次迭代中,我需要实现指数函数,其格式为exp((Expr))或者exp(Factor),当exp内部为表达式时需要两层括号,而为因子时可以只用一层括号。出于对性能的追求,我在Exp类中精心设计了termsIsExpr()方法,以此判断某个指数函数的terms属性中是否是单个的因子。然而,由于本人对指导书的阅读不够仔细(加之对自己的数学直觉过于自信),我认为某个因子之前加一个负号“-”并不影响其作为一个因子。然而事实并非如此:除了常数外,其他的因子前为负号时均应当视作一个表达式。这导致我在hw2的强测中一个点出错,并在互测中取得了被hack14次的光辉战绩。

性能低下产生的Bug

性能Bug主要出现在对表达式次幂的计算上。在hw1与hw2中,我对于表达式次幂的计算方法为“重复计算表达式八遍,并将其相乘”,这样会导致在表达式次幂的嵌套计算中时间复杂度指数级增长,例如对于数据((((x)^8)^8)^8)^8,我的程序的时间复杂度为8^4=4096。如此高昂的时间复杂度使我无法通过hw2强测中的第二个点。
但是事实上,我并不需要计算如此多次。我可以将表达式的内容先算出来,在将其重复相乘其次幂的次数。例如((x+1)^2)^2,之前我的算法我会重复计算两边(x+1)^2,但改进算法之后,我只需要计算一遍(x+1)^2,再将这个结果相承。对于数据((((x)^8)^8)^8)^8,时间复杂度为4*8=32,时间复杂度大大降低。

架构设计

hw1

在hw1中我已经为项目打好了大致框架,完成了表达式解析的全部内容,并实现了Num类,Var类,Expr类以及对应的计算方法,并在事实上支持表达式嵌套计算。但此时各个计算方法例如addTerms(), mergeTerms(), FactorMul()方法是直接对传入的Term对象的属性进行修改,而非返回一个新的Term对象,这就是我上文提到的我程序出现深浅拷贝Bug如此频繁的原因所在。

hw2

在hw2中要求支持指数函数的计算和自定义函数的计算。在此次迭代中我深受“深浅拷贝”Bug折磨,于是对我的代码进行了大刀阔斧的重构,将之前所有直接对对象属性进行修改的方法统一改为返回一个新的对象。这样的重构使我基本上杜绝了之后再次产生“深浅拷贝”的可能性。
为了支持指数函数的计算,我面临的一个很大的问题是如何比较两个指数函数的指数是否相等。不同于其它大部分同学的“重写equals方法,然后进行递归比较“方法,我选择利用我hw1现成的getOutputString()方法,将指数函数的指数转化成符合输出格式要求的字符串,通过比较两个字符串来判断两个指数函数的指数是否相等。我原本认为这样写会导致我程序的性能降低,但在完成作业后与同学的交流过程中发现这样写不仅没有导致性能上的降低,甚至还领先于很多使用“重写equals”方法的同学,并且使我得以节省大量的工作量。
对于自定义函数的处理问题,不同于大部分同学的“进行预处理,在字符串层面将函数替换为表达式”的方法,出于将自定义函数因子的计算与其它因子的计算统一起来的考虑,我选择将表达式整体作为一个因子,在解析过程中通过Func类中的静态方法mark2IndependentVars()以及mark2Exp()建立函数名对形参,函数名对表达式的映射关系。并在计算过程中使用Func类中的getExpr()方法将自定义函数转化为表达式。
对于输出结果的化简问题,我出于对程序性能以及复杂度方面的考虑(),选择“无脑”对指数函数的指数部分提取最大公因子,这样可以在性能与复杂度方面取得一个较好的平衡。

hw3

总体而言,hw3的难度比hw1与hw2难度低了许多。我只需要支持自定义函数的嵌套以及求导因子。
对于自定义函数的嵌套,我在hw2中的架构已经实现了此功能,因此不多加赘述。
对于求导因子,我在Cal类中新建了方法takeDerivativeOfExpr(),以实现对表达式的求导。不同于上周三上机实验中课程组所给的“严格按照求导法则一步一步求导”的方法,我选择先将表达式的结果计算出来,并将其转化为a*x*exp(y)的基本项的形式,再对这些基本项进行求导。这样可以大大缩减求导所需的时间。(事实上,经本人实测,“无脑”使用课程组提供代码的人基本上都在性能上存在问题,我针对性地攻击性能“一抓一个准”)

可拓展性分析

我自认为程序的可拓展性还算不错。这里以三角函数sin和cos为例分析程序可扩展性。
首先我需要新建类sin与cos,并实现接口Factor。然后在Token类的Type枚举中增加SIN和COS,在Lexer类中增加对sin与cos的解析,在Parser类中进一步将其解析成对应的因子,并完成表达式解析的过程。为了支持表达式计算,我需要在Cal类中修改基本项的内容,并增加相对应的计算方法。并修改getOutputString方法,以支持新的输出需求(说起来轻松,但实际实现起来轻不轻松就不知道了……)。

hack策略

以攻击程序漏洞为目的的hack

说实话,我个人认为课程组设置互测环节的本意可能是想让我们阅读他人代码,在比较与学习中提高面向对象编程的能力。但是经过本人实测,用评测机,生成大量数据进行轰炸才是找出他人Bug的最优策略。从hw1到hw3,我使用评测机轰炸的方法基本上找出了房间内被找出的除了性能问题以外的所有Bug。在使用评测机找出错误数据之后再进行Bug定位,找出Bug产生的原因,并尽量简化能使程序产生Bug的数据,这样既可以提高自身的阅读代码水平,又便于程序出现Bug的同学快速定位Bug产生的原因。

以攻击程序性能为目的的hack

为了攻击程序性能,首先要仔细阅读指导书的输入格式要求和cost要求,然后在不超过cost的前提下捏造一个尽可能复杂的数据。例如在hw2中我捏造了数据((x^8+x^7+x^3+x^2)^2)^3和(((x+1)^2)^2)^3这两个数据,它们的cost都是4096。而在hw3中我捏造了多重exp嵌套,并在最外层进行求导的数据,并达成了三杀的丰功伟绩。

优化分析

秉持着“能懒则懒”的原则,我在项目迭代中只进行了一次优化,那就是在hw2强测中因为性能问题未通过第二个点之后进行了优化算法,具体优化方法见上文Bug分析部分。

心得体会

我本以为上学期的CO已经是地狱,没想到只是地狱开始。我为跨过一条小河而沾沾自喜,但却没想到跨国小河后横杵在我面前的是一望无际的汪洋。一开学就如此高强度,这在我十余年的求学生涯中前所未有。高强度码代码和高强度测试更是让我每周都无比充实。但是平心而论,我还蛮享受这个过程的,让我每天都拥有明确目标,在周一晚上看到自己强测通过所带来的爽快感更是前所未有的。
以及,一定要认真看指导书!!!!

未来方向

这里本人提几个建议:

适当修改各次作业的任务量

说实话,比起前两次作业动辄两三天的任务量,第三次作业我只用了两个小时便完成了,为了平衡任务量,我认为可以将第二次作业的自定义函数部分移到第三次作业进行实现。这样既可以保证作业的连续性,又可以平衡各次作业的时间。

明确强测的数据强度

为什么互测有数据明确的数据强度限制,但是强测的数据强度限制却不公布出来。我一直不能理解这一点。就比如hw2强测的第二个数据点,很明显它远远超出互测的数据强度限制。不明确的数据强度限制会给同学们带来误导,使同学们找不到优化的目标。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值