BUAA_OO 第一单元反思与总结
OO博客作业—第一单元
第一次作业
架构分析
在第一次作业开始之前,我对于面向对象已经有了一定的了解,但是对于如何进行面向对象的编程却是一无所知,所以这一次作业进行了尝试,但却形成了一个四不像。
做完了三次作业再来看这次的架构时,这种架构显然是显得惨不忍睹,虽然强测是\(100\)分,互测也没有出\(Bug\),但是这次作业的架构我认为是远远不如第二三次作业架构的。在这次作业中,\(Items\),\(Item\)和\(Factor\)类完全做到了高耦合,而且\(Factor\)类的通用储存方法在第二,三次作业中显然是不可用的,并且在\(MainClass\)中的预处理字符串在后面加入\(Wrong\ Format\)的作业看来也是极其不可取的,我早在预习之前就看过了上届学长们的指导书,但是一直没有思考出三次作业通用的架构出来,这第一次作业架构写成这样也是做好了心理准备,但是类之间的耦合度之高,类内部的复杂度之高和整个作业的拓展性之低,复用性之低,还是突破了我的底线。与其说这是一次糟糕的作业,不如说正是这一次糟糕的作业使得我下决心在第二三次作业中采用了递归下降词法分析的方法。
类图分析

首先,通过\(MainClass\)类进行输入读取,因为此次作业没有\(Wrong\ Format\) 的需求,所以我将空格和制表符全部去掉,并且将多余的运算符化简,比如说\(+-+\)化简为\(-\)。再将所有的\(-\)化为\(+-\),这样使得表达式的每一项的运算符都是加号,从而减少了符号的储存。然后进入\(Items\)类的构造方法,创造出一个容器来储存\(Item\)类,即项。在\(Items\)类的构造方法中又会进入\(Item\)的构造方法,创造出一个容器来储存\(Factor\)类,即因子。在\(Item\)类的构造方法中又会进入\(Factor\)的构造方法,在\(Factor\)的构造方法中通过正则表达式进行对字符串的处理,\(Factor\)类可以通用储存变量因子和常量因子。以上就是表达式的分析与存储。最后再通过方法遍历\(Items\)来进行合并同类型和求导,最后对\(MainClass\)返回一个字符串,由\(MainClass\)进行输出。以上所有对字符串的操作都由\(StringOperation\)类完成。
复杂度分析

\(ev(G)\)即\(Essentail\ Complexity\),用来表示一个方法的结构化程度,范围在\([1,v(G)]\)之间,值越大则程序的结构越“病态”。
可见\(Item\)中的输出表达式方法,即\(getExp\)方法的\(ev\)值较大,我认为这是因为有着\(x**2\rightarrow x*x\),\(x**1\rightarrow x\)和\(x**0\rightarrow 1\)的特判与频繁调用\(Integer\)的\(toStrin g\)方法所导致的,在\(ifTheSame\)方法中也频繁调用了\(Factor\)类的\(get\)方法,这也导致了其\(ev\)值增大。而在\(Items\)类中的构造方法中,也没有将可以通用的方法抽象出来,从而导致了结构过于复杂,在\(firstPlus\)方法中也有这种情况,我认为这便是这两种方法的\(ev\)值过高的原因。

