OO第一单元小结

  秉承着计组学习到的设计与实现分离的原则,设计部分的反思都在历次作业的回顾中,而bug的出现大多是与代码的实现方式相关,所以把自己的bug分析加到了代码结构的部分。

一、历次作业回顾

  1.第一次作业

    (1)整体思路回顾

  第一次作业难度现在看来较低,但是当时的主要难度在判断表达式非法与读入数据,尤其当时的我还并不知道可以用大正则直接判断整个表达式的合法情况。

  输入部分:考虑到随处可添加的空白符真的很烦,所以我想在排查过所有可能出现的有关空白符的输入格式错误后,删除掉空格和'\t',之后无论是正则表达式还是处理符号就都不用考虑空白符的问题了。这样做了之后优点确实很明显,但是带来的问题可能是毁灭性的。如此做法相当于从反面考虑问题,即需要考虑到所有可能的非法情况,如果有遗漏,就一定判断不出来。

  在删除掉空白符之后,判断合法的思路又变回了正向判断:只要设定了所有合法输入的格式,则合法格式的数据就一定可以被读入,如果出现表达式尚未结束但是读不进来数据,即为非法。这种设计思路的优缺点其实也很明显:优点是写出合法输入很简单,特别是第一次作业每个因子只有5种合法形式。但是缺点是需要手动处理琐碎的字符串,只要考虑不仔细,在这里很容易出现小的bug。

  第一次作业难点主要就在于输入的处理,之后的求导和输出只要处理了系数和次方数可省略的情况即可。

    (2)代码结构分析

  整个作业我设计了3个主类:控制、多项式、项,还有两个类是实现了Comparator接口,用来给多项式依据不同参数进行排序的工具。 

  可以看到Poly的构造方法是不需要任何参数的,原因是我把输入系统放在了Poly类中。在第一次研讨课上,有一位同学分享的观点对我启发很深,每个类就要做他该做的,不该他做的该谁做谁做。所以显然在这里要把输入放在控制类里。

  另外这里还有一个值得商讨的地方:Poly到底该如何生成Term(项),即Term如果只有一个构造方法,那应该传入什么样的参数。传入未经处理的参数和传入处理好的参数这两种选择哪种更符合面对对象的思想。我当时的处理是把判断为项的字符串直接丢进项,具体是否合法、项的拆分都交给他的构造方法去判断。我也看到有的同学给Term构造方法传递的就是一个系数和一个指数。现在看来,如果多项式的构造方法中参数是一个原生字符串,那还是向项的构造方法中丢原生字符串;如果输入是在另外的类中处理,而多项式的构造也是依据准确的参数,那显然应该也向项中传入准确的参数,即应在构造的传递中保持一致性。但是如果再考虑上类的复用,在其他地方应用到这两个类是未必经过处理的,还是传入原生字符串较为合理。

  Poly类的求导方法是通过逐项求导实现的,而每项的求导方法是更改自身的属性,使自己成为一个全新的项。所以表达式求导后并没有生成新的表达式,而是把自己变成了求导后的样子。由于此次作业较为简单,感觉这么做好像没有什么问题。

  整个代码结构也很简单,Start控制程序入口,生成多项式,在多项式中构造每一项。

  构造方法的耦合程度略高,两个明显代码集成度低的方法是在大量利用if进行输出的化简,感觉没啥好方法对其优化。

    (3)bug分析

  第一次作业写完之后只有一个bug,即上述反向设计方式所带来的:没有考虑全空白符的非法情况。当时遗漏了指数中带符号整数符合和整数之间不可以存在空白符的情况,幸运的是在发现得早,没有带入测评,不过也让我知道了反向设计的危险性。

    (4)小小结

  第一次作业写完以后我已经意识到了这种反向判断合法性的方式其实可扩展性很差,稍微再复杂一点有可能就会考虑不全。而第一次作业其实也有很多没考虑到就上去胡写和很多不符合面对对象思想的地方。但是整体设计思路没有大的问题,细节处理也比较细腻,再加上题目比较友善,所以没什么大的问题。

  2.第二次作业

  第二次作业有一点一言难尽,还是慢慢来吧。

    (1)整体思路回顾

  第二次作业的难度主要在于对三角函数表达式长度的优化,优化的方法见仁见智,但是切记选择适合自己代码结构的优化方法!

  输入部分:第二次作业的指导书中,在助教们十分善意地提醒了我们要注意代码的复用后,(然而事实证明我这个水平的无论如何设计,还是没法在之后的作业中复用代码的)我毅然决然地抛弃了第一次作业中反向判断非法的方式,采用了大正则判断表达式合法,而后放心地读入数据。事实证明在读入数据量有保证的情况下,对表达式整体有一个正向的判断方式还是最让人放心的。

  数据结构的选择:参照指导书中的建议,我采用了hashmap取代了第一次作业中arraylist作为表达式存项和项存因子的数据结构。hashmap在读入时自带合并同类项,在方便使用的同时也为我之后的爆炸埋下了伏笔。个人感觉如果数据量较小的话,hashmap读入是没有arraylist读入加合并快的,因为hashmap需要频繁计算hash值,而arraylist的合并、排序算法的复杂度都是O(n2),在我们的测试数据量中n并不大,所以其实arraylist的速度更快。并且考虑到hashmap存入数据后对key访问的不方便,以及如果需要排序是很麻烦的一件事(如果有知道如何解决此二者的大佬求指导),如果现在再让我重构第二次作业,我会选择arraylist。

  处理数据部分:在第二次作业中,我的想法是因子求导后会生成项,而在项求导后会生成表达式(然而此次作业唯一有前瞻性的一点在第三次作业中也没有沿用)。求导后再次调用表达式的构造方法即可生成一个新的求导后的表达式。

  优化部分:

  优化的目标其实是很清晰的,利用sin2(x)+cos2(x)=1(或1-sin2(x)=cos2(x))把表达式化简到最短。但是优化的难点有二:首先是不确定是否要对符合条件的一项或两项做变换,每一项有多种变化方式,有的时候化简以后表达式反而会变长;其次是可以多次化简,每次变换有多种形式,我们无法得知究竟如何化简才能达到最终的最简形式。对此我的解决方式是:尝试每次可能的变换并记录下来,直到没有符合条件的项,最后选取过程中最短的一项输出。变换需满足的条件分为两种:1.两项可以合并 2.单项通过sin2(x)=1-cos2(x)变换后的表达式长度小于等于原表达式。对每一项做上述判断,如果条件成立,则新生成一个变换后的表达式,新的表达式也可以继续做判断。整个化简过程就变成了一个表达式树,而存储并遍历树的方式可以使用数据结构中所学:使用队列非递归遍历树。这样优化基本可以保证优化为除去平方差公式一类的非三角函数公式外的最短形式(包括各种骚操作)。但是!但是!我所使用的数据结构是hashmap,所以每次新生成一个表达式都要经过一个繁杂的构造方法(相较于arraylist),再加上如果测试数据满足变换条件的项多,生成上千个表达式是轻轻松松的,有超出规定cpu时间的危险。

  所以在和大佬同学讨论后,经他推荐,我最后一版采用了另外一种优化方式:试错法。此种方法成立所必须依赖的假设是:达到最简形式的每一步并不会使表达式变长。对表达式的每一项,考虑每种变换,如果能够使表达式不变长,则直接进入新的表达式判断,而不是在原表达式中继续判断下一项构造队列,终止条件则是遍历表达式后无法生成新的表达式。这种方法的完备性看上去比第一种方法稍差,但是其实应该是一样的。条条大路通罗马,第一种方法其实是把所有可能达到最简形式的路径全部记录下来,而这种方法是只选择其中一条道路。由于对于每一项可能的变换方式总可以在遍历到该项时达到,所以完备性应该是相同的。

    (2)代码结构分析

  类的设计基本与第一次作业无太大差异,但是其中的因子引入了enum域表示其因子类型,这点是第三次作业继承因子基类的雏形。

  

  冗余方法很多,是因为中间断断续续改版了好几次,导致添加了很多的方法。而修改以后原方法不再使用但也没有删除,所以并没有符合尽量少添加方法的原则。

 

  类之间的关系图较比第一次作业稍微复杂一些,但是也还算清晰,分层次构造思路比较明显。

  看到这个图才发现原来自己的代码耦合程度这么高,但是我发现包含多层嵌套的if-else的方法非结构化程度时高时低,不是很明白为什么。

    (3)bug分析

  第二次作业的bug主要是由优化引起的。犯过的第一个bug是设计导致的:按上述优化方法,需要考虑把式子转换成新式子后在转换回来的情况,即判断生成的式子是全新的。另外一个致命bug是在优化合并同类项时没有处理好重写的equals方法的调用,主要还是思路不清晰。

    (4)小小结

  坚决不要卡线提交!再卡线我是狗。哪怕知道自己的程序存在小bug或者代码风格分不满,否则没进互测得不偿失。本次作业应于周三上午9点前提交,然而因为周三早上起来以后突然想起自己的程序存在一个bug,一通操作最后提交上去了一个不三不四的版本,结果就是中测有一个点tle,没有输出。问题就在于优化的方法夹在两个版本之间有个变量的初始化没有处理好。

  3.第三次作业

    (1)整体思路回顾

  第三次作业可以进行括号和因子的嵌套了,由于java正则的限制,没法直接使用大正则,也就不得不回到了第一次的读入数据方式。以因子为单位,一个一个地读入,根据其后面的加号或者乘号判断是否生成新的一项。整体思路上依然还是和第一次一样采取一个反向思考的模式,也导致写出了很多小bug。

  在结构上设计了一个因子的基类和5个继承类。这次的设计经验不足,为了使用多态把很多域放在了基类中,子类利用基类子对象中的这些private域保存数据。但是其实现在看来很没必要,增加了访问次数,给基类添加了很多只有子类会调用的访问和设置方法,而且由于这些域的定义都在基类中,也降低了子类代码的可读性。每个子类设定自己的域就好,如果共同的域很多不想重写,其实可以考虑设计一个包含这些域的类并实现接口,利用组合而并非继承

  求导方面,因为从一开始就没打算优化代码,争取结果正确就好,所以递归求导可以直接生成可输出的字符串,而并没有像第二次作业一样求导生成上一层的对象。

    (2)代码结构

  类里面写了很多最后没有利用的有关合并同类项的方法。

  为了多态能够知道现在是在何种因子中,像第二次一样设定了一个enum类,并添加到了每种因子中,但是其实一个getClass()就可以解决。

  代码耦合程度很高。

    (3)bug分析

  此次作业出现的bug是最多的,主要分为两类:实现时考虑不周导致的WA,和设计时为考虑周全导致的TLE

  考虑不周导致的WA:主要还是因为是反向考虑,需要考虑到在何处可能出现什么样的bug并处理掉,有一点面对数据编程的意味。这次出现的此类bug都在输入数据部分。

  TLE:此次作业设计时在判断部分的最内部循环采用了一个toString方法而不是设置访问器方法,直接导致了如果括号嵌套很多的时候递归求很多次toString,直至超时。

    (4)小小结

  继承和多态的使用方法还是应该在设计的时候就考虑好,这次在设计的时候只考虑了继承,在优化的时候想利用多态却发现自己调用的是父类子对象,再一个个改很麻烦且没必要。

  如果能够避免反向处理数据还是要尽量避免。


 

