301
社区成员
发帖
与我相关
我的任务
分享在整体的作业中,我的代码共进行过两次重构,第一次重构(HW1)从之前的面向过程的处理方法,重构为了面向对象的处理方法。第二次重构(HW2)新增了poly(多项式)和elem(单项式)类,这两次重构对于当此和后续任务而言,受益无穷。
在第一次的作业中,我最初由于之前写代码的惯性,并未完全理解面向对象与面向过程编程的区别,以为只要我创建了几个类,分别实现了一些方法,便相当于是面向对象编程了。但从现在来看,我最初的方法是完完全全的面向过程编程,方法如下:
整体分为三步:预处理,去括号,后处理。
预处理:
String input = scanner.nextLine().replaceAll("\\s+", "");
这一步的难点在于,由于表达式是递归定义的,所以在进行幂次的展开的时候必须考虑到每个待展开的括号里面也可能存在带展开的带幂次的括号,所以无法使用(或是很难使用)正则表达时匹配的方式来实现,经过了长久的思考,我只想出了一种很原始的方法,即用堆栈来存储括号,每当读到右括号的时候,判断是否含有幂次,若有则进行展开,否则直接pop出和他对应的左括号。最终导致这一个方法就占用了50多行,几乎快达到了checkstyle规定的最大方法行数的上限,可见此种方法有多么的丑陋,而且是一种完全的面向过程的方法。(由于代码太过冗长此处就不贴了)
去括号:
这一步是我第一次构建代码过程中唯一使用面向对象方法的一步。结构与第一次实验相似,定义的类如下

这里新加了一个计算类,用于拆括号时计算表达式的相加和相乘。其中去括号的过程整体沿用的还是递归下降的算法。分别实现了parseExpr,parseTerm,parseFactor。
后处理:
这一步的实现和预处理相同,一样是又臭又长,我讲数字/变量和符号分别用arraylist容器顺序存储,对符号列表进行分析,将每个连续称号后面的正负号都提到第一个乘号前面,最后再将两个列表重新合并,则达到了我想要的效果,这个方法不出所料,又达到了几乎50行
至此,我勉强完成了第一次作业的初始架构。但写这里我发现,我还差一个关键的步骤无法完成——合并同类项,如果我同样再使用后处理的方式,我不敢相信我的代码将会多么的复杂,也将极大的增加我的调试难度,于是我决定重构。
这一次重构前,我构思了许久,通过分析发现,我完全可以将去幂,去括号,合并符号这些事情统统放到第一次架构中的递归下降阶段统一处理。而对于合并同类项的处理,我打算新增一个poly(多项式类),和elem(单项式类),先将原表达式用表达式来存储,最后在重写poly类的toString函数,实现输出,这样在用poly存储的过程中,很自然的就合并了同类项。最后的架构分为三步:建立表达式树(parse过程),解析表达式树为poly多项式,将多项式输出。值得注意的是,此时的单项式其实只是x的n次方组成,按理说完全可以用Integer来代替,但是考虑到后续迭代中单项式肯定不会这么简单,我还是建立了一个elem的单项式类(尽管这个类的属性只有系数和x的power两个),实践发现这个做法是正确的,如下

