题记:刚工作那会给新来组里的同事写的技术文章,用了一些轻松的口吻,当时的效果很好。现在翻看起来,觉得当小说看还是可以的。具体内容其实参考了《重构》以及另一本也是重构的书。
说到重构,什么时候你的代码要重构呢?有这么一个测试方法:“如果你发现修改自己的代码感到很崩溃,如果你发现阅读自己的代码之前需要先查看注释,这就表明你的代码需要重构了”。
大家知道,大型软件项目的开发是一个长期的,团体协作开发的过程,对于软件中的某一个模块,某一个功能,甚至是某一段代码,都有几个甚至十几个人修改过,你可以想象你的代码被你自己或是别人改过了多少次,也许看看CVS的版本提交就能明白。但是,随着每一次细小的完善和代码的bug修改,一段时间以后,你的代码已经面目全非了,尤其是我们目前正处于项目的系统测试和需求变更阶段,需要频繁的对代码进行修改和维护,不管你愿不愿意接受,代码维护的特点就像在沙漠里行进:“前进一步,后退半步”。因为程序的维护基本上不同于硬件的维护,它主要由各种变更组成,如修复设计缺陷,新增功能,或者是使用环境或配置变换引起的调整,而这种调整对于一个广泛使用的程序,其维护成本受用户数目的严重影响,用户越多,所发现的错误也是越多,通常情况,维护成本至少是开发成本的40%。另一个不好的消息就是每次bug的修改总会以20%到50%不等的几率引入新的 bug,周而复始,当混乱达到不可控阶段的时候,软件也差不多报废了。
因此,我们应该如何延缓软件的报废周期,延长其寿命,如何正确有效的重构我们的代码以便让我们接下来的修改和维护更加轻松愉快而不是痛苦不堪,这需要时间和智慧,更需要勇气,下面的论述,也许可以帮你找回一些勇气,当然,重要的是你自己要动手去做。
看过很多同事编写的代码,我可以很肯定的说,大家的代码肯定有以下一种或几种问题(也许应该称为弊端),正是它们的存在,让你在修改重构甚至是自己的代码时都感觉到很痛苦,夸大一点的说,你甚至都不想或不愿去阅读你自己写的代码,更不用说别人的代码,因为你自己心里清楚,它们写的的确很烂,但是也许你没有注意,它们主要是烂在以下地方:
1.大量重复的代码:也许你已经发现,即使在一个CTRL或一个Proxy类里的某一个方法,都有大量重复的代码,让你不得不在修改某一处的时候提心吊胆的用查找替换来解决问题,更不用说在不同的包类里面进行相同代码的修改,这样会很崩溃的。其实重复代码一般表现在一下两个方面,一是两段代码看上去几乎相同(这个对于CTRL+C+V程序员应该很有体会),另一个是两段代码都是实现相同的功能。
2.过长的方法参数序列:有的人在新增方法的时候很喜欢将尽量多的参数传递到新增的方法中,这可能与我们在过去学编程的时候老师教我们的:“把方法所需要的所有东西都以参数的形式传递进去,方便省事”。就像我们的一些同事,在Proxy端的deal方法里面,总是先尽可能多的将Request里的变量 get出来,比如dwid,Wsh,Map,dabo,之类的,然后把他们统统作为参数传到业务方法里。其实这样做是不合理的,因为太长的参数列会是方法本身变的难以理解,造成方法前后的不一致,而且一旦你需要多加一个参数的时候,就必须得修改方法,这种仅为一个参数就修改方法是行为很崩溃。
3.代码量过多的方法体:我看过很多尤其是终审的方法,将近两百多行,鼠标的滚动条要滚好一会才能到方法末尾,更不用说一行一行的去阅读,去理解,可以想象别人来修改这段代码的时候他有多么痛苦,心里一定在想“TMD,怎么这么长”。当然有时候一个方法体开始的时候并不是那么长,只是随着每次需求的变更,每次一点点的从方法里面加代码而没有注意重构,侥幸的认为这次的一点点根本不算什么,但积少成多,最后终于不可收拾了。就像一个人,开始并没有那么胖,然后每次去肯德基或者麦当劳就侥幸的认为我就吃一个香辣鸡腿堡(本人也喜欢吃,哈哈),胖不了的,过不了多久,可能就变成一个死胖子了。哈哈。
4.过多的if-else和switch-case逻辑:if-else和switch-case恐怕是像我这样的菜鸟程序员最喜欢用的东东了,看看我们的代码,出现它们的地方有多少。
5.Null检查:也许是我们被NullPointException这样的异常吓怕了,所以每次我们都是虔诚的写上"if(null!=***)"或者 “if(null==***)",但是我们这样做其实是在逃避而不是勇敢的面对。
6.局部变量的”全局“化:哪怕只在方法的某个角落里出现的变量,我们也很大方的把它放到全局域里,甘愿默默的忍受有可能在别处该变量被修改的风险。
7.数据集合类:我们设置了一些数据集合类来放一些我们要经常用到的数据,这样的思想确实能很好的避免多处修改,但我们往往是直接读取变量而不是使用 get方法,这样就把过多的内容暴露给了客户,有时候太热情反而是一种坏事。
8.数据泥团:很多数据总是喜欢成群结队的聚集在一起,就像社会上的青年小混混,应该给他们限定住,否则肯定会出乱子的。
9.注释的问题:太多的注释和太少的注释都不好,多多益善,聊胜于无的想法都是不可取的,对于注释,我们应该遵循这样一条原则:“注释的使用不是为了说明这段代码能做什么而是应该说明为什么要这么做”。
10.死代码:看看自己的代码,集中注意那些带黄线的部分,数数有多少是从未读取的变量或从未使用过的方法,如果有,赶紧清除它们,不要为以后着想,以后的问题不是现在放几个方法和变量就能解决掉的。。。。。。。。。。。。。。。当然,还有很多拉,比如像类之间的继承混乱,对象之间的过分依赖,方法之间的链式调用,都是有可能会出现问题的地方,但是这些问题就目前我们安监项目的代码里表现的并不明显,或者说像我们这样的在框架内编写代码的菜鸟程序员没有机会出现的问题,所以就没有必要去论述了。
先解决目前我们面临的这些问题,一步一步,不要想一口就吃出一个胖子,但是如何解决,借用已走同事王芳的一句话:“瓜娃子,莫慌撒”。哈哈,下面针对以上提到的十种常见问题,给出一些实际的办法,用point to point的方式,各个击破。
首先对于重复代码的问题,一般有两种情况:一是在同一个类中的不同方法体里有相同的代码段,(比如每次我们insert之前都要先检测是否存在的方法),对于这种情况,我们要毫不客气的将这段代码提取出来,写一个新方法,让这个方法的名清晰的表明它的功能。另一种情况是不同类中都有相同功能的代码段,对于这种情况,我们要先把这段代码提取出来,能后考虑能不能放到父类里面去,如果不能,那就放在类似QyxxServiceImp的类中,通过对象去调用它。在提取方法的时候,注意所提取的方法里是否引用了原函数的变量,如果有将改变量当成参数,然后注意所提取的方法体里如有仅限于方法体使用的变量,将这些变量设为所提取变量的临时变量。
(2)对于过长的方法参数列,我们应该遵循这样一条原则:“如果方法可以通过其他方式而不是参数列获得参数值,那么它就不应该通过参数取得该值”。因此,如果某些参数值可以通过调用已知的一个方法得到,那么就要把这个参数退换为在方法体内调用取值的那个已知方法。如果一些参数来自一个对象,那么就不要单个的取出这些数然后当成参数传递了,之间将这个对象传过去,就像我们传递我们的request对象一样。如果这些参数确实不来自同一个对象,如果有必要,我们也要毫不犹豫的为这些参数新建一个不可变的类,强制将它们放在一起,这样做是绝对值得的。
(3)对于代码量过多的方法体,其可读性和内部数据的耦合性肯定会让你崩溃的,我们第一个想到的是有没有相似的代码或者有意义的代码块可以提取出来,如果有,赶紧提出来。不要担心过多的方法调用会影响到性能,只要我们的代码清晰,我们就能更深入的认识,从而对系统和算法进行结构优化。事实上,我们的代码提取应该遵循这样一个原则:“每当我们感觉需要写注释才能说明清楚的时候,我们就要把这段需要说明的东西写进一个独立的方法里,并以其用途而不是实现手法来命名它”。哪怕有时候是对一行代码进行这样的提取,因为关键不在于方法的长度,而是在于方法体做了什么。另外,过大的方法体必然导致方法体内有大量的参数和临时变量,其中某些变量可能是保存某一表达式的结果,对此,你应该将这个表达式提取到一个独立的函数中,然后在方法体内所有用到此变量的地方改为对提取函数的调用。如果有些变量实在不能提取出方法来,那么就把所有涉及到这些变量使用的代码甚至是整个方法都放到一个新建的Class里面去(类命名为这个待处理函数的功能),接着建立一个final值域用于保存原方法所在的对象,然后将这些变量都变成这个类的值域,建立一个构造函数,用于接收原方法所在对象以及原函数的所有参数。然后在新建的类里面建立一个方法实现原方法功能或原代码段功能。通过这种变换,就将所有局部变量都变成了在对象里调用对象的值域,意味着你可以为所欲为了。
(4)对于过多的if-else和switch-case逻辑,首先来说说if-else,也许代码一开始可能很简单,只是变更阶段为了省事加上了额外的条件判断,对于这种问题,我们的第一个念头是此处多态能不能取代条件式,如果能,使用多态,如果不能,应该重新考虑程序的逻辑结构了。另外一种情况是if 和else子句非常相似,那么考虑将起重写,使同样的代码段无论对于那种情况都能生成正确的结果,然后去除条件式。对于复杂if-else,有时候可以考虑能不能应用DeMorgan法则:"!(a&&b)=>(!a)||(!b),!(a||B)=>(!a)& &(!b)".然后我们来说说switch-case,我们在程序开发过程中应该这样告诉自己:“少用switch-case语句,尽量用多态去代替它们”。因为多态的好处是当你需要根据对象的不同型别而采取不同的行为时,你可以不必编写明显的条件式。同时很多时候swith-case总是在你的代码里多次重复出现的,如果此时你要为它新添一个分支case语句,你不得不在多处修改,带来不必要的重复。关键的问题是我们在哪里应用多态?如何应用多态?对于第一个问题的回答是对switch(type code)括号里的类型进行多态抽取。第二个问题的回答需要一些步骤才能说清楚,那么,我们就慢慢来看看。 (a)将switch-case代码段抽取成一个独立的方法。 (b)为每一个的case:(type code)里的type code建立一个class,在这个class里再建立一个值域用以记录这个type code 的值。 (c)修改switch-case里的代码,将原来的type-code改成class类的形式。 (d)为所有这些新建的class再建立一个共同抽象父类,为父类设置一个抽象方法,方法名即为原switch-case实现的功能,方法参数为原 switch-case所在的对象。 (e)让每个子class实现这个方法,方法体即使原case里对应的那一部分功能实现。 (f)将用switch-case抽取出的方法体退换成[class对象.方法名(原方法所在对象)]的形式。哈哈,写的很累,估计看的也很累啊,其实就是一个技巧,其实就是此处应用了两次多态技术,第一次应用多态通过新建class的兄弟关系取代switch- case中的case情况不同,第二次应用多态通过每一个新建的class实现switch-case的原功能的逻辑处理,当然是每个class只负责处理case为它的逻辑。出入原方法所在对象是考虑你的case情况中可能会调用原switch-case所以对对象的其他方法。强烈建议你动手这么操作一次,你会受益匪浅的,当然有什么问题可以随时email我啊,飞鸽也行。
(5)对于Null的检查,客观一点的说,Null的检查确实可以做到很好的绕开空指针,但是如果写了太多的话,也会让你崩溃吧。当然如果写的少的话,就直接跳到下一个问题吧。哈哈,这样是不是很爽。所以我的建议针对有大量空指针检查的,而且检查后的行为也是相似的情况。在这种前提下如果类似if (null==AAA)这样的条件有默认值,就统统都采用默认值吧。如果没有默认值,为AAA这样的class建立一个子类,其行为就想是父类的null 版本,其实主要是子类在null情况下一些行为的不同,其他直接继承得了,然后在子类和父类class中都加入isNull()方法,很明显,父类 class的isNull()方法返回false,子类的返回true。接着将程序里所有if(null==AAA)的情况替换成if (AAA.isNull()),试着找出这样的地方:如果AAA不是null,执行A动作,否则,执行B动作,在子类中重写B动作,用于Null情况下的操作。最后连那些烦人的if(AAA.isNull())也删掉吧,因为此时多态已经为你做了应对Null指针的情况了。哇,删的很爽吧。哈哈。
(6)对于局部变量的”全局“化,我们还是表现的吝啬一点吧,方法体内的就放在方法体内,代码段内的就放在代码段,毕竟出了问题我们的测试组小姐会提 bug的,还是扣我们的分哦。重要的是说不定还真会扣我们的奖金(补助)哦。
(7)对于数据集合类,最好的解决方案是不要让客户直接去读取这些类的值域,你需要封装起来,然后设置get/set接口,让客户通过这些接口去读取数据,不要太热情了吧,就想对待一个小M一样,太热情了反而会引起她们的厌恶,而适当的敬而远之往往能出其不意的收获芳心。哈哈,女人有时候比程序还难以琢磨啊。
(8)对于数据泥团,上面说他们就像一群社会上的小混混,很多时候我们修理其中某一个,别的数据就要发飙了,貌似挺有哥们义气的,不管了,一网打尽吧,把他们统统都抓进号子里吧,为这些数据安一个家,把他们提炼到一个独立的对象中去,你会发现受益匪浅的。
(9)对于注释的问题,如果你发现需要用注释来提醒你做了什么,那么把那些相关代码提取出来吧,再取一个能代表它功能的名字,一看就能明白了。如果你写的注释是一个前提条件,试试用断言Assertion,很明显用程序说话比用注释来的有说服力。所以,注释不要太多了,因为你不是小说家,你是程序员,还是用我们那个原则吧:“注释的使用不是为了说明这段代码能做什么而是应该说明为什么要这么做”。
(10)对于死代码,无非就是指那些从未在任何地方使用的变量,参数,字段,代码段,方法或者类,他们也许曾经做出过贡献,只是由于代码的修改或需求的变更变的不可用了,对于没用的东西,把它们移走吧,给年轻人腾出地方,没有什么可惜的,我知道它们是你写的,但是现在它们死了,就让他们入土为安吧。。。。。。。。。。。。。。。。
当然还有很多代码中的问题需要我们解决了,但是并不打算写下去了,因为我知道你们看的很累了,那就休息一下吧。用我们惯用的不良风格,等问题暴露的更明显一点的时候再来出招吧。
Dimmacro 戊子年六月末