第一单元作业总结
一、第一次作业
1.架构分析
第一次作业中,我采用了预解析输入模式,这种方法对于架构设计的要求不高,基本上还是沿用了面向过程编程的思路——将解析过的式子存入容器中,再遍历容器中的语句,将式子中的因子连成一个长字符串,最后再进行化简。
为了存储每个阶段的式子,我创建了一个Expression类来存储每个步骤的Id与内容,因此当遇到下一步骤中存在之前步骤的Id时,即可通过遍历的方式替换掉当前步骤中的Id,得到一个不含有Id的式子,然后存储到另一容器中,直到遍历结束。
在将式子连接起来的过程中,会产生一些不符合要求的表达,比如说多余的“+”或“-”,因此我采用了字符串处理的方式进行化简需要进行化简,大致思路为先声明一字符数组来存储化简后的式子,然后开始对得到的式子进行遍历,一旦遇到“+”或“-”则令flag为1,并且另设symbol,遇“+”则不变,遇到“-”则乘以负一,在遇到下一个不是“+”或“-”的字符时根据symbol的值来确定该项的正负号并写入字符数组,接着再把不带“+”或“-”的项写入字符数组中。
2.UML图
优点:思路简单,易于理解,容易实现
缺点:代码过长,进本上完全面向过程编程,难以优化。
3.度量分析
由于本次是在main函数中使用switch-case语句完成的表达式结果的生成,因此复杂度极高,基本上没什么可读性。
method | CogC | ev(G) | iv(G) | v(G) |
---|---|---|---|---|
Main.Expression.Expression(String, String) | 0.0 | 1.0 | 1.0 | 1.0 |
Main.Expression.getContent() | 0.0 | 1.0 | 1.0 | 1.0 |
Main.Expression.getId() | 0.0 | 1.0 | 1.0 | 1.0 |
Main.find(ArrayList, String) | 3.0 | 3.0 | 3.0 | 3.0 |
Main.main(String[]) | 38.0 | 1.0 | 14.0 | 20.0 |
Main.mul(String, String) | 7.0 | 1.0 | 3.0 | 4.0 |
Total | 48.0 | 8.0 | 23.0 | 30.0 |
Average | 8.0 | 1.3333333333333333 | 3.8333333333333335 | 5.0 |
class | OCavg | OCmax | WMC |
---|---|---|---|
Main | 9.333333333333334 | 21.0 | 28.0 |
Main.Expression | 1.0 | 1.0 | 3.0 |
Total | 31.0 | ||
Average | 5.166666666666667 | 11.0 | 15.5 |
二、第二次作业
1.架构分析
在第二次作业中,我参考裴子祎同学在第一次作业讨论区的帖子,进行了重构。主要思路是创建Operator类,并使得每种运算符号单独成类,分别继承Operator类。在解析的过程中,按照运算符优先级,采用递归的方式将原式不断细分为更小的因子,并在此过程中去掉多余的括号,将化简结果依次以字符串的形式返回到上层函数中,然后再根据要求进行简化。
在处理自定义函数与求和函数的过程中,我采用了字符串替换和连接的方式来获取新的表达式。同时我也体会到了正则表达式的局限性——我本来准备采用正则表达式替换的方法来换掉自定义函数,但是最后发现并不可行,因为正则表达式中存在很多转义字符,而我们需要被替换的自定义函数中恰好不乏转义字符的存在,同时自定义函数的形式也比较灵活,因此若采用正则表达式进行替换则该过程会变得异常繁琐,所以最后还是回归到了字符串的拼接和处理上。
在解析过程中,我原本按照从前往后查找的方式发现运算符,再用多层if-else语句进行嵌套,来根据运算符的优先级来先处理掉优先级较低的运算符号。在这个过程中我曾遇到过一个问题,就是当处理到“-”时,一旦“-”之后有多个项相连时,就不能在Sub类中直接将“-”之后的各个项变号,因为变号只能发生在“-”后紧接着括号的情况,但在我的结构中并不能判断“-”后是否为带有括号的项。因此,我又想到了优先级的问题——既然不同的运算符号之间存在优先级,那么如果一个式子中只存在优先级相同的运算符,我们是怎么运算的?答案很明显,我们会从左向右依次运算,那我们也可以近似地理解成一个只存在优先级相同的运算符的式子中,左边运算符的优先级必右边的运算符优先级高。所以在查找优先级最低的“+”与“-”时,不应该从左往右查找,而应该从式子的右端开始查找,这样才能保证之后的处理不会影响之前得到的结果的正确性。
在拆括号的过程中,我将括号当作优先级最高的一种运算符单独成类,然后在遇到括号时,就将括号中的内容当成一个表达式,继续进行解析,返回一个已经脱掉括号的表达式。由于采取了递归地处理方式,因此不用担心括号嵌套的问题。而还有一种特殊情况就是三角函数的括号处理问题,由于我发现这次作业中三角函数中只能存在常数项与幂函数项并且不允许嵌套,所以即使不处理也不会影响正确性,因此我在拆括号的过程中略过了三角函数。
在最后的简化过程中,我写了process和simplify两个函数,其中process函数用于去除项之外多余的正负号,而simplify函数则用于清除“*”右侧可能出现的正负号——因为我的解析过程结构比较简单,因此只能保证去掉多余的括号而不能保证得到的表达式语法正确。其实在写函数的时候就发现了,由于我还不能很好地掌握面向对象的编程方式,因此在处理过程中还是会下意识地使用面向过程的思维方式,在各种方法的实现上缺乏灵活性——process函数和simplify函数更像是为之前步骤的局限打上的补丁。
2.UML图
优点:结构简单,不再完全面向过程编程了
缺点:代码风格较差,复杂度高,难以维护
3.度量分析
虽然进行了重构,但是还是没有解决代码复杂度高的问题,而复杂度较高的地方基本上都进行了或多或少的字符串处理,比如说查找特定运算符的几个方法,复杂度都不低。而parse方法由于需要按照优先级解析表达式,因此存在很多if-else以及循环语句,导致代码风格比较差劲,同时可读性也比较弱。
method | CogC | ev(G) | iv(G) | v(G) |
---|---|---|---|---|
Add.Add(Operator, Operator) | 0.0 | 1.0 | 1.0 | 1.0 |
Add.getResult() | 0.0 | 1.0 | 1.0 | 1.0 |
Brancket.Brancket(Operator, Operator) | 0.0 | 1.0 | 1.0 | 1.0 |
Brancket.getResult() | 0.0 | 1.0 | 1.0 | 1.0 |
MainClass.findBrancket2(String) | 8.0 | 4.0 | 2.0 | 5.0 |
MainClass.findEqual(String) | 3.0 | 3.0 | 2.0 | 3.0 |
MainClass.main(String[]) | 28.0 | 1.0 | 13.0 | 13.0 |
MainClass.process(String) | 13.0 | 1.0 | 5.0 | 6.0 |
MainClass.simplify(String) | 37.0 | 1.0 | 11.0 | 16.0 |
Mul.getResult() | 10.0 | 1.0 | 4.0 | 5.0 |
Mul.monoTopoly(String) | 15.0 | 1.0 | 8.0 | 10.0 |
Mul.Mul(Operator, Operator) | 0.0 | 1.0 | 1.0 | 1.0 |
Mul.process(String) | 13.0 | 1.0 | 5.0 | 6.0 |
Num.getResult() | 0.0 | 1.0 | 1.0 | 1.0 |
Num.Num(String) | 0.0 | 1.0 | 1.0 | 1.0 |
Operator.getLeft() | 0.0 | 1.0 | 1.0 | 1.0 |
Operator.getResult() | 0.0 | 1.0 | 1.0 | 1.0 |
Operator.getRight() | 0.0 | 1.0 | 1.0 | 1.0 |
Operator.Operator(Operator, Operator) | 0.0 | 1.0 | 1.0 | 1.0 |
Parser.findAddOrSub(String) | 20.0 | 8.0 | 7.0 | 9.0 |
Parser.findBrancket1(String) | 32.0 | 9.0 | 9.0 | 10.0 |
Parser.findBrancket2(String) | 7.0 | 5.0 | 3.0 | 5.0 |
Parser.findMul(String) | 11.0 | 7.0 | 7.0 | 9.0 |
Parser.findPow(String) | 9.0 | 6.0 | 5.0 | 7.0 |
Parser.parse(String) | 55.0 | 10.0 | 9.0 | 11.0 |
Sub.getResult() | 9.0 | 1.0 | 6.0 | 6.0 |
Sub.monoTopoly(String) | 15.0 | 1.0 | 8.0 | 10.0 |
Sub.process(String) | 13.0 | 1.0 | 5.0 | 6.0 |
Sub.Sub(Operator, Operator) | 0.0 | 1.0 | 1.0 | 1.0 |
Total | 298.0 | 73.0 | 121.0 | 149.0 |
Average | 10.275862068965518 | 2.5172413793103448 | 4.172413793103448 | 5.137931034482759 |
class | OCavg | OCmax | WMC |
---|---|---|---|
Add | 1.0 | 1.0 | 2.0 |
Brancket | 1.0 | 1.0 | 2.0 |
MainClass | 8.0 | 15.0 | 40.0 |
Mul | 5.0 | 8.0 | 20.0 |
Num | 1.0 | 1.0 | 2.0 |
Operator | 1.0 | 1.0 | 4.0 |
Parser | 7.666666666666667 | 11.0 | 46.0 |
Sub | 5.25 | 8.0 | 21.0 |
Total | 137.0 | ||
Average | 4.724137931034483 | 5.75 | 17.125 |
三、第三次作业
1.架构分析
本次作业相当于是在第二次组作业的基础上,增加了自定义函数的嵌套调用,并且允许三角函数的嵌套。因此我首先在之前作业的基础上增加了Sin类与Cos类,并且将两种三角函数符号的优先级等同于“(”和“)”,是三角函数可以进入递归,进行解析。
之后,我修改了主类中的simplify方法——因为原来的方法只能处理不存在括号嵌套的的表达式,因此我将simplify方法改为了一个可以递归调用的方法,使其能够不受括号的干扰——具体方法是每当遇到括号时,就将括号中的内容传入simplify函数中,来得到一个符合说明文档要求的内层表达式,之后再不断递归回到最开始的表达式上,处理完成。而在提交过程中我才发现我忽略了本次作业中对于表达式因子带括号的要求,因此偷了个懒,新写了一个addBrancket方法,来将答案中的单层括号全变成双层括号。
优点:结构直观、清晰
缺点:没有优化,代码多见多层的嵌套,复杂度高
3.度量分析
总的来说,本次作业基本上还是在打补丁的过程中完成的,因此可以预料到代码依然存在第二次作业的问题,甚至更严重。而其中,main函数中的simplify方法也被改得面目全非——因为之前的处理非常粗糙,因此再在这一步我不得不使用一个相当复杂的递归调用函数来弥补之前的不足。
method | CogC | ev(G) | iv(G) | v(G) |
---|---|---|---|---|
Add.Add(Operator, Operator) | 0.0 | 1.0 | 1.0 | 1.0 |
Add.getResult() | 0.0 | 1.0 | 1.0 | 1.0 |
Brancket.Brancket(Operator, Operator) | 0.0 | 1.0 | 1.0 | 1.0 |
Brancket.getResult() | 0.0 | 1.0 | 1.0 | 1.0 |
Cos.Cos(Operator, Operator) | 0.0 | 1.0 | 1.0 | 1.0 |
Cos.getResult() | 0.0 | 1.0 | 1.0 | 1.0 |
MainClass.addBrancket(String) | 5.0 | 1.0 | 4.0 | 4.0 |
MainClass.findBrancket2(String) | 8.0 | 4.0 | 2.0 | 5.0 |
MainClass.findEqual(String) | 3.0 | 3.0 | 2.0 | 3.0 |
MainClass.main(String[]) | 57.0 | 1.0 | 18.0 | 19.0 |
MainClass.process(String) | 13.0 | 1.0 | 5.0 | 6.0 |
MainClass.simplify(String) | 63.0 | 8.0 | 22.0 | 24.0 |
Mul.getResult() | 10.0 | 1.0 | 4.0 | 5.0 |
Mul.monoTopoly(String) | 15.0 | 1.0 | 8.0 | 10.0 |
Mul.Mul(Operator, Operator) | 0.0 | 1.0 | 1.0 | 1.0 |
Mul.process(String) | 13.0 | 1.0 | 5.0 | 6.0 |
Num.getResult() | 0.0 | 1.0 | 1.0 | 1.0 |
Num.Num(String) | 0.0 | 1.0 | 1.0 | 1.0 |
Operator.getLeft() | 0.0 | 1.0 | 1.0 | 1.0 |
Operator.getResult() | 0.0 | 1.0 | 1.0 | 1.0 |
Operator.getRight() | 0.0 | 1.0 | 1.0 | 1.0 |
Operator.Operator(Operator, Operator) | 0.0 | 1.0 | 1.0 | 1.0 |
Parser.findAddOrSub(String) | 20.0 | 8.0 | 7.0 | 9.0 |
Parser.findBrancket1(String) | 3.0 | 3.0 | 2.0 | 3.0 |
Parser.findBrancket2(String) | 7.0 | 5.0 | 3.0 | 5.0 |
Parser.findMul(String) | 11.0 | 7.0 | 7.0 | 9.0 |
Parser.findPow(String) | 9.0 | 6.0 | 5.0 | 7.0 |
Parser.parse(String) | 77.0 | 13.0 | 12.0 | 14.0 |
Sin.getResult() | 0.0 | 1.0 | 1.0 | 1.0 |
Sin.Sin(Operator, Operator) | 0.0 | 1.0 | 1.0 | 1.0 |
Sub.getResult() | 9.0 | 1.0 | 6.0 | 6.0 |
Sub.monoTopoly(String) | 15.0 | 1.0 | 8.0 | 10.0 |
Sub.process(String) | 13.0 | 1.0 | 5.0 | 6.0 |
Sub.Sub(Operator, Operator) | 0.0 | 1.0 | 1.0 | 1.0 |
Total | 351.0 | 82.0 | 141.0 | 167.0 |
Average | 10.323529411764707 | 2.411764705882353 | 4.147058823529412 | 4.911764705882353 |
class | OCavg | OCmax | WMC |
---|---|---|---|
Add | 1.0 | 1.0 | 2.0 |
Brancket | 1.0 | 1.0 | 2.0 |
Cos | 1.0 | 1.0 | 2.0 |
MainClass | 9.333333333333334 | 21.0 | 56.0 |
Mul | 5.0 | 8.0 | 20.0 |
Num | 1.0 | 1.0 | 2.0 |
Operator | 1.0 | 1.0 | 4.0 |
Parser | 7.166666666666667 | 14.0 | 43.0 |
Sin | 1.0 | 1.0 | 2.0 |
Sub | 5.25 | 8.0 | 21.0 |
Total | 154.0 | ||
Average | 4.529411764705882 | 5.7 | 15.4 |
四、Bug分析
第一次作业bug
由于对说明文档的理解不够细致,因此bug基本上都是由于没有去掉多余的“+”与“-”造成的。
第二次作业bug
虽然代码复杂度高,冗余也较多,但由于代码结构设计较为简单,因此在弱测中比较容易定位bug,也进行了一些修改,在强测和互测中未发现bug。
第三次作业bug
本次作业的bug有两点,首先是求和函数的边界问题,这个bug比较容易改,我通过修改相关变量的变量类型解决了这个问题;其次是自定义函数的嵌套问题,这本来这是这次作业的一项内容,但由于我考虑的疏忽和构造的测试点不够强等原因,导致我在过弱测的过程中并未有发现这一点,最后我通过在相关处理模块外嵌套一层循环解决了我内层自定义函数处理不到的问题。
五、心得体会
1.在码代码之前进行架构设计。因为我的在做第一次作业的过程中,只是粗略地考虑了一下架构就开始动手写,导致在提交截止时间前几个小时依然卡在各种各样的bug中,最后只能换方法写。而在之后的两次作业中,虽然有所好转,但是感觉自己还是会下意识地会选择面向过程的方法写,打上各种各样的补丁。
2.多做测试。不要因为过了弱测就不做测试,因为有的时候弱测可能真的测不到作业中基本要求的点,要学着进行自动化测试。
3.不要跟着思维的惯性走,多尝试不同的方法。如果卡在一处,要分清这个问题到底为什么出现,能否解决,能解决立马解决,不能解决尽早尝试其他方法,不要浪费时间。