二、互测debug方式

  互测中debug我主要采用的是黑盒方式,特殊数据单独处理。

  历次作业中互测debug主要分为两个阶段:构造数据和测试。

  构造数据:如果是像第一、三次作业,从反面考虑所有非法情况,构造非法情况的数据就很简单了。把所有特判的地方记录下来给别人测。另外再单独构造一些***钻的数据。针对正确性的情况,本人采取的是写一个数据自动生成程序。这个自动生成程序的核心是随机数,通过调节随机数的生成范围还可以调配生成不同数据的比例。至于非法数据的构造,我设置了一些概率很低的插入到表达式各处的合法字符,结果未必不合法,但是也测出了两个不同人的bug。

  测试:使用shell写了一个简单的运行所有人程序的脚本,再利用自己的程序把他们的结果读入并输出,比较是否相同。


 

 三、设计模式

  三次作业的设计过程并没有把输入,处理数据,输出三个部分分得特别清楚,可能是因为题目的输入部分难度较大,我把更多的时间投入到了如何设计处理输入。最失败的地方是在设计的时候只会去想如何优化,但是没有考虑优化方法能否结合自己的设计结构,这点直接或间接地影响到了第二次和第三次作业后期的优化工作,设计流程的完整性需要有保障

  另外在三次设计中,第二次的设计是最失败的,只想到了利用hashmap,但是没考虑用了hashmap以后很多方法的使用方法和arraylist也会有所不同,设计的时候确实无需考虑实现的细节,但是确定了实现方式还是要考虑其对其他部分所带来的影响的。


四、最后

  接触了一些面对对象的基本常识性的东西,感觉面对对象的思想的重要性会逐步增加。就这个单元来说,作业和对面对对象的理解都不好,感觉还是程序写得太少,每次实现的时候都有貌似选择恐惧症的无从下手,面对很多种实现选择并不知道哪一种好。可能还是只有多写点东西才会对面对对象有点感觉吧。

  Good Luck & Have Fun

posted @ 2019-03-26 13:27  ericliu0206  阅读(141)  评论(0编辑  收藏  举报