一、架构分析
1.1 第一次作业
类图及分析
Parser:用于解析表达式,并且利用递归处理不同的运算。
MatchRet:保存结点,包括表达式和下一个token。
TokenType:定义token的种类。
Token:定义token,包括种类、值和指向下一个token的指针。
Expression:进行加减乘乘方运算。
思路
我的想法和大部分同学都不一样。我并不是用的递归下降法,整体思路可以分成如下几个步骤:
1、在Parser类里将输入的表达式解析成链表。链表包含了一连串的Token,Token的种类有空、数字、运算符、变量、左括号、右括号。这些种类均在TokenType类中定义。在解析链表的过程中去除空格和制表符,多余的运算符。
2、在Parser类里按四种优先级处理不同优先级的运算。其中,第四优先级用于处理加减法,第三优先级处理乘法,第二优先级处理乘方,第一优先级处理括号。每一个优先级调用下一优先级,第一优先级再调用第四优先级,层层嵌套,递归调用。在处理过程中,调用Expression类里的加减乘乘方方法进行计算,更新链表。
3、将表达式调整后输出。这是因为,如果第一项的系数是负数,而之后存在某一项系数是正数,可以调整一下输出顺序使得输出长度更短。例如,可以将-1+3*x调整为3*x-1,优化性能。
优点:可以直接把嵌套括号的部分解决。
缺点:写起来难度较大,容易出错。我觉得有一个可以稍微降低难度的改进方法,就是像其他同学那样先对表达式进行预处理,去除空格和制表符,以及多余的运算符,而不是在解析链表的时候再去除。
类复杂度分析
方法复杂度分析
1.2 第二次作业
类图及分析
Expression类进行了一些修改,之前由于系数都是大整数,我直接用一个大整数数组来储存系数,数组的不同位置代表x的不同指数。现在我进行了一些修改,用了HashMap,键是变量,值是系数。同时,原来的加减乘乘方方法也都进行了相应的改动。
除了第一次作业定义的类以外,我又添加了几个新的类。
Function:处理自定义函数。
GroupedVariable:保存变量。HashMap的键是变量名(我担心将来迭代时会出现其他变量),值是指数。
Coefficient:保存系数。HashMap的键是表达式(e的指数),值是系数。
思路
相比于第一次作业,这次作业添加了自定义函数和指数函数。关于自定义函数,我先把将其解析成名字,形参和内容三个部分,利用字符串替换将函数的形参替换成实参,再将表达式中出现自定义函数的地方替换掉。这里虽然难度不大,但是要注意的细节很多。例如,要对自定义函数进行预处理,将exp替换掉,要将形参x,y,z预先替换成其他字符,把形参替换成实参时不能直接按逗号分割,返回替换好的表达式要加上括号等。
关于指数函数,解析成链表时要加入相关判断,运算时其优先级应为第一优先级,和括号同级。具体进行有关指数函数的计算时,我是在Coefficient类里写了系数的加减乘方法,GroupedVariable类里写了变量的乘法,然后在Expression类里调用这些方法进行运算。同时,为了合并同类项,提升性能,我在GroupedVariable,Coefficient和Experiment类里重写了equal,hashcode和toString方法。
类复杂度分析
方法复杂度分析
1.3 第三次作业
类图及分析
这次作业用到的类和上一次无变化。
思路
这次作业在定义函数时,可以调用已经定义的函数。对此,我将上一次作业的Function类进行了细微的改动就可以实现这一功能。
此外,这次作业还添加了求导运算。在解析时,求导应为第一优先级,和括号,指数函数相同。在进行求导运算时,我在GroupVariable类,Coefficient类和Expression类里各添加了求导方法,然后按需要调用这些方法。
类复杂度分析
方法复杂度分析
二、bug分析
2.1 第一次作业
在计算--(x-1)^2时,我得到的结果是-1-2*x-x^2,经检查,发现是将-(x-1)看做整体平方了。我意识到我不该把符号看做表达式本身的性质,而是要将这二者分开处理。不过因为这件事,我强测错了两个点,互测被hack了17次。。。
2.2 第二次作业
我认为这次的bug出得很不应该。我的问题出在没有将形参x,y,z用其他字符替代,导致在替换时出现了问题。然而,研讨课上就有同学提到了这一点,只是我由于当时还没写到这一步,并没有很理解他们在说什么,也没有非常重视。之后在自己测试程序时,也并没有测到这个问题。这说明,要重视别人遇到过的错误。
2.3 第三次作业
三、测试他人程序策略
最初,我尝试通过阅读同学的代码,发现其中的逻辑漏洞,但事实证明我没有这个能力。因此在绝大部分时候我都在进行黑盒测试。我构造测试用例的策略在于测试各种特殊情况,比如结果为1,-1,0的情况。以及大数测试,主要用于第一次作业。此外,我还会编一些尽可能复杂的用例,包含各种类型的计算,有时也会有收获(当然,更多的时候根本交不上去)。
四、心得体会
我上学期上过先导课,并且成绩还不错,本以为完成这一单元的作业不会太困难,但没想到OO正课作业的难度还是超出了我的想象。我甚至一度想要放弃,好在目前还是坚持下来了。
关于架构,很多学长都在他们的博客中写道,架构很重要,他们因为最初的架构设计得有缺陷,导致后面的工作量巨大。我并不否认架构的重要性,但我认为,不应该在写作业之前花费太多时间在设计架构上。因为在真正开始之前,有很多问题都是难以预先考虑的,想太多也不会有什么收获。况且,我们本来就不可能一开始就设计出一个完美的架构。我认为,想个大概就可以开始写了,发现有不合适的地方也可以再调整。
关于测试,我最大的体会是应当写完一个方法就测试它,确保正确后再写下一个方法,而不是等到全部写完后统一测试,那样根本不知道问题出在哪。以前,我以为要测试一个方法只能通过Junit,但事实上也可以在主函数声明变量,然后调用想要测试的方法,观察输出结果是否正确。
还有一点,我以前习惯在修改完一个方法的bug后把之前写过的所有用来调试的print语句全部删除,但现在,我意识到我不能保证这个方法之后就不需要改动,而重新写这些print语句很麻烦。因此,现在我会把这些语句注释掉。
五、未来方向
我希望老师可以在第一次作业前稍微讲一下大概要写些什么,如何去写,不然感觉完全没思路,无从下手。此外,我希望以后互测时,平台在提示互测数据不合法时也可以告诉我们为什么不合法,这样我们知道该如何调整,而不是各种问同学,得到一些也不知道是否正确的答案。