[关闭]
@pockry 2017-06-22T23:10:34.000000Z 字数 13539 阅读 2863

整洁代码之道——重构

移动


写在前面

现在的软件系统开发难度主要在于其复杂度和规模,客户需求也不再像Winston Royce瀑布模型期望那样在系统编码前完成所有的设计满足用户软件需求。在这个信息爆炸技术日新月异的时代,需求总是在不断的变化,随之在2001年业界17位大牛聚集在美国犹他州的滑雪胜地雪鸟(Snowbird)雪场,提出了“Agile”(敏捷)软件开发价值观,并在他们的努力推动下,开始在业界流行起来。在《代码整洁之道》一书中提出:一种软件质量,可持续开发不仅在于项目架构设计,还与代码质量密切相关,代码的整洁度和质量成正比,一份整洁的代码在质量上是可靠的,为团队开发,后期维护,重构奠定了良好的基础。

接下来笔者将结合自己之前的重构实践经验,来探讨平时实际开发过程中我们注重代码优化实践细节之道,而不是站在纯空洞的理论来谈论代码整洁之道。

在具体探讨如何进行代码优化之前,我们首先需要去探讨和明确下何谓是“代码的坏味道”,何谓是“整洁优秀代码”。因为一切优化的根源都是来自于我们平时开发过程中而且是开发人员自己产生的“代码坏味道”。

代码的坏味道

“如果尿布臭了,就换掉它。”-语出Beck奶奶,论抚养小孩的哲学。同样,代码如果有坏味道了,那么我们就需要去重构它使其成为优秀的整洁代码。

谈论到何谓代码的坏味道,重复代码(Duplicated Code)首当其冲。重复在软件系统是万恶的,我们熟悉的分离关注点,面向对象设计原则等都是为了减少重复提高重用,Don’t repeat yourself(DRY)。关于DRY原则,我们在平时开发过程中必须要严格遵守。

其次还有其他坏味道:过长函数(Long Method)、过大的类(Large Class)、过长参数列表(Long Parameter List)、冗余类(Lazy Class)、冗余函数(Lazy Function)无用函数参数(Unused Function Parameter)、函数圈复杂度超过10(The Complexity is over 10)、依恋情结(Feature Envy)、Switch过多使用(Switch Abuse)、过度扩展设计(Over-extend design)、不可读或者可读性差的变量名和函数名(unread variable or function name)、异曲同工类(Alternative Classes with Different Interfaces)、过度耦合的消息链(Message Chains)、令人迷惑的临时字段(Temporary Field)、过多注释(Too Many Comments)等坏味道。

整洁代码

什么是整洁代码?不同的人会站在不同的角度阐述不同的说法。而我最喜欢的是Grady Booch(《面向对象分析与设计》作者)阐述:

“整洁的代码简单直接。整洁的代码如同优美的散文。整洁的代码从不隐藏设计者的意图,充满了干净利落的抽象和直截了当的控制语句。”

整洁的代码就是一种简约(简单而不过于太简单)的设计,阅读代码的人能很清晰的明白这里在干什么,而不是隐涩难懂,整洁的代码读起来让人感觉到就像阅读散文-艺术的沉淀,作者是精心在意缔造出来。

整洁代码是相对于代码坏味道的,如何将坏味道代码优化成整洁代码,正是笔者本文所探讨的重点内容:整洁代码之道-重构,接下来笔者将从几个角度重点描述如何对软件进行有效有技巧的重构。

重构 — Why

在软件开发过程中往往开发者不经意间就能产生代码的坏味道,特别是团队人员水平参差不齐每个人的经验和技术能力不同的情况下更容易产生不同阶段的代码坏味道。并且随着需求的迭代和时间推移,代码的坏味道越来越严重,甚至影响到团队的开发效率,那么遇到这个问题该如何去解决。

在软件开发Coding之前我们不可能事先了解所有的需求,软件设计肯定会有考虑不周到不全面的地方,而且随着项目需求的Change,很有可能原来的代码设计结构已经不能满足当前需求。