\(OCavg\)代表类的方法的平均循环复杂度。
在\(Items\)类中因为要不停的循环来合并同类项并且进行优化,所以平均循环复杂度较高。
优缺点分析
优点
这次作业架构最大的优点就是容易实现,易于思考,\(DeBug\)比较容易,调试时思路较清晰。
缺点
这次作业架构的容易实现和易于思考造就了架构的低拓展性,低复用性,对于下两次作业帮助性极少。
第二次作业
架构分析
这一次作业我在第一次作业的互测结束后就开始构思了,也想了很多方法,但是总觉得实现起来极其麻烦,就迟迟没有开始设计,到了老师上课的时候,老师说建议用递归下降词法分析来实现第二次作业,便着手研究这个方法,网上查了许多资料,期间解决了很多麻烦,包括阅读指导书中的文法,如何消除左递归,最终到了思考如何实现这次作业这一步,我发现,这个递归下降方法天然适配检查\(Wrong\ Format\)这个需求,但是如何储存所分析出来的数据成了问题,首先是储存格式,我采用了指导书最后所推荐的方法也就是
-对于每一种函数(常数、幂函数、三角函数),建立类
-对于每一种函数组合规则(乘法、加减法、嵌套),建立类
-对于上述的两种类,均实现一个求导接口
-通过上述两种类及其求导接口,把整个表达式构建为树结构,进行链式求导。
不同的是我在接口处增加了一个方法也就是输出自身表达式的方法,这样可以适配乘法的求导也可以通过输出建立的树的表达式来\(DeBug\)。
储存格式解决了,但是如何建立表达式树呢,我想起了数据结构所讲过的建立表达式树的方法,即数字和符号分别用两个栈储存,再通过栈建立表达式树。通过分析,我决定只建立一个栈,只储存函数组合规则和函数共同的接口,每读到一个运算符就将其前两个接口分别作为此运算符的左子树和右子树,最后将栈里唯一的接口拿出来即为这棵表达式树的顶点,这样的优点是不用单独分析括号来建立树,括号之内的表达式一定只存在在一棵树上,且这棵树只包含这个表达式,只需要委托输出判断是否加括号即可,但是缺点也很确定,也就是建立的树的深度极其不均匀,时间复杂度可能较高。
问题都解决了,实现时因为没有\(Wrong\ Format\)的需求,我依旧是先将空格和制表符都去掉,然后化简运算符,然后才进行递归下降词法分析同时建立表达式树,表达式树建立后,用接口中的求导方法对表达式树的顶点进行求导,也就是对这棵树进行\(DFS\),期间调用所有节点的求导方法,在\(DFS\)期间通过传递一个字符串并在其后不断添加字符的方法使得这个字符串最终为求导的结果,然后最终返回这个字符串即为求导所得。
对于此次作业的优化问题,针对因子,我的方法是在输出求导式和自身表达式时进行幂函数的0,1次方判断并进行改变输出字符串的优化。然后针对组合规则,我的优化方法是通过判断是否为0或者是否为因子来判断是否不加括号,或者直接切掉无用输出,这两种方法特判极多,我认为写的不是很清晰。但是复用性极好,我的第三次作业在这方面只改了\(Sin\)和\(Cos\)的求导优化。
类图分析

首先通过\(MainClass\)进行字符串的读入和预处理,然后进入\(RecursiveDrop\)进行递归下降词法分析来建立表达式树,函数组合规则中储存了它的左右子树,因子作为叶子储存了必要的\(BigInteger\)数据。最后通过调用接口\(Element\)的求导方法进行求导,并且返回一个字符串,即为求导后的结果。以上所有对字符串的操作依旧都由\(StringOperation\)类完成。
复杂度分析

