一、程序结构分析
作为总结,首先来看一下本人从HW1到HW3的UML图:
HW1:
由于HW1任务很简单,处理时本人相当于沿用面向过程的思想实现,将操作切割成输入处理、计算、输出三个类来实现,每个类之间也没有直接联系。由于没有迭代开发的经验,故成功地为HW2的代码重构作下铺垫。
HW2:
在HW2中,本人重新设计了自己的整个代码结构,核心思想仍是模块化思想,将题目分三个步骤解决。在此基础上,用工厂模式建立一个Function接口,而题目所求的几类因子均要实现这一接口。Parse类解析字符串并创建不同的Function,而Poly和Print类则调用Function的方法进行进一步处理。整体来说本人HW2的结构较为清晰,也为后面HW3的实现打下了基本框架,具有一定的可扩展性。
HW3:
对于HW3的结构,本人只是在HW2的基础上做了一些类的细化和增添,整体上并未有大的改动。我将异常处理单独创建成一个类,并只在分析输入字符串的时候调用,避免由于反复throw错误而爆bug,降低了一定的耦合度。在优化输出时,新建了一个Item类,以Item为单位优化输出,解决了原来HW2的输出类冗杂情况。但是,可以发现,本人对于Poly类和Function接口之间关系的处理是有明显问题的。Poly实现了Function接口,却又反复调用Fuction,这无疑使代码的耦合度和复杂度大大提升。
由于本次是迭代开发作业,下面我将重心放在HW3,基于DesignateJava这一插件对HW3代码度量的有关数据,对代码结构进行进一步详细分析。
我们先来看关于类的相关度量:
LOC(代码行数):从这一数据可以看出,本人对类的划分有待进一步优化,例如有331行之长的Item类可能和其它类相比比较冗余,存在着进一步划分的可能。
DIT(类所在的继承树深度):本人此次并未采用继承方法来进行工厂模式的创建,故所有类的DIT均为0,有些扩展DIT的可能性请见本文第四点内容。
LCOM(方法的内聚缺乏度):从表中可以看出,本人设计的Parse和Signint两个类的内聚度不够,其原因可能是对外部暴露了过多的接口,使得两个类对外部的依赖性过高,是不好的结构。
FANIN(类的扇入)、FANOUT(类的扇出):类的扇入扇出分别表示调用该类的上级模块数和该类调用的下级模块数,从这一数据我们可以窥见代码结构上的严重缺陷:Trig类作为一个实现Function接口的逻辑上最底层的类,却又调用了整个工程最多的下级模块,这就造成了类之间的循环依赖,是高耦合的体现。
接下来我们进一步深入,根据DesignateJava发现的Implementation Smell,从方法层面具体分析一下代码结构的缺陷。
根据表格,可以看出HW3中的方法最大的问题就是代码复杂度过高,很高的圈复杂度导致了代码性能的低下。除此之外,Poly的derive方法中包含了魔数,会使代码的可读性下降。
以上,本人从类与类之间,到类的内部,再到方法内部逐层分析了Unit1作业的代码结构。总体来说,本人的代码的整体框架还是比较好的,只是由于本人未能充分规划类的划分以及各类之间的联系,导致了代码的高耦合性和某些方法的高复杂度。
二、程序Bug分析
本人的Unit1之旅是一次惨烈的造bug之旅,HW1的风平浪静之后,我迎来了HW2和HW3的狂风骤雨。下面本人将来“回味”在HW2和HW3中创造的那些bug。
HW2:
Bug1:在运行过程中计算出指数超出范围的情况时,仍输出WRONG FORMAT。
错误原因:本人未能处理好抛出异常的时机,使得抛出异常与Parse类的一个方法绑定在一起,这样在求导过程中,调用该方法时若指数超出范围即会报错。其实从根本上看,这是本人代码的高耦合性导致的必然结果。
Bug2:在碰到可优化的三角函数时,求导结果出错。
错误原因:巧妙地将一个方法中的sin写成了cos。虽然只是笔误,但仍可以体现出本人方法的复杂度过高,过于冗杂,这样在过程中很难保证代码书写的正确。
HW3:
Bug1:算法bug,因为算法复杂度过高,导致超出了运行时间限制和运行内存限制。
错误原因:正如前文所述,HW3中本人存在类之间循环依赖的问题,以及一些方法的很高的圈复杂度。究其根本,还是本人在设计层面未能做好相应工作,没有提前确定好每个类的具体功能、方法以及类与类之间的联系。写代码时只是随心所欲地不断添加当前需要的方法,这是一种很差的代码习惯,也希望大家引以为戒。
Bug2:解析字符串时错误,导致括号内时无法解析“-”号,求导结果出错。
错误原因:在提取项前符号时一味固执地采用正则,导致有些情况不能处理而出错。其实回想起来,HW3中每个项前最多有三个符号,像这种情况完全不需要用正则捕获组这种“杀鸡用牛刀”的处理方式,直接循环提取字符处理反而显得更细致,复杂度也不高。因此,这个bug告诉我们,写代码时不能思维僵化,应根据不同情况采取不同的算法,让算法适应情况,而不是认准一个算法,强行让情况适应算法。
三、Hack策略
很惭愧,本人在hack方面实在没什么经验,每次在被人家无情地屠宰时,自己只能hack出两三个bug。我在第一单元的hack策略是完全可以当作反面教材的:不去分析他人代码的具体内容,只是构造一些自己能想到的测试点和得到的他人构想的测试点进行测试。细想起来,这种测试的覆盖度和复杂度都是很难达到要求的,故希望大家继续引以为戒。
不过,通过参考讨论区一些dalao的经验分享,我想在这里提出自己关于全方位测试的一个假想:
第一步,构建自动评测机,和他人代码的输出结果进行对拍。自动评测可谓是一件大杀器,往往很多代码(比如本人的)在经过评测机一顿猛测之后就会暴露出成堆的bug。第二步,构建全覆盖的小测试样例。自动评测机固然是很猛的,但其缺点也很明显,构造的数据过于随机化而往往难以生成边界数据。此时,就应通过人工构造测试数据的方法去测试,人工构造的数据,不应追求复杂度,而应追求覆盖度,通过一些小的边界性样例来对前一步的测评查缺补漏。第三步,姑且称之为“肉眼观察法”。理论上,仅仅通过观察代码,应该就是能发现绝大多数bug的,但很多同学(没错又比如本人)可能觉得自身对他人的代码有一定的阅读障碍,故往往只是“盲狙”。我想,在观察他人代码时,抱着一种比较学习的心态而不是一味就错的心态,是不是可能会更好呢?这点我不得而知,毕竟观察别人的代码的工程量是巨大的,而且我们不可能将所有时间都分配在OO互测上,所以这第三步,可能就看每位同学的心情了。
四、对于工厂模式的应用可能性分析
对象创建模式是有很多种类的,但由于我们课程在Unit1中重点学习了工厂模式,故接下来以工厂模式为例来分析对象创建模式的应用可能性。
1、以求导方法为接口的工厂方法模式
本单元的核心任务就是面对复杂度逐渐提升的多项式的求导,故以求导为接口是不难想到的,这也是课程组的推荐代码结构。在求导的接口(姑且命名为Function)之下,让三角函数,幂函数,常数函数等具体函数去实现这个接口,后续若有更多类型的函数也可以直接去实现这个接口,放入工厂之中。这样我们在创建时可以只用一个Function去引用不同的实例化对象,求导时也可以忽略函数的具体类型而只去引用Function的求导方法,十分方便。
2、以优化方法为接口的工厂方法模式
本单元中,对求导输出结果的优化也是一个工作量很大的任务。在本人实现优化时,将所有优化方法都放入了Output类中,导致类的冗杂。其实,对于优化也可以建立一个工厂,对于每一类优化情况,均建立一个相应的类去实现优化方法这一接口。这样一来,当我们求导完成后,只需将结果抛入工厂中,就可以生成相应的优化结果,而不用去具体分析结果内容,可以大大减少模块间的耦合度以及类的冗杂度。
3、以平凡的多项式项为父类的抽象工厂模式
通过观察课程组提供的HW3参考代码,我有了一些启发。既然本单元处理的对象是多项式,那把多项式的项作为一个抽象类,围绕其建立工厂,不失为一种直接而又清晰的代码结构。对于项抽象类,提前定好如指数、类型等通用的属性,以及求导和toString()等公共的方法。这样,我们只需要抽象化地对一般的多项式进行我们的操作,整个代码结构也显得层次分明。
五、代码对比和心得体会
俗话说“没有对比就没有伤害”,通过将自己的小破码与优秀代码进行对比,本人确确实实地发现了自身的不足。前文中有反复提到过,这种差距主要体现在代码结构方面,而又来源于设计阶段的考虑不周。现在想来,可能是我的设计思路还没有完全转换到面向对象思想上来,还未完全掌握面向对象的设计方法。记得高老板曾经说过,写代码前的设计阶段是整个工程最重要的。通过Unit1的经历,我对这句话有了更深的认识。在接下来的单元中,我将会吸取教训,将更多的精力分配在设计代码结构阶段,并通过不断地学习,真正做到面向对象设计与构造。
以上就是本人Unit1的心得总结,再次希望大家能吸取我的经验教训,一起进步。如果对这篇文章有什么想法,抑或是文章中有什么错误之处,也欢迎同学们找我讨论。