更何况,我们很少有机会从头到尾参与并且最终完成一个项目,基本上都是接手别人的代码,即使这个项目是从头参与的,也有可能接手团队其他成员的代码。我们都有过这样的类似的抱怨经历,看到别人的代码时感觉就像垃圾一样特别差劲,有一种强烈的完全想重写的冲动,但一定要压制住这种冲动,你完全重写,可能比原来的好一点,但浪费时间不说,还有可能引入原来不存在的Bug,而且,你不一定比原来设计得好,也许原来的设计考虑到了一些你没考虑到的分支或者异常情况。

我们写的代码,终有一天也会被别人接手,很可能到时别人会有和我们现在一样的冲动,所以开发者在看别人代码时候,要怀着一颗学习和敬畏之心,去发现别人的代码之美,在这个过程中挑出写的比较好的优秀代码,吸取精华,去其糟粕,在这个基础上,我们再去谈重构,那么你的重构会是一个好的开端。

总之,我们要做的是重构不是重写,要先从小范围的局部重构开始,然后逐步扩展到整个模块。

重构 — 作用

重构,绝对是软件开发写程序过程中最重要的事之一。那么什么是重构,如何解释重构。名词:对软件内部结构的一种调整,目的是在不改变软件可观察行为的前提下,提高其可理解性,降低其修改成本。动词:使用一系列重构手法,在不改变软件可观察行为的前提下,调整其结构。

重构不只可以改善既有的设计结构,还可以帮助我们理解原来很难理解的流程。比如一个复杂的条件表达式,我们可能需要很久才能看明白这个表达式的作用,还可能看了好久终于看明白了,过了没多长时间又忘了,现在还要从头看,如果我们把这个表达式运用Extract Method抽象出来,并起一个易于理解的名字,如果函数名字起得好,下次当我们再看到这段代码时,不用看逻辑我们就知道这个函数是做什么的。

如果对这个函数内所有难于理解的地方我们做了适当的重构,把每个细小的逻辑抽象成一个小函数并起一个容易理解的名字,当我们看代码时就有可能像看注释一样,不用再像以前一样通过看代码的实现来猜测这段代码到底是做什么的,我一直坚持和秉持这个观点:好的代码胜过注释,毕竟注释还是有可能更新不及时的,不及时最新的注释容易更其他人带来更多的理解上的困惑。

此外重构可以使我们增加对代码和业务逻辑功能的理解,从而帮助我们找到Bug;重构可以帮助我们提高编程速度,即重构改善了程序结构设计,并且因为重构的可扩展性使添加新功能变得更快更容易。

重构 — 时机

理解了重构的意义和作用,那么我们何时开始重构呢?笔者一直坚持这种观点:重构是一个持续的系统性的工程,它是贯穿于整个软件开发过程中,我们无需专门的挑出时间进行重构,重构应该随时随地的进行,即遵循三次法则:事不过三,三则重构。这个准则表达的意思是:第一次去实现一个功能尽管去做,但是第二次做类似的功能设计时会产生反感,但是还是会去做,第三次还是实现类似的功能做同样的事情,那你就应该去重构。三次准则比较抽象,那么对应到我们具体的软件开发流程中,一般可以在这三个时机去进行:

(1) 当添加新功能时如果不是特别容易,可以通过重构使添加特性和新功能变得更容易。在添加新功能的时候,我们就先清理这个功能所需要的代码。花一点时间,用滴水穿石的方法逐渐清理代码,随着时间的推移,我们的代码就会越来越干净,开发速度也会越来越快。

(2) 修改Bug的时候去重构,比如你在查找定位Bug的过程中,发现以前自己的代码或者别人的代码因为设计缺陷比如可扩展性、健壮性比较差造成的,那么此时就是一个比较好的重构时机。可能这个时候很多同学就有疑问了,认为我开发要赶进度,没有时间去重构,或者认为我打过补丁把Bug解决不就行了,不需要去重构。根据笔者之前多年的经验得出的结论:遇到即要解决即那就是每遇到一个问题,就马上解决它,而不是选择绕过它。完善当前正在使用的代码,那些还没有遇到的问题,就先不要理它。在当前前进的道路上,清除所有障碍,以后你肯定还会再一次走这条路,下次来到这里的时候你会发现路上不再有障碍。

软件开发就是这样。或许解决这个问题需要你多花一点时间。但是从长远来看,它会帮你节省下更多的时间。也就是重构是个循序渐进的过程,经过一段时间之后,你会发现之前所有的技术债务会逐步都不见了,所有的坑相继都被填平了。这种循序渐进的代码重构的好处开始显现,编程的速度明显会加快。

