前言
在日常生活中,当我们买的水果放久了之后会发出一种难闻的气味(“坏味道”),这个时候我们就应该把它扔掉。同样,代码也有“坏味道”,当然确定什么是和不是代码“坏味道”是主观的,它会随语言、开发人员和开发方法的不同而不同。在工作当中,很多时候都是在维护之前的项目和在此基础上增加一些新功能,为了能让项目代码易于理解和维护,要时刻注意代码中的“坏味道”,当发现代码如果有坏味道了,要及时去重构它使其变成优秀的整洁的代码。本文列举代码中一些常见的“坏味道”和相应的重构方案。
过长方法 (Long Method)
这种“坏味道”表现为方法代码行数过长
,方法行数越长,就越难以理解和维护它。一个比较有用的方案就是当你觉得需要对方法中的内容加注释的时候,你应该将这个代码段作为一个新方法提取出来,哪怕有时候仅仅是一行代码也可以这么做,而且方法的命名要尽量做到见名知意,如果局部变量和参数干扰到方法的提取,则可以使用引入参数对象来进行提取。一般情况下,方法中条件运算符和循环是可以将代码移至单独方法的一个很好的代码段,对于条件运算符,可以尝试分解条件,如果方法出现循环,可以尝试提取方法。
过大的类 (Large Class)
这种“坏味道”表现为一个定义了很多的变量、方法代码行数很长的大类
,刚开始的时候类通常都不“大”,一段时间之后,随着业务的发展新功能的增加,类通常都会就会变得越来越“大”,通常程序员都喜欢在原有的类上添加属性或者添加新的方法的方式来完成功能的开发,当一个类的代码行数过多或者功能职责过多的时候,就意味着我们应该将其拆分了,常用有以下三种不同的拆分方式:
- 提取新类,当大类的部分行为可以分解为一个单独的组件,则可以使用提取类的方式拆分。
- 提取子类,当大类的部分行为可以以不同的方式实现或在极少数情况下使用,则可以使用提取子类方式拆分。
- 提取接口,当有必要列出客户端可以使用的操作和行为的列表的时候,则可以提取接口的方式拆分。
通过重构大类,可以使开发人员无需记住一个类的大量属性,在许多情况下,将大类分成多个部分可以避免代码和功能的重复。
过长参数列表 (Long Parameter List)
这种“坏味道”表现为一个方法超过三个以上的参数
,当一个方法合并了几个算法之后就会可能出现过多参数的情况,这些参数用来控制方法将要运行哪种算法以及如何运行的。长参数列表也可能是由于我们将类的对象创建过程拆分产生的,想象这么一个场景,当我们把用于创建方法所需对象的代码片段从方法内部移至用于调用方法的代码,然后创建的对象作为参数传入方法,这样,原始类就不再了解对象之间的关系,依赖性降低了。当有多个这种对象需要创建之后,每个对象将需要自己的参数,这意味着参数列表会更长。随着时间的流逝,我们就会越来越难于理解这种方法的长参数列表的具体含义了,清除这种“坏味道”的方式就是将方法的参数列表封装成一个对象的属性。通过重构之后,可以使代码的可读性更高,代码更简短,同时可能还会让你看到以前未被注意的重复代码。
过多注释 (Too Many Comments)
这种“坏味道”表现为一种方法充满解释性的注释
,当开发者意识到自己的代码不直观或不明显时一般都会给代码加上相应的注释。写代码注释的意图通常都是好的,是为了可以有更好的可读性让后面易于维护,在这种情况下,代码注释就会掩盖了可以改进的可疑代码的“坏味道”,好的方法名或者类名就是最好的注释。
The best comment is a good name for a method or class.
当我们遇到没有注释就无法理解代码片段时,首先应该尝试以无需注释的方式更改代码结构,解决过多注释通常有以下几种方式:
- 提取变量,当如果要使用注释来解释复杂的表达式的时候,则可以使用“提取变量”的方式将表达式拆分为可理解的子表达式。
- 提取方法,当如果注释解释了一段代码片段,则可以通过提取方法的方式来将这一部分变成一个单独的方法,这个时候往往方法的名称就是注释的内容。
通过提取变量或者提取方法的方式可以使代码变得更加直观和明显。
Switch 滥用(Switch Abuse)
这种“坏味道”表现为代码中存在一个复杂的 switch 运算符
,通常,if
条件语句的代码可以分散在程序中的不同位置,当需要添加新条件后,就必须找到所有开关代码并进行修改。根据经验,当看到 switch
时,你第一时间应该想到要用多态性去重构代码。如果 switch
是基于类型判断的,可以使用“用子类替换”或“用状态/策略替换”。但是当运算符中没有太多条件,并且它们都使用不同的参数调用相同的方法,那么多态其实是多余的。在这种情况下,则可以使用“将参数替换为方法”,然后将该方法分解为多个较小的方法,并相应地更改 switch
,代码经过重构之后改进其的组织方式。当然如果 switch
操作只是执行简单的判断时,则没有必要进行代码重构。还有就是,在工厂设计模式(工厂方法和抽象工厂)使用开关运算符来选择创建的类时,也没有必要对其进行重构。
异曲同工类(Alternative Classes with Different Interfaces)
这种“坏味道”表现为两个类有着相同的功能,但方法名称不同
,产生这种代码的原因通常是创建其中一个类的程序员可能并不知道功能上等效的类已经存在。清除这种“坏味道”有以下几种方式:
- 方法重命名,重命名相同功能的方法,使它们在所有替代类中相同。
- 移动方法、添加参数和泛型方法使得方法的签名和实现相同。
- 如果仅仅是重复了方法的部分功能,可以使用提取相同父类的方式重构,在这种情况下,现有的类将成为该父类的子类。
通过重构异曲同工类后,可以去除掉不必要的重复代码,从而减少代码的行数,同时代码也会有更好的可读更易于理解。
临时变量滥用(Temporary Field)
这种“坏味道”表现为一些临时变量仅在某些情况下才获得其值,在这些情况之外,它们都为空
,通常,当我们在创建一个算法后需要定义一些临时变量以供该算法输入使用。此时,程序员往往会决定在类中为此算法去创建变量,而不是在方法中创建大量参数,导致这些变量仅在算法当中才会使用,其它地方都不会使用这些变量。一个应对的方式就是将这些临时变量和对其进行操作的所有代码都提取出来放到单独的类中。
重复代码(Duplicate Code)
这种“坏味道”表现为两个或者多个代码片段看起来几乎相同
,当我们多个人同时在同一项目中的不同部分上工作时,通常就会发生复制,产生重复的代码。因为正在实现不同的功能,因此可能并不知道其他人已经编写了类似的代码,这些代码其实是可以根据自己的需要进行复用的。当代码中的一些特定部分看起来不同但实际上实现相同的功能时,这样的代码有着更多细微的重复,这种情况下的代码重复可能很难找到和修复。如果重复代码在两个处于相同层次结构的子类
出现时,我们可以通过以下方式进行重构:
- 提取方法,将重复的代码片段提取为方法然后放到共同的父类当中。
- 如果重复的代码在构造方法内部,则将其提取到父类的构造方法当中去,然后再在当前类的构造方法中使用
super
的方式调用父类构造方法。 - 如果重复的代码结构上相似但又不完全相同,那么则使用模板方法方式重构。
如果重复代码在两个或者多个不同的类
出现时,我们可以通过以下方式进行重构:
- 如果这些类不是层次结构的一部分,可以使用提取共同父类的方式来为这些类创建一个保留所有之前的功能的单个父类。
- 当很难或者不可能创建父类,那么可以在其中的任意一个类中使用提取类的方式来重构,然后在其它类中使用刚刚创建出来的类。
通过重构合并重复的代码可以简化代码的结构并使其更加简短和易于后期维护。
总结
本文总结了一些代码中常见的“坏味道”并给出了一些解决方法,重构是需要我们开发人员时刻都要去做的,要将重构始终贯穿在整个开发过程中,不断去发现代码中的“坏味道”,不断的持续的渐进重构。最后不管我们是如何去重构代码的,其背后的指导思想都是 Solid 原则。