OO第一单元作业总结
目录
一、整体分析
1.大致介绍
在我的设计中,设计主体是 “项Term”和“解析器Parser” ,其余类主要起辅助作用
不同于教程推荐的“Factor接口+Num/Var/Expr/Sin/Cos”的设计,我将Factor下属的可能的类直接存储在了Term中,这样一来也就自然不需要Factor这个接口了。
具体地讲,类图中Term的“系数Cofficient”即为一个项中的数字部分,“指数IndexOfX/Y/Z”分别表示x、y、z的指数,“表达式Exprs”为存储Expr的数组,“三角函数Coss/Sins”为存储三角函数的两个数组。
同时,我没有使用“Lexer”,这是由于早期设计中,对于递归下降理解有限,为了避免Lexer于Parser之间的交互问题,我将Lexer集成在了Parser之中,两种方式我认为差别不大。
2.为什么采用这种Term设计?
我的Term类的设计的好处在于,直接将Factor接口集成在了Term类中, 避免了在进行运算时的类的上下转换。
例如, 在运用Factor接口的设计中,如果要将Term与x相乘,我们需要首先遍历Term的Factors,寻找Term中的变量x,对其进行深拷贝,然后在乘法后返回一个新的变量,加入到Factors中,并删去原有的变量。(当然,也可以直接更改原变量x,但是这样做在之后的扩展中未必安全)
对此,我的评价是:系统化、规范化但实现复杂
而 在我的这种设计中,将Term与变量x相乘只需要直接将“IndexOfX”加一。
之后在第二次的作业增量开发中,我为Term新增了两个Expr数组,分别存放sin、cos括号中的因子,直接避免了创建新的三角函数类。而对三角函数的具体处理,也只是表达式的化简和比较,都是第一次作业已经实现的功能,大幅简化了工作。
3.其余几个类的设计考虑
首先说Parser,
我的Parser实际上是“Lexer+Parser”,一人干两份工,这或许是一个设计上的失误,因为我设计开始时还没有系统地了解过递归下降。不过这样也省去了Parser和Lexer的交互,而且也没有出太大的问题,所以之后也没有改动。
在具体实现方面,Parser就是负责解析输入数据,并以Term为单位进行存储。在每个Term的新建过程中,首先初始化一个基础的Term,然后依次去乘每一个用乘号分割的部分。
之后讲一讲“Process类”,
这个类算是比较取巧的一个类,主要作用是把输入的表达式预化简到一个“符合日常使用规范”的样子。
在本单元的作业中,输入的数据很多并不符合我们的日常习惯,例如-+-001、-x*-003以及许多空白符。因此,我使用一个工具类来把表达式预化简,使用正则匹配来循环匹配连续的加减号并合并、匹配前导零并去除、去除空白符……此外,为了减轻Parser的工作量,这里还把乘方预展开了,这样Parser就不需要考虑乘方的情况。
最后我想讲一讲“Derivation类”,
这个是第三次作业的求导类,求导的具体思路如下:
二、开发历程
1.第一次作业
第一次作业是从无到有的开发,也是最费时费力的一次作业。
在最初着手开发时,我参考了课程组给出的建议,设计了Factor接口和下属的Num、Var类。
随后我遇到了第一个难点——类的上下交互
我最初的设计是这样:
但是,这样的工作量太大了,要设计多种乘法,而且还牵扯到类的上下转换,太过复杂,容易出错且不容易找bug。
于是,我马上进行了第一次重构
在新一次的设计中,我采用了现在的Term设计,流程就得到了大幅度简化:
之后,第一次作业的最大难点便解决了,之后只需要进行合并同类项即可,而这种设计中,只需要比较两个Term的指数是否相同即可。
2.第二次作业
在第二次作业的迭代开发中,我新增了Func类,用来存储自定义函数,并使用字符串替换的方法将输入的表达式预处理,这样一来,第二次作业和第一次作业的区别就只有三角函数了。
对于三角函数,我的处理如下:
至于三角函数的化简合并,我只做了最简单的处理,并未加入各种复杂的公式。
3.第三次作业
在第三次作业的迭代开发中,由于我的第二次作业已经可以处理自定义函数定义的嵌套调用,于是只需要考虑求导。
为此我增加了求导工具类“Deviration”,具体实现在第一部分已经介绍,不再赘述。
三、Bug分析与Hack策略
1.我的Bug
在第一次作业中,我忽略了指数的前导零带来的影响,导致在计算指数的时候出现错误,最终解决方案为改为使用Integer.ParseInt()来解析指数,避免了前导零的影响。
第二次作业在设计之初,由于深拷贝和浅拷贝问题,导致了三角函数的指数化简出现问题,最终自己写了递归深拷贝方法来解决问题。
在强测中,由于括号检测的优先级问题,导致三角函数括号不对应,更改后解决问题。
第三次作业未发现Bug。
2.Hack策略
第一次作业中,着重检查了连续符号、前导零、大数运算这几个点,最终Hack成功6次。
第二次作业中,配合简单的评测机,着重检查了三角函数和自定义函数的相互嵌套、三角函数和自定义函数的指数运算、无意义多重嵌套括号等,最终Hack成功5次
第三次作业中,配合简单的评测机,着重检查了嵌套三角函数的求导、函数定义求导和相互调用、自定义函数和三角函数以及求导运算的嵌套,最终Hack成功4次
3.测试小结
在测试过程中,我注意到“单元化测试”的重要性,即完成某一部分的功能后,立即进行单独测试,这样可以及时发现错误,避免功能复杂后难以Debug。
同时,在Hack过程中,我也注意到部分同学进行了更多的化简运算,取得了更高的性能分,但也因此出现了Bug导致被Hack,这或许是对应着“多写多错”,在这一方面,应当更加注意测试与思考,避免因小失大。
四、心得体会
1.论规范化与系统化
这一单元的作业中,最麻烦最困难的在我看来反而是第一次作业,一方面是假期刚过,需要进入状态,另一方面是从零开始,码量和思考量都比较大。以至于,第一次作业大概花费了20到25小时,用了两天的时间才写完,最终成品是450~500行。这是最艰难,最折磨的部分。
当然,最终在第一次作业的实现中,也并不是很“面向对象”,这既与题目要求和设计有关,也和我思维的转变有关。
我前面也说过,第一次作业设计之初,是希望非常系统、规范地定义各个类和方法的,但是无奈最终实现太过复杂,让人望而生畏,于是我在第二天上午一边继续完成,一边重新构思,然后在中午放弃了之前一天半的设计,然后用一个下午完成了现在的版本。(所以实际上或许,这次作业的完成时间大概在7~8小时)
关于这一点,也是我最大的体会,就是系统化带来的是高可读性和高可拓展性,而简单化带来的是快速的完成度和简易的上手度
在此之后,我当然也会再进一步去学习系统、规范的方法,但是要我在第一周就完成这样的设计或许真的不太现实。
2.关于迭代开发
在进行第二次作业的设计时,我认真考虑了迭代开发工作的进行。
我认为,重构的情况可以有,但是大规模重构显然是有问题的:要么是第一次作业可拓展性太差,属于架构不够合理;要么是迭代工作不到位,没有认真考虑迭代工作。
关于迭代开发,我当时也在讨论区发布了一个帖子。简单来讲,就是我希望通过“封装复用”以及“递进开发”来进行迭代工作。 而事实上,这种思想也卓有成效,让我在二、三次作业中均避免了重构,且减少了工作量。
最终成品,第二次作业代码量是700行,用时5小时左右(但是后面Debug也花费了一些时间);第三次作业代码量是900行,用时4小时左右。
当然,在代码量上,由于部分方法没有进行单独抽象,实际上复制粘贴也占一定的比重。这种行为是不规范、不合理的,在之后的设计中我也会尽量避免这种偷懒行为。