代码重构

Code refactor

Posted by Bryan on October 6, 2018

代码重构

重构一直是一项写出可靠代码的重要手段,一般情况下,很难一次写出可靠到完全没有重构空间的代码,即使当前情况下完全没有重构空间,但是之后随着业务的发展,代码质量可能会慢慢下降,或者原先的设计无法满足新的需求,这时候,重构就相当必要了。最近刚好有空重新阅读了大作《重构,改善既有代码的设计》,整理一下相关内容

两顶帽子

很多时候,我们重构代码是在添加功能的时候发现原有代码无法支持新的功能时进行的,这时候,很容易犯的错误是一边增加新功能,一边进行重构,这种做法很容易导致修改被遗漏,从而引入新bug。事实上,按照本书中说法,功能添加和代码重构是两顶帽子,你必须时刻清醒,你目前带着哪一顶帽子。如果当前带着重构的帽子,就必须保证原有接口不变,原有的功能不受影响。但是如果带着功能添加的帽子,就不要随意更改原有接口,从而导致原有代码可能运行失败。

代码测试

代码重构是依赖一个可靠的测试环境的,因为代码重构进行是小步前进的,对一块代码进行重构优化后,测试一下,原有的代码是否能正常工作,继续优化,再次测试。按照这种方式,没有一个完整的代码测试,重构是基本无法实施的。

代码的坏味道

为了知道是否需要重构,我们首先得培养自己的品味,如果自己都不知道什么是好代码,什么是坏代码,那么重构就无从谈起了。因为重构主要处理的坏代码,作者提供了一些坏代码的味道,如果你在代码中发现这些,那么大概率你就需要重构了。下面是部分坏味道:

  • Duplicated code (重复代码)
  • Long Method (过长函数)
  • Large Class (过大的类)
  • Long Parameter List (过长参数列)
  • Divergent Change (发散式变化)
  • Shotgun Surgery (霰弹式修改)
  • Feature Envy (依恋情结)
  • Switch Statements (Switch 语句)
  • Inappropriate Intimacy (狎昵关系)

Duplicated code (重复代码)

重复代码被认为是坏味道之首,大部分时候为了方便,简单复制粘贴,后续基本都会带来问题。当代码需要改变的时候,到处寻找重复代码进行一一改造。相信我,维护起来会让你想死的。遇到需要复制粘贴时,多想一想,是不是可以提炼一下,不要偷懒。

Long Method (过长函数) / Large Class (过大的类)

大的函数或类基本是与优雅绝缘的,一般情况下,尽量分拆大的方法或类,这样代码的灵活性会大大提升

Long Parameter List (过长参数列)

复杂的函数也是一个坏味道,而过长的参数一般情况下表示方法承担了过多的功能,因此不是一个好味道

Divergent Change (发散式变化) / Shotgun Surgery (霰弹式修改)

发散式变化表示一个方法会因为多种条件的变化而产生变化,这种情况下一般是方法承担的责任太多,因此需要分拆责任,将多种因素的变化分离至不同的方法中

而与之正好相反的是霰弹式修改,表示一个条件变化会导致多个方法变化,这种情况下一般是方法承担的责任过少,多个方法都联合承担同一个责任,换一个角度来看,多个方法耦合严重。此时应该将多个方法封装为一个类,或者进行合理的整合

Feature Envy (依恋情结) / Inappropriate Intimacy (狎昵关系)

依恋情结是一个类的方法大量依赖另一个类的数据,这种情况下,调整此方法的位置,让方法移动到其依赖类中可能是一个更好的选择

狎昵关系是指两个类互相耦合,互相希望探访对方的private数据,这种情况下,建议将类共同点提炼至额外的地方,避免这种过度亲密的类。

Switch Statements (Switch 语句)

Switch语句被认为是一个不好的信号,本质上switch是代码重复,建议采用多态来实现。

重构手段

在重构的书中,介绍了比较多的重构手段,可以方便对现有的代码执行重构。但是手段不是绝对的,各自适用于不同的场景,需要根据具体情况判断是否适用。

Extract Method(提炼函数)

对于大的函数,提炼函数是一个很好的工具。如果函数中部分代码可以提炼为特定的功能,那么就提炼为一个新的函数,按照函数意图给新函数命名。

Replace Temp with Query(以查询取代临时变量)