(3)Code Review时去重构,很多公司研发团队都会有定期的Code Review,这种活动的好处多多,比如有助于在开发团队中传播知识进行技术分享,有助于让较有经验的开发者把知识传递给欠缺经验的人,并帮助更多的人对软件的其他业务模块更加熟悉从而实现跨模块的迭代开发。Code Review可以让更多的人有机会对自己提出更多优秀好的建议。同时重构可以帮助审查别人的代码,因为在重构前,你需要先阅读代码得到一定程度的理解和熟悉,从而提出一些建议和好的idea,并考虑是否可以通过重构快速实现自己的好想法,最终通过重构实践你会得到更多的成就感满足感。为了使审查代码的工作变得高效有作用,据我以前的经验,我建议一个审查者和一个原作者进行合作,审查者提出修改建议,然后两人共同判断这些修改是否能够通过重构轻松实现,如果修改成本比较低,就在Review的过程中一起着手修改。

如果是比较大型比较复杂的设计复查审核工作,建议原作者使用UML类序列图、时间序列图、流程图去向审查者展现设计的具体实现细节,在整个Code Review中,审查者可以提出自己的建议或者修改意见。在这种情景下,审查者一般由团队里面比较资深的工程师、架构师、技术专家等成员组成。

关于Code Review的形式,还可以采取极限编程中的“结对编程”形式。这种形式可以采取两个人位置坐在一起去审查代码,可以采取两个平台比如IOS 和android 的开发人员一起去审查,或者经验资深的和经验不资深的人员一起搭配去审查。

重构的这三个时机要把握好原则,即什么时候不应该重构,比如有时候既有代码实现太混乱啦,重构它还不如重新写一个来得简;此外,如果你的项目已经进入了尾期,此时也应该避免重构,这时机应该尽可能以保持软件的稳定性为主。

理解了重构是做什么,重构的作用,为什么要重构,以及重构的时机,我们对重构有了初步认识,接下来笔者重点篇幅来讲解如何使用重构技巧去优化代码质量达成Clean Code .

重构技巧 — 函数重构

重构的源头一切从重构函数开始,掌握函数重构技巧是重构过程中很关键的一步,接下来我们来探讨下函数重构有那些实用技巧。

重构技巧 — 条件表达式

重构技巧 — 案例

前面这多章节内容主要都是理论内容,接下来我们来看看具体的重构案例。

Map去除if条件表达式

关于该技巧的实现方法,上章节有讲述,我们直接看代码案例如下代码所示:

原始的条件表达式代码如下图1所示:

  1. public static int getServiceCode(String str){
  2. int code = 0;
  3. if(str.equals("Age")){
  4. code = 1;
  5. }else if(str.equals("Address")){
  6. code = 2;
  7. }else if(str.equals("Name")){
  8. code = 3;
  9. }else if(str.equals("No")){
  10. code = 4;
  11. }
  12. return code;
  13. }

重构后的代码如下所示:

  1. public static void initialMap(){
  2. map.put("Age",1);
  3. map.put("Address",2);
  4. map.put("Name",3);
  5. map.put("No",4);
  6. }

上述代码是直接通过Map结构,将条件表达式分解, Key 是条件变量,Value是条件表达式返回值。取值很方便,显然高效率O(1)时间复杂度取值。这种重构技巧适合于比较简单的条件表达式场景,下面是比较复杂的没有返回值的条件表达式场景,我们去看看如何处理。

反射去除分支结构

原始的条件表达式代码如下图1所示:

image

图1 条件表达式示范

image

图2 通过Map和反射重构示范

如上图2所示,通过Map和反射去分解条件表达式,将条件表达式分支的逻辑抽取到子类中的覆写函数中,提取了共同的抽象类,里面包含抽象接口 handleBusinessData,子类继承实现它。

多态取代条件表达式

image

图3 重构后的案例结果图

image

图4 重构后的案例-多态如何使用

image

图5 重构后的代码结构图

image

图6 重构-抽象类、简单工厂模式思想去实现条件表达式的分解

如上图6所示,在原始的条件表达式中,有两个条件表达式分支(分支逻辑):

共同抽取了基类抽象类:AbstractPassengerOperation,其两个分支子类去继承抽象类。