在方法中的\(ev\)值分析中,可以见到,许多的求导方法\(derivation\)和输出自身表达式的方法\(expression\)的\(ev\)值都过高,我认为这是因为在这两种方法中进行优化的特判极多所导致的,我自己目前还没有好的解决方法。
优缺点分析
优点
这次作业架构最大的优点就是通过吸取了第一次作业的教训采用递归下降词法分析和表达式树方法,这使得程序的复用性和可拓展型极好。
采用了统一接口,使得表达式的数据储存极为方便。
对表达式树求导时只需要调用一次树顶点的求导方法即可轻松返回结果,极其方便。
缺点
在函数组合规则的优化中所进行的特判极多,极易出错。
表达式树不利于合并同类型之类的高级优化。
第三次作业
架构分析
因为第二次作业没有出现\(Wrong\ Format\),所以早在中测结束时我就开始了如何加入此需求的思考,有三条路摆在我的面前,前两条路都是和建立表达式树分开来检查,分别是通过正则表达式来检查和再写一个专门检测\(Wrong\ Format\)的递归下降类,但是我还是选择了第三种,即建立表达式树的同时,检测是否有\(Wrong\ Format\)出现,因为我认为递归下降天生就是做检查\(Wrong\ Format\)的工作的,不必再多写一个,也因为我对第二次作业所写的递归下降的扩展性有信心,虽然去掉了\(MainClass\)中的字符串预处理,并将其糅合到递归下降中着实有些许复杂,但是因为思路清晰,所以只是代码写着比较复杂,其实难度较小。
这次实现也遇到了一些问题,就是在消除左递归后,表达式文法被分为了两个,项的文法也被分为了两个,所以我在思考如何判断是进入还是不进入第二个表达式或者项,最终,我采取了预读一个运算符的方法,如果是加或减,则进入下一个表达式,不是,则退出。这样看起来似乎没问题了,但是随着测试的深入,又有一个新问题出现了,就是如何区分因子中的表达式和最初的表达式,比如\((x))))\)这个测试点,当因子返回到表达式后,表达式检测到下一个字符是\()\)而不是\(+\)或\(-\)时就会退出递归下降而不会报错,为了解决这个问题,我在表达式之前又新增了一个入口,那么因子里的表达式退出会退出到因子里,而最初的表达式退出会退出到新增的入口里,在这里新加一个判断字符串是否读完的语句即可判断是否为\(Wrong\ Format\)。
另外,为了判断次方是否大于50,我也通过新加了一个方法来使对于次方的处理区别于其他数字的处理。
针对\(Sin\)和\(Cos\)中的因子,我只需将这两个类中储存的数据换做一个因子,再更改求导规则以及优化规则即可。比较简单,这里不再赘述。
类图分析

此次作业的\(MainClass\)只负责读入字符串将其传入递归下降类中,而取消了预处理。其余都和第二次作业的思路完全一样。
复杂度分析


这次的代码复杂度依旧大部分集中在因子求导和组合规则求导中,原因因为依旧有许多的特判。另外因为加入了\(Wrong\ Format\)递归下降也变得比较臃肿,虽然符合代码风格,但是其中依旧有些重复的部分,这也导致了它的高复杂度。
优缺点分析
优点
对表达式树求导时依旧只需要调用一次树顶点的求导方法即可轻松返回结果,极其方便。
缺点
在函数组合规则的优化中所进行的特判极多,极易出错。
表达式树不利于合并同类型之类的高级优化。事实上,本次作业的优化相比于第二次作业的优化来说是几乎没变的。
递归下降中重复造的轮子有些多了,看着有些混乱。
分析\(Bug\)
本人的程序在强测与互测环节中未发现\(Bug\),在弱测中曾经出现过乘法输出自身表达式时未加括号从而输出错误的\(Bug\),在优化输出后就消除了。
本人在互测环节未使用自动评测机,而是手动构造测试点,主要针对程序自身的边界条件以及优化出现的特判问题。
第一单元总结
虽然我在寒假就已经略读过一边上届的指导书了,但是写这三次的作业难度还是有些超出了我的预期,写第一次作业时,满脑子想的都是如何用正则表达式应付第二三次作业,后来接触到递归下降这种方法,我才有了一个确定的方向。我认为认真去思考如何实现一个架构还是很重要的,先是要评估需求,并且考虑后续的需求,然后写出一个高内聚,低耦合,扩展性强,易于维护,并且考虑后续迭代需求的架构,是很重要的。同时,要去勇于拥抱自己不会的新技术,我在学习递归下降时也是犹豫了比较长的时间,考虑这个架构的可行性到底大不大,但是架构最终证明了递归下降确确实实比正则表达式思路清晰,容易迭代,可以说用了合理的架构比如说递归下降可以起到事倍功半的效果。
在完成第一单元后,回过头来,老师,助教和讨论区的同学们给了我很多思路,我首先要感谢他们,在完成作业的过程当中,本人也学到了很多东西,比如说递归下降方法,工厂模式,写出后两次作业的成就感也很高,总之,第一单元的旅途虽然很紧张,但是还算愉快。

浙公网安备 33010602011771号