函数中,有些临时变量用于存储表达式计算的结果,可以直接使用表达式提炼为新的函数中,然后需要需要使用临时变量的地方,调用此方法,从而消除临时变量。消除临时变量可以有利于提炼函数的实施

Split Temporary Variable(分解临时变量)

临时变量被多次赋值,既不是循环变量,也不是收集计算结果,针对每次赋值,创建一个独立,对应的临时变量。可以理解为临时变量承担了多个存储值的责任,这不是好的做法。而且容易产生不容易调试的bug

Remove Assignments to Parameters(移除对参数的赋值动作)

对参数进行赋值不是一个好的实践,应该采用临时变量取代参数的位置。这个和分解临时变量类似,参数承担了多个责任,既承担了数据传递责任,也承担了存储临时变量的责任,需要分解。而且同样容易导致很难发现的bug

Replace Array with Object(以对象取代数组)

如果数组中元素代表不同的东西,此时采用对象代替数组,对于数组中每个元素,以一个字段来表示。这个适用的场景是原始的数组用于存储多种信息,使用数组的地方需要自己记住元素在数组中的位置,这样对调用者要求太高,而且可读性很差,采用类来替换后,可读性大大提高

Replace Magic Number with Symbolic Constant(以字面常量取代魔法数)

魔法数直接出现在代码中兼职是一种灾难,阅读者很难理解这种魔法数值,遇到需要使用魔法数值的情况,采用字面常量来替代,字面常量需要根据含义进行命名

Encapsulate Field(封装字段)

封装字段是指将不必要的字段设置为private,事实上,除了字段,还有函数,都需要尽量封装,不必要的权限不要开放。

Decompose Conditional(分解条件表达式)

如果条件表达式比较复杂,可以将if,then,else三个段落分别提炼为独立的函数。条件表达式作为导致函数复杂性的重灾区,如果可能的话,尽量拆分

Consolidate Conditional Expression(合并条件表达式)

条件表达式多种情况下返回相同的值,此时可以合并,如果合并后条件变得复杂,可以将合并后的条件独立为新的方法

Replace Nested Conditional with Guard Clauses(以卫语句取代嵌套条件式)

多重的条件表达式会导致代码难以理解,大部分情况下可以使用return减少嵌套层次,遇到某种情况下已经处理结束,可以直接通过return返回,这样可以避免写很复杂的嵌套代码。

Introduce Assertion(引入断言)

当需要对程序状态有某种假设时,可以使用断言,这样可以尽快将错误暴露出来。在实践中,我会在函数最前面进行状态判断,如果状态异常,则抛出错误。尽量将错误前置

Separate Query from Modifier(将查询函数和修改函数分离)

函数既要返回对象状态值,又修改对象状态。此时需要建立两个函数,一个负责查询,一个负责修改。这种情况下可以认为是函数承担了修改和查询的责任,函数应该是承担单一责任的,承担过多责任会导致灵活性下降。在实践中,我会有查询方法和修改方法,但是在修改方法后依旧会返回修改后的值,从而避免多一个查询操作。

Parameterize Method (令函数携带参数)

如果有若干函数做着类似的工作,函数本体中包含了不同的值,此时可以建立单一函数,以参数表示不同的值。这种方式事实上是建议将多个类似的函数进行合并,从而简化问题,去除重复代码。在实践中,我会将重复的功能代码进行合并,对于提供的接口,我会保持分离状态,这样保证功能实现部分是统一的,但是又不会因为功能的实现导致调用者需要添加额外的参数

Introduce Parameter Object(引入参数对象)

某些参数总是同时出现,可以使用对象替代这些参数。对应联合出现的参数,将参数进行封装,可以缩短函数中参数长度,也便于函数中参数的组织

Replace Constructor with Factory Method (以工厂函数取代构造函数)

你希望在构建对象时不仅仅做简单的建构动作,将构造函数修改为工厂函数。这种方法一般适用于复杂的构造函数,或者可能会多种不同的类或子类会被创建出来,或者是存在与业务相关的构建,为了隐藏这个复杂的构造关系,使用工厂方法就最合适不过了。

上面介绍的只是我实践中经常会使用的一些重构方法,更多的重构方法,大家可以阅读原著。希望大家都能写出可靠的代码,没有bug