为了分解条件表达式,笔者采取了多态的重构技巧去实现,具体有两种实现方式,第一种实现方式是采用抽象类去实现多态,代码结构图如图5 passenger文件夹,UML类图如上图6所示。第二种实现方式是采用接口去实现多态,代码结构如图5 passenger2 文件夹,UML类图如上图7所示。

image

图7重构-接口状态者模式思想去实现条件表达式的分解

如上图7所示,在原始的条件表达式中,有两个条件表达式分支(分支逻辑),其分支逻辑分别放在了子类HotelCNPassengerState 和 HotelENPassengerState中,统一提取了接口类 PassengerState类,里面包含子类都需要实现的两个基础接口。从图7,可以看出,是使用了状态者模式。

经过了上述重构之后,我们达成了什么效果:

重构技巧-移动平台Android实战篇

前面笔者从理论和实际案例的角度对重构进行了分析,包括为什么需要重构、重构的作用、重构的时机、如何进行重构等内容,推荐提前阅读。

接下来笔者将从实践的角度去分享,即在平时开发Android工程中,我们如何高效去做重构,重构和开发怎么比较好的有效结合起来。

Long Method 实战

Long Method 是笔者前面提到的“代码坏味道”之一,这也是开发者一般经常容易犯的典型错误。

接下来笔者介绍在Android平台中如何去解决这个“bad taste”,实际上我们可以通过计算函数的圈复杂度(cyclomatic complexity)来判断函数是否过长,一般cyclomatic complexity > 11 ,就可以认为函数过长,需要进行重构优化,那么关于函数重构的优化技巧在前面几章我也有重点提到。

在解决圈复杂度过大这个问题,首先我们要去发现你的工程哪里存在问题,这一步我们可以通过工具或者第三方插件帮我们去解决,比如打开Android studio 工具栏 Analyze –> Run inspection by name,如下图8所:

image
图8 Analyze工具示意图

如图8所示,选择Run inspection by name ,打开如下图9所示界面:

image
图9 Overly long method 界面示意图

如图9所示,输入long 出现Overly long method ,选择如上图所示,点击会打开一个新的界面如图10所示:

image
图10 Run ‘Overly long method’

如图10所示,可以选择对当前工程,当前File,当前Module 或者其他Module进行分析,等待运行一段时间分析结果如下图11所示:

image
图11 Run ‘Overly long method’ 结果

得到图11分析结果之后,我们就可以有针对性的去进行优化重构了,知道哪些类哪些函数需要去优化,具体重构优化是一般可以将过长的函数拆分成几个不同的小函数,拆分原则:一个函数的功能要保持职责单一,查询和修改职责分开;所以可以通过不同类型的功能业务逻辑处理或者查询、修改功能去拆分大函数。

Too many parameters 实战

函数参数过多,也是典型的“代码坏味道”之一,同理打开如上图8所示的界面,然后输入 too many pa 关键字打开如下图12所示的界面:

image
图12 Analyze too many pa 关键字界面

选择图中所示的 “Method with too many parameters”,会出现如上图10所示的界面,然后选择“Whole Project”,运行之后,分析得到的结果如下图13所示:

image
图13 “Too many parameters”结果分析图

Redundant local variable 实战

冗余的局部变量,同样是造成代码坏味道的源头,输入 “Redudant  关键字”,同理执行得出分析结果如下图14所示,然后我们根据分析后的结果有针对性的去重构优化:

image
图14 冗余局部变量分析结果

Unused Declaration –无用函数实战

无用函数是“代码坏味道”来源之一,很多函数因为历史遗留的原因,需求已经下线了但是代码还在遗留在工程里面,或者因为重构,历史遗留代码没有完全删除或者想暂时留着下个版本使用,这些都是不好的习惯,不用的代码应该立即删除,而不应该保留在工程项目中。

同理打开如上图8所示的界面,然后输入 Unused declaration关键字打开如下图15所示的界面:

image
图15 Unused declaration

分析结果能得出你整个工程中没有被使用的函数,我们都可以删除掉。

无用函数参数-实战

同理,输入关键字 Unused method parameter, 如下图16所示, 执行可以分析出工程中有哪些函数存在无用参数,可以针对性的进行优化。

image
图16 unused method parameter

