三次作业的思路与bug测试
第一次作业
第一次作业内容为简多项式单导函数的求解,项包含常数项(带符号整数)与变量项(系数与幂函数的乘积),表达式由加法和减法运算符连接若干项组成。且输入中确保格式的正确性。
1.1 实现方案
根据需求,我们很容易联想到用 HashMap 表示每一项中幂函数指数与系数的对应关系。而且因为这是第一次作业,我没有考虑可扩展性,直接在多项式类Ploy中使用了类型为HashMap
-
数据层次:根据指导书表述,容易联想到建立指数与系数的
HashMap
来表示多项式,我建立了多项式类Ploy
,项类Term
,主类MainClass
。 -
输入解析:我采用了按项拆分字符串的方法,提取过程中应用了正则表达式。
三次作业包你掌握正则表达式(●'◡'●) -
求导功能:我在Ploy类中实现了
diffPloy()
方法,该方法会返回一个求导后的导函数,即一个新的Ploy对象。 -
输出:我在Ploy类中实现了
printFirst()
方法。
①在 MainClass
里实例化 Ploy
对象 ploy,借用正则表达式将输入的字符串拆为单项的字符串,将拆好的字符串扔进Term
里进行指数和系数的解析得到一个新的Term
对象 term,再调用 ploy 的addTerm()
方法,将 term 对象添加进 ploy 中的 HashMap
,至此完成了输入的解析与存储。
②再在 MainClass
里对 ploy 调用 diffPloy()
方法,进行求导,得到求导后 Ploy
类的实例对象 diff。
③最后对 diff 调用print()
方法完成输出。
1.2 程序分析与总结反思
1.2.1这次作业的不足
-
这次作业的OO味不浓,
Ploy
类,Term
类之间的关系为简单的组合关系,面向过程的影子还是深深存在。其中最严重的一个问题是,本着解决一次作业算一次的思想,这次作业的扩展性贼差,预示着我下一次作业的重构。 -
对于程序的具体实现,现在看来最让我不满意的是
Term
的构造方法,即从项的字符串中提取出指数和系数。由于没有考虑扩展性对Term
项再进行常数项和变量项的拆分,Term
的构造方法逻辑上不够清晰。总的来说,不应让一个方法完成过多的任务。Term 的构造方法完成的任务过多,显得非常臃肿。从判断常数还是变量项、到设置变量项的指数、再到设置变量项的系数,因为指数和系数均有省略的可能,使得判断比较多,导致if-else语句块很大。
1.2.2 程序量化分析
在分析结果中可以看到ev, iv, v这几栏,分别代指基本复杂度
(Essential Complexity (ev(G))
、模块设计复杂度(Module Design Complexity (iv(G)))
、Cyclomatic Complexity (v(G))
圈复杂度。
ev(G)
基本复杂度是用来衡量程序非结构化程度的,非结构成分降低了程序的质量,增加了代码的维护难度,使程序难于理解。因此,基本复杂度高意味着非结构化程度高,难以模块化和维护。
Iv(G)
模块设计复杂度是用来衡量模块判定结构,即模块和其他模块的调用关系。软件模块设计复杂度高意味模块耦合度高,这将导致模块难于隔离、维护和复用。
v(G)
是用来衡量一个模块判定结构的复杂程度,数量上表现为独立路径的条数,即合理的预防错误所需测试的最少路径条数,圈复杂度大说明程序代码可能质量低且难于测试和维护,经验表明,程序的可能错误和高的圈复杂度有着很大关系。
第二次作业
第二次作业需要完成简单幂函数和简单正余弦函数求导,相比第一次作业,数据方面增加了 sin(x)与cos(x)因子,规则方面增加了乘法。依然确保输入空格格式的正确性,但是不确保数据的正确性,并且指数增加了范围限制(小于等于10000),需要进行格式判断。
2.1 实现方案
鉴于第一次作业的 HashMap
,重构是显然的了。
-
格式检查:我把
CheckFormat
单独写成了一个类,在输入时直接进行格式检查,也有同学把格式检查分散到各个类,在每个类中进行检查。 -
数据层次:根据指导书的描述,我设计了表达式类
Expression
,项类Term
,因子接口Factor
,有四个类SinFactor
、CosFactor
、XFactor
、Const
实现了Factor
接口(有点不标准的工厂模式的味儿)。 -
输入解析:我依然采用了按项拆分字符串的方法,再在项中逐个提取因子
Factor
,提取过程中应用了正则表达式。 -
求导:
Expression
类中实现加法规则,调用Term
的求导,Term
调用Factor
的求导。 -
输出:我重写了
Expression
的toString()
方法。 -
优化思路:我只实现了合并同类项和第一项输出正项的优化,并未考虑
sin(x)**2 + cos(x)**2 = 1
的优化。为了方便地合并同类项,我在Expression
类中仍沿用了HashMap
,保存项和系数的对应关系,为此需要重写Term
的equals()
和hashcode()
方法,根据Term
的幂函数指数xindex
、sin函数指数sinindex
,和cos函数指数cosindex
判断两个Term对象是否equal(是否可合并)。
2.2 程序分析与总结反思
2.2.1 这次作业的不足
-
首先,这次作业中的类,存在着一些不必要的成员变量。比如
Term
中的op
和termsign
,这两个属性只在方法parseFactors(String)
中用来确定最终常数项constant
的值,除此之外并没有用处,并不应该设置为Term
的成员变量。 -
其次,程序的扩展性还是比较弱。考虑到
Term
的特殊性,Term
中表示属性的成员变量实际上只有costant、xindex、sinindex、cosindex
,成员变量factors
根据这四个属性构造生成。这样做非常方便,但是程序的扩展性并不好,下一次作业又进行了大改。 -
最后,依然是如何对于一个方法减小其规模的问题,应该使各个类各司其职。比如,
Expression
的toString()
不应完成Term
的toString()
应该完成的内容。在输入方面,我重写了Expression
的toString()
方法,不足的是,由于Term
中并没有重写toString()
方法,使得Expression
没有方法可以调用,导致Expression
类的toString()
方法很臃肿。
2.2.2 程序量化分析
由以上分析,大致可以得出程序中丑丑的代码应该在Term
类与Expression
类中。
Expression
的问题在上文已经提及,由于Term
中并没有重写toString()
方法,使得Expression
没有方法可以调用,导致Expression
类的toString()
方法很臃肿。
2.3 bug分析
在此反思,我没有进行大规模测试,只测了正常的数据,甚至连x**10000*x**10000
这种边界数据都没有测试,要命的是,我还偏偏疏忽了指数为10000时的合法性,检查到指数 >=10000直接WF。
互测时我发现了自己的bug,并由于我自己的bug,发现了组里其他两名同学的bug。我主要测试了边界数据,比如项相加或者求导后为0,比如总指数大于10000,值得一提的是,有的同学在将x**2
替换成x*x
的过程中出错,比如x**20003
会错误输出x*x0003
。
第三次作业
数据方面,第三次作业加入了表达式因子,规则方面,增加了嵌套规则。且输入中不保证不出现非法字符、非法空格,需要进行输入格式的判断。
3.1 实现方案
一开始看到指导书的时候很绝望,因为引入的问题实在比较多,有点不知所措。首先是非法格式的判断,因为之前的两次作业中中进行输入处理时我都默认了其格式的正确性。其次是究竟怎么对嵌套规则进行求导我还比较迷茫。
-
格式判断:在思考之后,我设计了
CheckFormat
类用于判断格式的正确性。这样后续用正则表达式提取的时候能够没有后顾之忧。缺点是判断格式的过程比较复杂。 -
数据层次:根据指导书说明,我设计了表达式类
Expression
,项类Term
,五个因子类SinFactor、CosFactor、XFactor、ConstFactor、ExprFactor
实现了Factor
接口,SinFactor
的成员变量有内层因子Factor factor
和幂指数,ExprFactor
的成员变量是一个表达式。 我在每个因子类中设置了getREGEX()
静态方法,得到每个因子类的正则表达式。此外,我建立了Factory
类,使用工厂模式实例化每个Factor
,把根据输入的因子字符串判断应该实例化哪种Factor
的工作丢给了工厂。 -
求导功能:求导的思路就很明朗了,所有可以求导的对象都有
diff()
方法,我在Expression
层次实现加法法则,在Term
层次实现乘法法则,在可嵌套因子sin/cos
层次实现嵌套法则,而ExprFactor
的diff()
直接递归调用Expression
的diff()
方法即可。 -
输出:对于输出,我重写了各个类的
toString()
方法。 -
优化方面:出于对自己水平的正确认知,以写完作业并成功提交为主要任务,我放弃了优化。
合并同类项、零项不输出、首项输出正项这些通通没有考虑,把主要精力放在了功能的实现上。值得一提的是,如果优化,需要注意sin(x*x)
和sin(x**2)
的区别,前者会直接WF。
CheckFormat
类完成了很多工作。首先,因为非法空白的情况已经列举出来,可先判断非法空白、非法空格、非法字符,如果没有上述WF
,就将空白全部删去。
对于静态方法 CheckExpr()
,它的功能是检查删除空白后表达式的正确性。参考讨论区jyc同学的方法,并利用栈 Stack
的思想,将 sin(Factor)
替换为sin<@>
, 将非sin/cos
内层嵌套的 表达式因子 替换为#,并将@原来的内容(因子)存入ArrayList
、将#原来的内容(表达式)存入ArrayList
。
CheckFormat
中的 checkfactor()
方法用于检查因子格式的正确性,checkploy()
方法用于检查替换后的表达式的正确性。因此只需要对替换后的表达式字符串调用checkploy()
方法;对factorlist
中的因子字符串调用 checkfactor()
方法;对exprlist
中的表达式字符串递归调用CheckExpr()
方法即可。
CheckFormat
类的另一个静态方法是replaceExpr()
,它的功能是把sin()
替换成sin<>
,把表达式外层括号()
替换成[]
。从而解决正则的匹配提取问题。
输入存储的思路和判断格式的思路非常相似。Expression
的成员变量是 ArrayList
,对于构造方法 Expressin(String)
,先进行replaceExpr()
,再对每一项用正则表达式进行提取即可,继而调用构造方法Term(String)
,初始化成员变量ArrayList
,在此过程中利用正则表达式提取出每一个因子的字符串 factorstring
,再直接调用工厂类Factory
中的方法 createFactor(String)
即可创建因子。
在SinFactor
的构造方法中,在初始化其factor
成员前也对提取出的内层字符串调用了CheckFormat.replacedExpr()
方法进行括号的取代。
由于每种因子的REGEX
很容易写出,故项的REGEX
只需对其进行简单组合。需要注意的是sin和cos以及表达式因子的正则中需采取非贪婪匹配。
String sinFactor = "sin<(.*?)>(" + exp + ")?";
String regex = "\\[(?.*?)\\]";
3.2 程序分析
可以看出复杂度比较高的方法主要来自CheckFormat
类。CheckFormat
类完成了很多工作,这是我觉得有点难受的地方,没有将格式判断扔给底下的小类处理,而是写了一个巨大的类,大概有200多行代码。而且类中的方法逻辑不是特别清晰,if-else
块比较大。
3.3 测试与bug分析
我依然没有学会大规模测试,依然人工构造数据和测试边界数据。
首先是中测中我忘记了前导0,找了好久的bug…… ╥﹏╥...中测里也有一些同学对于空格的处理不当,不应该被判WF
的数据点被误判了WF
。
强测没有测出我的bug,但是互测时我的roommate发现了我的一个问题,在输出时,如果一项中含有因子0,我会直接return "0"
,看起来好像没有什么问题,但是事实上,我没有输出项的连接运算符……比如"x+0+0"
,我会错误输出"+100"
。
至于互测时别人的bug,我依然人工构造了边界易错数据。很多同学有sin(x**2)
直接输出为sin(x*x)
的问题。此外,也有一些同学优化导致多层嵌套时会出现TLE。
bug分析与测试
bug分析
这三次作业中,中测、强测、互测一共发现了我的3个bug。这几次作业的bug主要有如下原因:
-
指导书阅读不仔细导致出现错误。比如指数小于等于10000理解为指数小于10000,忘记有符号整数可以包含前导0.
-
手抖的bug。
-
返回值为字符串时,在方法内多处return导致字符串的格式不一致,比如第三次作业中的bug。
我的这几个bug集中于输入模块和输出模块。
往往容易出错的模块是干实事的模块,比如数据输入、复杂的数据操作(优化)、数据输出。这些易出错的模块的共同特点是:比较繁琐,容易出现遗漏;要处理的场景比较多,且不太容易归纳。
测试
我的hack策略主要是考虑细节与边界。比如第一次作业hack空白字符;第二次作业hack指数范围、求导后为0、x**2
直接替换为x*x
导致的错误;第三次作业考虑到性能hack多层嵌套、hack优化导致的错误输出。
第三次作业中,由于我的输出比较长,我在hack时瞄准了屋里一位优化得很短,但还是和我一个屋的roommate,(●'◡'●)和没有优化的我一个屋一定是有理由的。果然这位同学在化简的过程中出现了错误。
应用对象创建模式
第二次作业中依稀有了工厂模式的影子,第三次作业中才真正成功应用了简单工厂模式。由于作业中因子的添加需要在同一个位置判断,并且我们并不关心实现的细节,非常适合应用工厂模式。
通过Factory
类实现Factor createFactor()
方法,对每个传入的因子字符串,进行正则匹配以返回正确的因子类型。
public static Factor create(String request) {
Factor factor;
Pattern constP = Pattern.compile(ConstFactor.getRegex());
Matcher constM = constP.matcher(request);
Pattern powerP = Pattern.compile(PowerFactor.getRegex());
Matcher powerM = powerP.matcher(request);
Pattern sinP = Pattern.compile(SinFactor.getRegex());
Matcher sinM = sinP.matcher(request);
Pattern cosP = Pattern.compile(CosFactor.getRegex());
Matcher cosM = cosP.matcher(request);
Pattern exprP = Pattern.compile(ExprFactor.getRegex());
if (constM.matches()) {
return new ConstFactor(request);
} else if (powerM.matches()) {
return new PowerFactor(request);
} else if (sinM.matches()) {
return new SinFactor(request);
} else if (cosM.matches()) {
return new CosFactor(request);
} else {
return new ExprFactor(request);
}
}
心得体会
虽然没有对象还是要面向对象,OO的工作量名不虚传,我觉得它的困难主要在于,面对一个没有经验、不知道从哪儿下手的作业,我们需要一层层分析需求,设计数据层次、实现具体流程与细节。每一步都需要花费大量的精力,但写完程序的一瞬间真的成就感满满,虽然自己的程序跟别人比还有着很大的差距,但也能看到自己从pre提交都不会到现在的小进步。
对比一些优秀代码,还是有像星星一样多的不足:
-
可读性方面,优秀代码逻辑清晰,变量命名容易理解,该加注释的地方就加注释。而我的代码注释不多,也有很多逻辑不够清晰的地方。
-
没有使用异常。
-
一些类中有着多余的成员变量。
-
一些类中对其他类的依赖关系比较强。
-
设计时很少考虑代码的扩展性,导致代码的扩展性和重用率低。
-
一些方法或者类过于长。
-
类的功能不够明确。存在这个类完成了那个类的任务的问题。
-
不够OO,主要依赖类之间的组合关系,还是有些像"写成类的面向过程程序"。
另外:总是当豆豆龙(ddl)战士,第三次差点儿交不上作业,只完成了功能的正确性,没有追求性能,要对自己要求高一些。
加油加油,多多学习~希望码量up up ~ 拯救一下我这不太OK的编程饭碗。