经历了这一次重构,代码的逻辑变得非常清晰,并且每个方法基本都在20行以内,非常便于我debug。值得一提的是,我单独实现了单项式和多项式的乘法和加法,而不是全部写在一个方法里,这种做法不但能缩短每个方法的行数,也非常利于以后写其他方法时对其进行复用,所以推荐之后的同学们可以参考一下这个方法,具体如下:
poly的pow -> polypoly -> polyelem -> elem*elem
poly+poly -> poly+elem -> elem+elem
总结而言,第一次作业我耗费了大量的时间,从第一次架构完毕,到第二次架构的重构,但是是值得的,这一点从hw2开始就能看出来。
第二次的作业新增了自定义函数和指数函数exp,首先是exp的处理,这次的单项式可以表示为
$ax^bexp(poly)^c$
由于我第一次作业中已经将单项式单独用elem类来处理,所以这次只需要在elem类中新增两个属性:即exp的指数c和exp内部的poly。
第一个难点,现在的elem和poly是递归定义的,即poly类中有一个elem列表,而每个elem中由于有exp的存在,也有一个poly属性,我开始不确定是否可以这样定义,经过查询后得知是可行的,但必须将递归的那个属性定义在最后,防止无限循环。
第二个难点,是对于两个单项式是否可以合并的判定,这个方法是递归的,即先判断b是否相同,在判断exp的c和poly组合起来是否相同,这里为了简化合并的方法,我在从表达式树向poly转换过程中将系数c都乘进了poly中,这样只需要判断两个poly是否相同即可,而poly的判等方法也很好理解,具体实现过程如下:
public static boolean plusible(Elem a, Elem b) {
a.simplify();
b.simplify();
if (a.coefficient.equals(BigInteger.ZERO)) {
return true;
} else if (b.coefficient.equals(BigInteger.ZERO)) {
return true;
} else if (a.xpower != b.xpower) {
return false;
} else if (!a.exppower.equals(b.exppower)) {
return false;
} else {
return Poly.equals(a.expPoly, b.expPoly);
}
}
public static boolean equals(Poly a, Poly b) {
a.simplify();
b.simplify();
if (a.elems.size() != b.elems.size()) {
return false;
}
for (Elem e1 : a.elems) {
boolean flag = false;
for (Elem e2 : b.elems) {
if (Elem.plusible(e1, e2)) {
if (!e1.getCoefficient().equals(e2.getCoefficient())) {
return false;
}
flag = true;
break;
}
}
if (!flag) {
return false;
}
}
return true;
}
到此,就完成了对exp的处理。下面再说对于自定义函数的处理。
在写这一部分之前,我构思了两个方向:字符串替换,直接解析表达式。
前者很好理解,每当读到一个函数,先读取他的参数因子并记录,将函数表达式中的变量因子以字符串替换的方式代替为参数因子,并将替换后的表达式进行解析。
后者具体有两种实现方法
下面我将分析这两个方向,三种方法的优劣:
对于字符串替换的方向,其实现并不复杂,其实是一个不错的方法,但是我若使用这个方法,我将会在topoly这一步中就使用了tostring函数,而topoly和tostring分别我都整体架构中的第二步和第三步,这样会导致这两个步骤的耦合性非常的高,会严重增加后续debug和增量开发的复杂度和思考量。所以我选择了第二个方向。
第二个方向的第一种做法不论是在性能上还是架构上都非常的优雅,因为只解析了一次表达式,后续只是一个copy的操作,大大的加强了性能。但是我能力有限,在纠结了一段时间后还是写不出来,同时考虑到测试点对于时间的要求并不高,我不得不放弃了这个方法。
最后我选中的第二种方法,这种方法的好处就是我的整体架构的三个步骤之间没有任何的耦合,举个例子,假如我某一个数据点的输出错误,我可以瞬间定位到究竟是parse,topoly还是tostring过程出了问题,大大加快了我debug的速度。
(这里有个小tips,除了poly和elem中,也可以将其他的类也实现tostring方法,这样在debug时可以清晰地看到目前解析到了哪一步,但需注意tostring函数必须没有side effect)
至此便完成了hw2,最终架构如下:

第三次作业我在周二发布的当晚仅用了两个小时左右就完成了搭建,得益于此次增量开发只新增了dx,并且也得益于我前两次作业的良好的构建。
对于”自定义函数可以调用其他自定义函数“这个新增要求,我之前的代码就可以实现,所以基本没有做修改。dx方面,我只需实现对elem单项式的求导即可,多项式的求导无非就是单项式的求导之后再相加,最后架构如下


可以发现我的poly和elem类的圈复杂度较高,这也很正常,因为这两个类中的计算方法和tostring方法是这次代码的重头,复杂度高也无法避免。整体而言其他的类的圈复杂大还是很低的,主要得益于第一次作业中的重构
第一次的bug出现在hw2的强测和互测,这次的bug可以说是非常非常非常的可惜,错误出现在了在优化性能时,需要提取一个poly的每个elem的系数,并求他的gcd,由于题目 的正确性要求,gcd不能是负数,也就是不能将负数提到exp的外面,但我的程序当exp中只有一个单项式并且其系数为负时,求出的gcd是负数,导致挂了两个强测和无数个互测。第二个bug,由于题目规定指数不会大于8,本着不浪费性能的考量,我的power都使用了int存储,但其中一个强测点是
0
(((((((((((x^8)^8)^8)^8)^8)^8)^8)^8)^8)^8)^8)^8
导致超出了int范围而报错,只能说以后一定要看清楚题目,严谨的考虑数据类型
第二次的bug出现在hw3的互测中,我在求gcd时出现了除0错误(怎么又是gcd!!),是一个连强测都没测出来的bug。以后需要更加细心。
优化方面,无非就是是否要将exp的poly中的常数系数提出?提的话提多少?通过分析,我发现,假如答案是要提,那一定是提gcd才是最优解,大大简化了优化的时间复杂度。所以我只需要对于每个exp,先去寻找他的poly的gcd,然后判断此次提取是否会减少字符串长度,若会,我才会提出,否则不作处理。
第一单元作业,尤其是前两次的作业,非常的耗时,耗费了很大的经历去思考我的架构,重构,debug。
还有最后互测阶段,我实在无法认可这个设定,互测安排在了周日,在完成了繁重的一周的学业之后,却还不得不周日里花时间去互相测来测去,去构造一些甚至强测都测不出来的数据,意义不大。
第一单元的实验对我的架构思路帮助比较大,希望以后的第一单元实验可以更贴近第一次作业一些,毕竟第一次的架构很重要,事关之后的两次作业。
最后感谢老师,所有辛勤付出的oo助教们,以及为我提供思路的舍友和朋友。