infer 实战

Infer 是Facebook开源的静态代码检查工具,可检查 Android 和 Java 代码中的 NullPointException 和 资源泄露。除了以上,Infer 还可发现 iOS 和 C 代码中的内存泄露,内存泄露,内存泄露。

Android studio 已经将infer集成到工具栏里面,点击Analyze->infer Nullity,执行分析得出的界面类似如下图17所示:

image
图17 infer Nullity 分析结果图

点击图17所示的分析结果具体项,可以定位到具体的代码文件,然后我们去手动判断 或者直接点击“Infer Nullity Annotations”,工具直接帮我去完成改造结果。

第三方插件与Android studio 的集成

FindBugs 集成

FindBugs是一个开源的静态代码分析工具,基于LGPL开源协议,无需运行工程就能对代码进行分析的工具。它不注重style及format,注重检测真正的bug及潜在的性能问题 ,以bytecode(*.class、*.jar)为对象进行检查。除了单独运行,还可以用作Android-studio 和Eclipse 的Plug-in,以及嵌入Ant或者Maven作为task之一进行运行。

Findbugs自带60余种Bad practice,80余种Correntness,1种Internationalization,12种Malicious code vulnerability,27种Multithreaded correntness,23种Performance,43种Dodgy。它可以检测检测java programing中容易陷入的bug pattern,比如equals()实现时的一般规约违反Null pointer的参照 ,Method的返回值的check遗漏 ,初始化前field的访问, Multi-thread的正确性,无条件的wait,Code的脆弱性 ,可以变更的静态object ,内部数列参照的return等。

Android Studio 可以通过插件的方式安装,具体是打开Android Studio->Preference –>搜索plugin 选择 Plugins Tab ,打开界面如下图17所示:

image
图17 搜索FindBugs-IDEA界面

如上图17中,点击install ,downloading plugin install ,然后重启Android studio ,会有提示界面如下图18所示:

image
图18 Android FindBugs Enable

点击“Enable Android FindBugs”,会打开界面,在见面中添加Plugin For Android FindBugs即可。

然后在Android Studio工具栏上,打开如下图19所示的界面:

image
图19 FindBugs 入口界面

如上图19所示,可以分析对前的文件,可以分析一个Module files ,也可以分析一个工程文件,选择一项会得出分析结果如下图20所示:

image
图20 FindBugs分析结果图

根据图20所示的结果,我们可以查看具体的Bug details ,存在什么问题,然后具体跟踪到对应的代码,根据对应的建议去修改。

MetricsReloaded 集成

