在本学期开始以前,我按照助教们所给的寒假做业指导书自学了Java语言的相关知识,了解了Java语言的基本语法,输出一句“Hello World!”,掌握了基本的一些输入输出方法,也学习了一下如何使用正则表达式。比较顺利地完成了寒假做业的三个小程序。可是,作完以后我就一直在回想,我当时写的时候好像一直是在使用相似C语言的方法进行思考。好比,要对输入的字符串进行格式检查了,那就在main函数下面写一个检查格式的函数呗;要对正确的字符串进行处理了,那就在下面写一个处理字符串的函数呗。甚至,我以为将他们一古脑儿放在main函数中也是不错的选择,直观,便捷。那合着咱们这门课只是一门披着Java外衣的C语言课了?当时的我为了解决这些疑问,Google了许多关于面向对象编程的特色,查询了不少其与面向过程编程的不一样之处,感受也只是浅尝辄止,听他们说好像是对的,可是本身叙述就不明就里。正则表达式
现在,通过了一个月的理论学习+三次课下做业,我对于面向对象编程的思想有了进一步的理解,虽然感受理解得还不是很到位,可是起码可以感觉到与面向过程之间的巨大差异了。下面我将按部就班陈述个人学习过程,聊一聊我每一阶段的心得体会。编程
首先贴出UML类图和代码量化分析图。小程序
首先让我简单介绍一下代码量化分析图中的三个参数分别表明什么含义。设计模式
ev(G)表明的是基本复杂度,主要用来衡量程序的非结构化程度。显然,非结构化程度越高,意味着这一段代码的维护性越差,越松散,给人的阅读体验也越差。架构
iv(G)表明的是模块设计复杂度,主要用来衡量模块与模块之间的耦合程度。若是一个模块的iv(G)越高 ,那么就意味着它越须要其余的模块,与其余模块的相关程度也越强,维护难度越高,隔离难度也越大。函数
v(G)表明的是模块结构的复杂度,主要用来衡量模块内部的逻辑究竟有多复杂。通常来讲,结构越复杂的模块,带给维护人员和测试人员的工做量也越大,产生错误或者遗漏的可能性也越大。学习
能够见到,在第一次做业中,我仅仅设置了两个类,MultinomialDifferentiate类(下文称M类)是main函数所在的类,主要行使的功能就是得到输出,而后传递给Differentiate类(下文称D类)进行格式检查以及求导计算。对于一个输入进来的字符串,处理过程大体是这样的:M类得到一行字符串输入,调用D类中的Validation函数,判断格式是否正确;若格式正确,那么调用D类中的decompose函数对字符串进行肢解,从而获得幂函数的系数以及指数,将他们分别存到D类中的coef和exp两个Arraylist中;最后M类调用D类中的calculate函数进行计算,再经过output函数进行输出。outJudge函数是用于化简输出字符串使其为最短形式。在我看来,肢解过程,求导过程和简化输出过程都比较地简单,也没有太多的技巧可言,故在此再也不赘述。我想好好介绍一下的是我所使用的格式判断方法。测试
我身边的很多同窗都使用了大正则的方法,虽然此次的合法表达式的的确确可使用一个正则表达式彻底地描述,可是因为正则表达式的特性,会致使若是输入过长则爆栈的现象。因此我使用的方法是先将空格彻底清除,而后一小段一小段匹配的方法,简单来讲就是若是匹配到正确的一小段,那么就将其替换成空字符串,这样循环下去,若是最终的字符串是一个空串,那么该字符串便合法。固然,我须要在匹配以前进行一些特判,好比所有是空格的状况。同时还须要注意匹配的顺序,应该先匹配开头的幂函数,而后匹配其余的幂函数,最后匹配纯整数,由于不一样的顺序会致使误匹配的状况发生,因此应该按照格式复杂度由高到低进行匹配。关键部分代码以下:优化
1 Pattern error1 = Pattern.compile("\\d[ \\t]+\\d"); 2 Pattern error2 = Pattern.compile("[-+^][ \\t]*[-+][ \\t]+[\\d]"); 3 Pattern error3 = Pattern.compile("([-+][ \\t]*){3,}"); 4 5 if (error1.matcher(str).find() || error2.matcher(str).find() || 6 error3.matcher(str).find()) { 7 return false; 8 } 9 10 if (Pattern.matches("^[ \\t]+$", str)) { 11 return false; 12 } 13 14 String strVal = str; 15 16 strVal = strVal.replaceAll("[ \\t]", ""); 17 18 String start = "^([-+]{0,2}((\\d+\\*)?x(\\^[-+]?\\d+)?|\\d+))"; 19 String end = "[-+]{1,2}(\\d+\\*)?x(\\^[-+]?\\d+)?"; 20 String pureNumber = "[-+]{1,2}\\d+"; 21 22 strVal = strVal.replaceAll(start, ""); 23 strVal = strVal.replaceAll(end, ""); 24 strVal = strVal.replaceAll(pureNumber, ""); 25 26 return strVal.equals("");
由个人代码量化图中能够看到,D类中的outJudge函数三项复杂度都比较高,这主要是D类的output函数直接调用了其的缘故,就像C语言同样。这样的设计显然不够好,调试起来显然也并不容易。spa
同理,先贴出UML类图和代码量化分析图。
在第二次做业中,输入字符串在第一次做业的基础上加上了三角函数sin和cos的输入,其实从整体架构上来讲,复用第一次做业的结构是彻底没有问题的。可是能够看到,我在第二次做业中,根据老师的建议加入了InputHandler类(下称I类)的输入处理模块,同时设置了一个Expression类(下称E类)和一个Term类(下称T类)。这个程序的执行程序大体是这样的:先由I类接收输入的字符串并做简单的格式判断,而后由main函数调用E类中的checkSyntex方法,检验在表达式层面可能出现的格式错误,好比空格的错误出现,好比非法字符的出现等等,这一阶段还不涉及项内的格式错误;接着E类会删除空格,根据加减运算符分割出一个一个的项(每个项中的可能出现的正负号已经被其余字符所替代),而后调用T类中的checkSyntex方法,检验项层面可能出现的格式错误,若无错误,则将其放入一个以Term为元素的Arraylist当中。最后,调用E类中的differentiate方法便可完成求导工做。在互测阶段观摩了其余同窗的代码以后,我发现许多人都是在最一开始就使用一个大正则判断整个表达式是否符合格式。这样的判断当然没有错,可是一个须要占据三四行的大正则的可读性极差,而且我以为也不太符合面向对象的思想。拿个人代码架构举例子,若是我在E类中就将格式审查工做作完,那么至关于T类是一个附属于E类的类,由于T类会默认获得的数据都是格式正确的,可是这显然很不利于模块的隔离与复用,因此在E类和T类中分别判断本身所负责的格式是否有错,那么这两个模块均可以单独拎出来与其余的模块共同工做,显然复用性就更高了。至于第二次做业的优化问题,我只完成了sin(x)^2+cos(x)^2=1的初级化简,因此在此就不献丑了。
因为个人代码结构是层层递进式的,可是每个模块之间我让他们尽可能保持独立性,因此能够看到这一次平均的ev(G),iv(G),v(G)值都降低了,证实代码的质量有了一些进步。可是checkSyntex函数和outJudge函数仍旧作的不够好,由于感受这两个模块对其余模块的依赖很是地大。
同理,贴出UML类图和代码量化分析图。
平心而论,第三次做业的难度和第1、第二次做业的难度不可同日而语,起码从架构设计上来不那么简单了。首先观察个人UML类图,能够明显地看到整个架构是一个层层递进的架构,而我使用的也是较为常见的递归降低法。接下来我具体介绍一下整个程序的架构:Main类是main函数所在的类,InputHandler类用于处理控制台的输入,而后与表达式处理相关的有Expression类(下称E类),Term类(下称T类),Factor类(下称F类),TriangleFunction类(下称Tri类),PowerFunction类(下称Pow类),Constant类(下称Con类),AdditionDIff类(下称Ad类),MultiplyDiff类(下称Mu类)和NestingDiff类(下称Ne类)。从逻辑层次上来分,此次输入的字符串能够分为表达式,项和因子三个层次,因此我设计了三个类来分别对应这三个层次。同时,咱们仔细探究求导过程,能够发现,最基本的单元都是因子的求导,因此在F类下面,我又设计了三个处理不一样类型因子的类,真正的求导是在这三个类里面进行计算的。你可能发现了,在这三个类里面并无表达式因子的处理方法,那是由于我认为表达式因子并不可以算做真正的最基本的元素,因此我会将表达式因子直接放入嵌套方法中进行计算,但如今想起来,也许多设计一个表达式因子类会使得整个结构更加地完整,易于理解。回到正题,那么每一个因子的求导结果是如何联合在一块儿的呢?我设计了三个运算规则类,即Ad类,Mu类和Ne类。通过个人理解,我认为Ad类实际上是专门为E类所服务的,也就是E类将表达式拆分出一个一个地项,而后将这些项传递给Ad类,Ad类再分别调用每一个项自身的求导方法(在T类中),将返回的字符串经过加减号拼在一块儿便可。Mu类的功能相似,调用每个因子的求导方法,而后将他们返回的结果按照乘法的求导规则拼在一块儿便可。Ne类比较不同,我理解为专门处理表达式因子的,即Ne类中会再次创立一个新的E类对象,再通过前述的一连串过程得到最终的结果。能够发现,这样的设计模式使得一种运算规则与一个层次绑定,让结构变得更加地清晰,调试起来也更容易发现问题,定位问题,解决问题。
因为我使用了较多的类(共11个类),因此整个架构清晰了许多,从代码量化数据也能够看出,三项复杂度比上两次做业有了明显的减小,达到了较为理想的水平,我很欣慰我在这过程当中一直在进步,收获颇丰。
首先,最大的体会固然是这门课主要讲授的内容,面向对象编程思想。什么是对象?对象是一个客体,具备特定属性,可以完成特定工做。对象与C语言中的函数有什么区别?两者都可以完成特定的功能,可是对象更加立体,更加符合通常人的思惟方式。举个例子,对象是一个具备独立思考能力和独立行动能力的健全人,而C语言中的函数则是一个根据指令完成任务的机械手臂,两者均可以完成他们能力范围内的特定的工做,可是他们能同样吗?对于机械手臂而言,咱们须要给予它数据,告诉它它须要完成什么工做,以及一步一步究竟怎么完成都须要预先设定好,这显然是一种流水线的工做方式,可以提升工做效率,可是没有变革工做方式。而对于一个健全人而言,咱们须要给予他数据,再告诉他他须要完成的任务,以及最终返回的结果是什么样子的就能够了,至于他是怎么完成的,咱们彻底不须要关心。更深刻地来讲,以C语言为表明的面向过程式的语言是按照任务完成的前后顺序来进行结构设计的,而以Java为表明的面向对象式的语言则是以数据的传递路径来进行结构设计的。数据即信息,两个模块之间只须要按照规定传递正确的数据,整个任务就能够正确地完成。这让开发大型工程更加地轻松,要修改某些部分或者添加一些功能不须要改动其余部分,只须要增长一个模块进行数据处理便可。由此看来,大型工程的开发难度降低了,向下兼容性变好了,修改完善变简单了,测试也变得更加舒服了,这就是我对于OOP的一些小小我的体会。
其次,我又一次感觉到了“拆分”思想的强大力量,这主要是在第三次做业中得到的。乍一看第三次做业指导书,内容繁复,条条框框不少,各类格式的可能性也很是地多,看起来根本无从下手。可是指导书也是给出了明确的逻辑分层,当我顺着指导书的逻辑往下走,我发现其实质上就是表达式,项和因子三个层次组成的,每个层次中都有其自身特有的正确格式,每一种可以想到的格式错误也均可以明确地归为三个层次中的一个,由此,我只须要将三个层次的界限划清楚,在每个层次中按照指导书要求实现对应的格式判断就彻底解决了。求导规则的拆分也是一个很优秀的思想,从繁复的求导可能性中提取出了最为本质的规律,而后任何字符串求导均可以轻易地归于这些求导规则,那么咱们在实现的时候只须要完成每个小模块的小功能便可。在设计好以后,不到一天时间,我就完彻底全地将整个程序构建完毕并经过了课下测试,不得不感叹“大事化小小事化了”思想的强大力量。
最后,是以测试为导向的设计思想的理解与应用。这一设计思想是第一次研讨课中彭毛小民同窗提出的(在此感谢彭毛小民同窗的启发),我听完后大受启发。在设计的初期,也就是阅读指导书的阶段,咱们就要开始设计本身的测试用例了,这样作有许多的好处。第一,咱们在设计测试用例的时候实际上是一个加深对于指导书内涵的理解过程,碰到一个模棱两可的例子,咱们就会去从新阅读指导书,而后强化对于规则的理解,慢慢地,咱们对于指导书的规则便了然于胸,必定程度上避免了设计架构的时候常常忽略指导书的一些要求而形成后期的反复修改;第二,研究测试用例时咱们就会不断斟酌咱们脑子里那个设计架构,不断地优化它,好比一些方法究竟该放在哪一个类中更好,数据的流向如何设置更为合理等等;第三,这样设计出来的测试用例更加地完整,涉及到了方方面面,从简单的错误到复杂的错误所有涵盖,后期进行互测时就能够进行较好的全面覆盖。我在第二第三次做业中都应用了这种设计理念,效果很是地喜人,在写代码阶段更加地流畅,写出来的代码结构更加地清晰,在互测阶段被他人轻易找到bug的概率也大大降低了。
侥幸地说,在这三次做业中,我总共只被其余人找到了一个bug,这个bug是在第三次做业中被找到的。简单来讲就是cos函数的正则表达式匹配中,我在指数部分忘记了整数前面能够带符号这一特性,使得判断出错,进一步致使个人求导结果出错。虽然最后我经过一个合并修复就将这个bug修复完成,可是仔细想一想,这个bug是一个很是严重的bug,由于我连正确的功能都没有可以彻底实现!在自测的时候我其实发现了相似的问题,可是当时是在sin函数中发现的,因此我就改了sin函数的正则表达式判断,原本想着连cos也要一块儿改的,可是可能由于什么其余事情而最终忘记了。这个经验告诉我,即便咱们有了比较完备的自测数据,在修改bug的时候必定要有头有尾有始有终,咱们保不齐在何时就会走神,就会有疏忽的地方,因此必定要对本身的程序抱有必定程度的怀疑,也永远不要骄傲地认为本身的程序就必定没有bug了。
再来讲说我找到的别人的bug,第一第二次做业我找的bug主要都是格式方面的bug,我使用的测试方法是黑盒测试加上读代码的有关格式判断的一部分,这样的测试方法仍是比较高效的,首先利用本身的测试数据来测试他人的代码,而后再一个一个代码地阅读有关格式判断方面的错误,对于第二次做业还须要阅读一下有关优化方法的一些错误。可是对于第三次做业,关注得更多的就是逻辑方面可能产生的错误,因为代码量过于巨大,因此我没有阅读他们的代码,而是尽可能设置一些特殊结果的数据,好比最终结果是0的,或者将全部的因子都用括号括起来的这种,来探查他们是否在化简的时候或者在输出的时候没有考虑彻底。我以为须要提一下的就是若是使用string.split函数进行项的分割的话,须要注意最后若是是分割符的话,那么split函数是不会在最后造成一个空字符串的元素的,因此须要咱们手动判断最后是不是单独的分割符,好比最后是一个单独的乘号这种状况。其余的bug很大部分是在优化部分出的错,由于一些没有考虑全的缘由,输出了空字符串或者重复输出了加减号运算符这种,这也是我在本身编写代码的时候须要注意的地方。
我最大的遗憾就是第三次做业没有可以找到很好的优化策略,作出满意的优化效果。因为分数导向问题,我把更多的时间放在了如何使架构更加合理,减小bug出现的可能性上,而对于优化输出所作的工做确实不够多。也许是个人架构不够合理,因此优化的难度会很大,这也不得而知。只但愿在第二单元的做业中我可以让个人程序真正变成对用户友好的产品,让用的人不至于这么的难受。
其次就是代码复用性太低的问题。虽然说三次做业下来,个人代码模块性更好了,可是三次做业我重构了三次,究其缘由仍是前两次做业的设计不够成熟,可拓展性太差了,同时每一个模块之间的耦合度过高,因此难以复用。但愿在第二单元的做业中我可以从第一次做业开始就将可扩展性放在一个比较重要的位置上。具体的重构方法见第一部分的三次做业简介。