一、三次作业简单介绍
第一次作业:简单多项式导函数的求解,仅包含常数与幂函数。
第二次作业:包含简单幂函数和简单正余弦函数的导函数的求解,在第一次作业的基础上,增加了sin(x)、cos(x)和导数的四则运算。
第三次作业:包含简单幂函数和简单正余弦函数的导函数的求解,在第二次作业的基础上,增加了复合函数的求导。
二、三次作业的类图与度量分析
第一次作业:
1、架构构思:
第一次作业是我第一次接触面向对象的程序设计,没有经验,再加上这次作业并不是非常需要“面向对象”,我的设计也比较偏向面向过程。这次的难点就在于读入字符串的合法性审查和多项式分割存储,我专门设计了一个 StringChecker 类用于完成这项功能。由于所有的项都可以写成 axb 的形式,为了简化程序,我仅仅设计了一个 Poly 类,并将读入的字符串将 +- 变为 +-<space>,用<space>进行存储。最后是一个主类用于完成多项式的主体求导和输出功能。
2、项目分析:
类图
度量分析
3、自我总结:
可以非常明显的发现,主类的Cyclomatic Complexity非常非常高,重点就在于 output 方法的设计问题,导致其Cyclomatic Complexity 和 Design Complexity 很高。
一方面,我应该将主类设计的越简单越好,将 parsePoly、computePoly 和 output 方法的实现扔出去新建类;另一方面,我应该重写 Poly 类的 toString 方法,简化程序的输出,降低复杂度。
除此之外,我的StringChecker 类设计的是有些丑陋的,其中包含了很多格式特判,这主要是因为我对正则表达式的使用熟练度不足。
4、程序bug分析:
非常幸运的是,我的程序在强测环节和互测环节并没有被查出bug,但在历次提交和我的自我测试中,我在不断修复的时候明显感觉到了自己的设计问题。主要问题件就是前文提到的output 方法和StringChecker 类的编写。
StringChecker 类中的合法性检查方法,我的正则表达式都是一个个小正则,这样的好处是避免了爆栈问题。但是由于我在初次设计时没有考虑完善,所以是在一个不断修补的状态中的,并不是非常流畅。
在 output 方法的实现过程中,因为我为了缩短输出长度,它的循环复杂度很高,我在输出时有很多错误,debug的时候很困难,这也是自己的设计问题,应该结合Poly 类的 toString 方法,这样的好处不仅仅在于降低复杂度,还简化了debug的难度。
5、性能分析:
我在这次作业中使用的是 ArrayList 存储多项式,使得我在合并同类项时还需要进一步的处理,使得程序复杂。同时,我没有考虑将 + 提到最前的问题,使得很多题性能分没有拿满。
应该使用 HashMap 存储多项式,自动合并同类项,在输出时按照系数排序,由大到小输出。
6、互测分析:
第一次作业的互测屋气氛非常友好,没有狼人。
由于本次作业并不复杂,我的策略就是专盯大家的合法性检查和输出这两部分。大正则容易爆栈或者出错,小正则容易漏情况,这都是经常出现bug的地方。同时,在输出时由于性能分的存在,x、-x,0等情况也都容易出错。
我按照每个人程序的代码设计结构分别构造被测样例,进行提交,有效性不错。同时我也学习到了很多正则的写法、输出的优化等等,收获很多。
另外,本次互测大家状态透明,其实是比较容易发现bug的,每次状态更新后简单分析即可得到每个人大致的bug情况,进而有针对性的检查。
第二次作业:
1、架构构思:
第二次作业相比第一次,面向对象的特性有所增强。我在课上刚公布题目的时候,就想到了每一项都能写成 kxasinb(x)cosc(x) 这种形式,写成这种形式的好处是几乎与第一次作业在架构方面相差不大,便于处理。坏处就是我们都猜到了第三次作业肯定是复合函数,然而这种形式可扩展性几乎为零,无法适应后面的任务。
所以我在设计之初,构造了 Factor 抽象类,并通过继承得到 Cons、Power、Sin、Cos 类,增强可扩展性。然而,想的总是比做得好,由于自身水平问题,我在求导上卡了非常长的时间,最终无奈放弃,转回最初的设计方法。
有了第一次作业的教训,我将 (a,b,c) 设计成 HashMap 的 key ,将 k 设计成 HashMap 的 value。重写hashCode 和 equals 方法,完成 Term 的设计。并且听取了研讨课上大佬们的意见,将 Error 写成静态方法,不断调用。拆分 PolyChecker 类的检查方法,分而治之。主类大大简化,只负责类的创建与方法的调用。余下的事情就非常简单了。
2、项目分析:
类图
度量分析
3、自我总结:
可能是因为还是不熟悉面向对象,加上中途换道的影响,这次的分析结果非常差劲。大量方法的Essentail Complexity、Cyclomatic Complexity 和 Design Complexity 都有很多问题。Term 类和 Poly 类也由于构造和合并同类型的原因循环复杂度极高。
虽然这次我采取“能扔就扔”的策略,让每个类不那么臃肿,但是部分方法的循环问题却是我之前没有想过的。大量的循环导致了方法的复杂,同时也大大增加了出错的可能和debug的难度。
非常羞耻的是,这次的输出我考虑到了 toString 方法的重要性,但在具体实现时为了偷懒使用了打表的方法,使得 print 方法非常丑陋。
4、程序bug分析:
同样非常幸运,我的程序在强测环节和互测环节并没有被查出bug。
在历次提交和我的自我测试中,这次作业的出错总体来说还是比较少的。原因主要在于这次的 PolyChecker 结合了上次互测屋中其他同学的优秀设计经验,分治效果非常明显。另外,虽然 print 方法很丑陋,但是丑的好处是考虑全面,避免了输出的bug。
再加上我的化简方法非常捡漏,使得化简的bug非常容易显露,便于将bug扼杀在摇篮里。
5、性能分析:
这次作业除了使用 HashMap ,正项提前外,化简方法只考虑了非常简单的 sin2(x)+cos2(x)=1 这一最简单的形式,使得我的性能分几乎全部损失。
我在讨论区中也看到了很多大佬思路的分享,但在当时感到很难实现,加上自己容易写成bug,最终放弃。
6、互测分析:
第二次作业的互测屋气氛仍然非常友好,没有狼人,加上对hack情况的隐匿,群起攻之的情况也消失了。
这次作业大家的设计复杂度剧增,加上第一次互测的经验,很多简单的测试比如爆栈、空输入等没有成效。于是我在同学的帮助指导下采用了自动评测的方法进行测试,然而由于我的测试样例生成程序是用正则表达式自动生成的,导致我仍然需要手动构造错误样例,最终的结果就是初尝自动评测非常失败,最后仅仅找到一个bug。并没有结合被测程序的代码设计结构来设计测试用例。
于是在后期我已经转为学习他人代码架构,为第三次作业做准备。我发现大家更多也是采用与我类此的方法,其他更精妙的设计还是少见,更多学习的还是小的细节,有点遗憾。
第三次作业:
1、架构构思:
构思过程简单来说,就是四个字——一脸懵逼。第二次作业的构思过程已经让我品尝过一次失败,这次依然有着非常多的困难去让我克服。
这次我仍然选择了构造 Factor 抽象类,并通过继承得到 Cons、Power、Sin、Cos 类,有了前两次的经验,我在这些因子的内部就重写了 toString 方法和 clone 方法,降低输出的复杂度;按照统一的结构,给每个因子设计专有的 diff 方法,为之后的求导做准备。同时 PolyChecker 类也延续了第二次的模式,进行改进。
然后,就没有然后了,我一直卡壳。
幸运的是,我的朋友一直非常无私的帮助着我,为我解答疑惑。我按照讨论区提到的构造树状结构求导,回顾了大一下的数据结构内容,采用中缀转后缀进而构造树结构,完成了 Poly 的设计。
接下来,求导和输出就是一个通过树不断递归的过程。在输出中,我感受到前两次作业中化简的一些不便,这次并没有采用直接输出的方法,而是利用 StringBuilder 做一个缓冲,减轻输出判断的负担。
2、项目分析:
类图
度量分析
3、自我总结:
这次设计我划分的很细致,3个包,14个类,使得类图比较庞大。
由于字符串的解析、中缀转后缀和树的构建中的的循环分支判断(while、switch),求导和输出的递归,使得这次设计相关类的循环复杂度依然非常高。
但是因为这次作业的难度很高,我自己已经是拼命完成的,所以我对复杂度的优化感觉并不是非常有头绪,还是希望能够看到更多大佬的设计和建议。
4、程序bug分析:
这次就没那么幸运了,我在强测中爆了3个点,互测中被hack了7次。
在中测中,由于前文提到的复杂度过高,我的debug可以说是非常非常困难。需要不断的尝试、尝试、再尝试,过程非常的痛苦,最终才能找到bug,而bug果然也是在循环复杂度最高的中缀转后缀和树的构建这两部分内容中。这种复杂度过高的情况是我之后必须要避免的,无论是bug出现的可能性还是debug的难度都是非常高的,很影响代码质量。
强测和互测这一共10个样例错误,最终发现是一个问题:我在中缀转后缀的过程中,由于是按运算符判断,对于 *+\d 和 *-\d 这两种情况就会存在误判,解决办法是在中缀转后缀前,将 * 替换为 *# ,避免这种误判。
5、性能分析:
这次作业能活下来就已经很艰难了,自动放弃性能分。
6、互测分析:
第三次互测屋可以说是非常不友好了,厮杀极其惨烈,狼人非常多,还有狼王。
这次互测我在之前自动评测的基础上,采用了 Nemo 大佬在Github上提供的利用递归下降编写的生成样例程序。互测效果非常显著,同时在自动评测发现bug后,结合被测程序的出错点,构造样例测试确定bug类型,尽量避免同质bug。
三、Applying Creational Pattern
1、架构构思:
我在三次架构设计中采用的方法是,先思考所需的类和方法,创建它们并且不实现具体属性,不断尝试组合与构造,不断修改,最终得到一个合理的框架结构,然后再进行分析设计合理性,最后开始着手程序的实现。
2、面向对象思想:
三次作业带来的最主要的就是思想的转变,从面向过程到面向对象,这个转变很艰难,到现在也只是摸到点头绪,面向对象是一种思维方法。类、方法的构建与层次关系都是要不断思考不断改进的。
3、重构:
(1)抽象类的使用:
相对于接口我还是更喜欢抽象类,它避免了对属性的重复定义和部分方法的重复实现,同时同一了各个因子的方法参数,并且在使用时不需要区分具体是哪一个子类。
1 public abstract class Factor { 2 private BigInteger index; 3 4 Factor() { 5 this.index = BigInteger.ZERO; 6 } 7 8 Factor(BigInteger index) { 9 this.index = index; 10 } 11 12 public BigInteger getIndex() { 13 return index; 14 } 15 16 public abstract Factor clone(); 17 18 public abstract String toString(); 19 20 public abstract Factor[] diff(); 21 22 }
(2)方法的重写:
toString 、 clone 和 equals 这三个方法非常的实用,针对不同子类的重写可以帮助我们简化代码。同时,在需要 HashMap 处理 key 时,hashCode 方法的重写也是很必要的。
(3)架构改进:
我这次的树结构其实是有些复杂的,在浏览其他同学的优秀代码时发现很多非常精妙的设计,值得学习,比如划分成加组合项、乘组合项和嵌套组合项,这个思路老师课上有提到过,但是当时并不太会实现,在尝试过树结构和学习他人代码后,有了一些思路,这种方法更为简便,能极大降低复杂度,减少bug的出现概率和debug难度。
(4)工厂方法:
这其实是我之前的一个盲区,工厂方法能更加灵活地创建对象,帮助梳理结构,减少层次,这是需要我仔细学习的地方。
四、总结
第一次写博客,可能存在很多问题,请大家见谅。
面向对象的第一单元已经结束了,收获很多,压力也非常的大,尤其是最后一次作业。我在架构设计、代码风格和程序拓展性方面都有了很多的学习与思考,发现了自己的很多问题,仍然需要不断努力。