MetricsReloaded是一个计算代码复杂度即圈复杂度的Jetbrain开源开发的第三方插件。关于代码复杂度,有个维度的衡量,在这里需要普及下软件复杂度的相关知识:基本复杂度(Essential Complexity (ev(G))、模块设计复杂度(Module Design Complexity (iv(G)))、Cyclomatic Complexity (v(G))圈复杂度。

ev(G)基本复杂度是用来衡量程序非结构化程度的,非结构成分降低了程序的质量,增加了代码的维护难度,使程序难于理解。因此,基本复杂度高意味着非结构化程度高,难以模块化和维护。实际上,消除了一个错误有时会引起其他的错误。

Iv(G)模块设计复杂度是用来衡量模块判定结构,即模块和其他模块的调用关系。软件模块设计复杂度高意味模块耦合度高,这将导致模块难于隔离、维护和复用。模块设计复杂度是从模块流程图中移去那些不包含调用子模块的判定和循环结构后得出的圈复杂度,因此模块设计复杂度不能大于圈复杂度,通常是远小于圈复杂度。

v(G)是用来衡量一个模块判定结构的复杂程度,数量上表现为独立路径的条数,即合理的预防错误所需测试的最少路径条数,圈复杂度大说明程序代码可能质量低且难于测试和维护,经验表明,程序的可能错误和高的圈复杂度有着很大关系。

同理,如上图17所示一样去安装MetricsReloaded插件,安装成功后执行Analyze->Calculate Metrics,打开如下图21所示的界面:

image
图21 Calculate Metrics 界面

如上图所示,选择Complexity metrics,执行分析结果如下图22所示:

image
图22 Calculate Metrics 分析结果图

如上图22所示界面中的红颜色部分,代表需要去重构优化的,点击当前行,会定位到源代码,然后我们针对性去优化函数。上图中,可以分析出方法的圈复杂度、类的圈复杂度、包的圈复杂度、模块的圈复杂度、工程的圈复杂度。

Sonar 集成

对于Android(Java)工程,Sonar官方提供了Java Plugin和Java-specific Plugins,这些插件可以实现大部分Findbugs、PMD、Checkstyle、Android Lint等的检查规则。主要可以从以下几个方面检测代码质量:

(1)复杂度:项目中方法、类、文件的复杂度分布情况;

(2)重复:展示代码中重复严重的地方;

(3)单元测试覆盖率:统计并展示单元测试覆盖率(主要用于java工程);

(4)代码标准:通过PMD、CheckStyle等代码规则检测工具规范代码编写;

(5)代码注释:没有注释或者过多的注释都不是一个良好的编程习惯; 

(6)潜在的bug: 通过PMD、Findbugs等代码检测工具检测出潜在的bug;

(7)架构设计:可以检测耦合、依赖关系、架构规则、管理第三方的jar包等。

集成Sonar之后,我们需要着种解决的就是代码重复率问题,这也是“代码坏味道”最典型的问题,开发者最容易犯这个问题,特别是不少开发者喜欢偷懒,容易拷贝来拷贝去,造成工程代码的重复率比较高。一次构建运行之后,我们可以得出分析结果,类似如下图23所示:

image
图23 sonar构建运行结果

点击重复率,我们可以看出哪些文件之间的代码是重复的,然后针对性使用抽取工具类、合并类、合并分解函数等技术重构手段去优化。

SonarLint集成

前面我们所讲到的Sonar之前的提供的本地工具是需要依赖SonarQube服务器的,这样导致其运行速度缓慢。 新出的SonarLint的扫描引擎直接安装在本地,速度超快,实时探测代码技术债务,给程序员最快速的反馈,排除代码异味的绝佳利器,帮助程序员获得Clean code。 新版SonarLint也能链接SonarQube服务器,但这并不必要。本地安装SonarLint来做代码本地扫描,本地发现本地修改,而且能快速看到修改结果,快速处理代码臭味,有效控制技术债务。

按照如上图17所示一样去安装SonarLint插件,安装之后重启Android Studio ,即可动态扫描出结果如下图24所示:

image
图24 SonarLint扫描结果图

重构技巧实战-小结

本文我们讲述了在Android程序开发过程中如何结合工具去帮助我们做重构优化的各种技能包括Android Studio自己已经集成的插件Code Inspection 、infer Nullity以及FindBugs、MetricsReloaded、Sonar、SonarLint等第三方插件工具,其实还有很多类似著名的插件比如QAPlugin、PMD、Hammurapi 、Lint4j等工具,大家可以自行尝试使用,在这里我不一一说明。

所谓工欲善其事必先利就是这个道理,我们如果需要去做重构优化,首先我们要知道我们做的不好的地方-代码的坏味道在哪里,这种工作如果靠人为手动的去发现,那么效率和产出将会及其低下,所以我们需要借助于集成插件工具帮我们自动去扫描发现问题,然后再去针对性的重构优化,产出Clean code 。

写在最后

重构是一门比较大而深的话题和课题,笔者这次主要探讨了如何通过有效的重构技巧去写成优秀的整洁代码,代码整洁之道就是要将重构始终贯穿在整个开发过程中,不断的持续的渐进重构,从而将以前的技术债全部还完。

重构是个技术活,需要很资深的人士去整体架构把控技术方案和产品质量,才能使重构做的更加有效并且不会引入新的问题,但是无论我们最终采取什么手段去重构,最终我们都需要尽量符合Solid设计相关原则:

当我们在做重构优化的时候应该充分考虑上面这几个原则,一开始可能设计并不完美,不过可以在重构的过程中不断完善。但其实很多人都跳过了设计这个环节,拿到一个模块直接动手编写代码,更不用说去思考设计了,项目中也有很多这样的例子。当然对于简单的模块或许不用什么设计,不过假如模块相对复杂的话,能够在动手写代码之前好好设计思考一下,养成这个习惯,肯定会对编写出可读性、稳定性、健壮性、灵活性、可服用性、可扩展性较高的代码有帮助。

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注