重构 02:开始诊疗

Translated from:Refactoring

大多数代码,都具备“屎山”的特征。

代码的味道

  • 纳尼?代码怎么会有味道?
  • 虽然闻不出来…但是我敢打赌它肯定会发臭!

诊断:臃肿

臃肿的代码,包括臃肿的函数和类等等,太过臃肿的代码导致程序几乎无法运行。通常臃肿的问题不会马上就出行,而是随着时间推移,随着程序的发展,而逐渐积累(特别是没有人去关心这个问题时)。

病症:过长的函数

症状

一个函数包含太多的代码行。一般来说,任何超过十行的函数都应该让你产生不安的感觉。

long-method-01

产生原因

就像 Hotel California (译者注:不是很清楚这个梗,应该是这个),某些函数总是在一直添加东西,但没有任何逻辑被拆分或删除。由于写代码比读代码容易,这种“臭味”一直没有被注意到,直到函数变成一个丑陋、巨大的怪兽。

从开发人员的心态来说,创建一个新函数往往比在原有函数中添加逻辑更难。“只是再加两行代码而已,没有必要给这段逻辑单独放一个函数里…”。带着这种想法,我们经常在函数中塞入一行又一行的代码,最终产生了一团乱七八糟,像意大利面条一样的东西。

解决方案

一个经验法则是,如果你觉得需要在一个函数里,针对一段代码需要写注释,那你应该把这段代码放到一个新的函数中。如果是一定需要写注释的代码,哪怕是一行,也应该拆分出一个单独的函数。如果函数有一个描述性的名字,那后面的开发人员就不需要看代码来了解内部的运作。

long-method-02

小结

  • 在所有面向对象的项目中,函数越短的类寿命最长。一个函数或者函数越长就越难理解和维护。
  • 此外,长函数为不必要的重复代码提供的完美的藏身之处。
long-method-03

优化后会影响性能吗?

很多人说,增加太多函数会影响性能?但是在几乎所有试验中,其带来的性能损耗微乎其微,不值得担心。

而且通过以上措施,我们获得了清晰易懂的代码,在优化过程中,更有可能找到真正有效的函数来重组代码,获得真正的性能提升。

病症:过长的类

症状

一个类包含太多字段/函数/代码行。

large-class-01

产生原因

一般来说,类一开始被创建的时候都会比较小,但是一旦随着岁月流逝,这个类就会变得越来越大,功能也会越来越复杂。

和过长的函数一样,比起新创建一个类,程序员们更喜欢将新功能放在现有的类中。

large-class-02

解决方案

当一个类包含了大量的函数时,需要进行分析并且通过以下手段尝试将它们拆分。

  • 如果类中包含了可以独立的逻辑或者组件时,可以直接 拆分类
  • 如果类中包含的逻辑可以差异化实现,或者很少运行到的逻辑,可以尝试 拆分子类
  • 如果类中的函数和逻辑可以完全抽象,调用时再进行实现,可以尝试 拆分接口
  • 对于复杂的UI界面,比较好的函数是将模型数据和逻辑拆分到单独的对象中。这是可能会在2个地方存储一些数据的副本,并且保持数据的一致性。重复数据观察模式 提供了一种处理这种情况的方案。
large-class-03

重构了之后…

  • 重构这些大型类后,可以显著降低开发人员的心智负担。
  • 通常拆分大类后,可以避免很多重复的代码和功能。

病症:基础类型的滥用

症状

  • 一些简单的事物(如货币、范围、电话号码特殊字符等)使用基础类型而不是使用对象
  • 使用常量来编码信息,比如定义常量 USER_ADMIN_ROLE = 1来描述具备管理员权限的用户
  • 使用字符串常量作为数据类数组的字段名
primitive-obsession-01

产生原因

和其他问题一样,基础数据的滥用也是一时的懈怠。“只是想要一个东西放临时的数据!”程序员产生这种想法的时候,都是觉得比起创建一个全新的类,直接创建一个基础类型更方便。当每一次遇到相同的情况时,都会有这样偷懒的想法,导致我们最后创建了一个又一个的基础类型存放数据,而容纳这些基础类型属性的类也逐渐变得越来越大。

基础类型通常用于“仿冒”类型。比如,可以使用一组数字或字符串(而不是单独的数据类型)来构成某些实体的允许值列表。然后,通过常量为这些特定的数字和字符串提供易于理解的名称,这就是为什么它们被滥用的原因。

另外一个不正确使用基础类型的方式是使用基础类型“仿冒”类的字段。比如类中包含了大量不同的数据数组,在类中指定的字符串常量用于获取这些数据数组的索引。

解决方案

primitive-obsession-02

重构了之后…

  • 使用对象替换基础类型,可以使代码更加灵活。
  • 重构以后代码更容易理解,更加具备组织性。对特定数据的操作都在同一个地方而不是分散在各处。在阅读代码时不会出现难以猜测的奇怪常量,或者为什么有奇怪的常量出现在数组中。
  • 更容易找到重复的代码。

病症:大串的函数形参

症状

一个函数的参数超过三个或四个。

产生原因

把几种类型算法合并到一个函数中后,可能会给这个函数带来一长串的形参。大串的形参可能是为了控制函数中哪些逻辑被执行、被如何执行。

大串的形参也可能是拆分类的副产品。例如原始函数中创建特定对象的代码转移给了调用方(译者注:这样原始函数就会更加的灵活),调用方创建对象后通过参数传递,这样这个特定对象就和原始函数解耦了。但是如果调用方传递了多个不同类型的对象,每个对象都需要自己特有的参数,这样原始函数可能就需要大串的形参了。

函数中大串的形参会导致理解困难,当形参越来越长,函数也就越来越难以使用。通常也可以使用相关对象来代替大串形参,如果这个对象不能包含所有需要的信息,通过会创建额外的对象再作为函数参数。

解决方案

  • 检查传递参数的值。如果有些参数只是另一个对象的调用结果,那么可以 使用函数调用替换参数。这个对象可以放在自己的类字段中,也可以作为函数参数传递。
  • 不要将从另一个对象接受到的一组数据直接作为参数传递,而是将对象本身传递给函数,参见 保留整个对象
  • 如果有几个不相干的数据元素,有时可以 引入对象参数 ,将他们合并为一个参数对象。
long-parameter-list-02

重构了之后…

  • 精简、易读的代码。
  • 可能会发现以前没注意到的重复代码。

病症:重复代码簇

特征

有时相同的代码簇出现在程序的不同部分,比如连接数据库的参数。这些代码簇应该被整理成类。

data-clumps-01

产生原因

通常这些重复代码是由于架构不完善或者“Ctrl-C + Ctrl-V编程”造成的。

如果要确认一些代码是不是重复代码簇,只要删除其中一个数据值,看看其他值是否还有意义。如果不是这种情况,则表明这组变量应合并到一个对象中,这是一个好兆头。

解决方案

  • 如果重复的数据由一个类的字段组成,使用 拆分类 将字段移到它们自己的类中。
  • 如果在函数的参数中传递了相同的数据块,使用 引入对象参数 将它们设置为一个类。
  • 如果只是部分数据被传递给其他函数,请考虑将整个数据对象传递给函数,而不仅仅是单个字段。保留整个对象 将帮助解决这个问题。
  • 检查使用这些代码的字段,有时将这些代码转移到数据类中可能是个好主意。
data-clumps-02

重构之后…

  • 代码更有组织性易于理解。对特定数据的操作现在被收集在一个地方,而不是杂乱无章地分布在整个项目中。
  • 减少代码体积。
data-clumps-03

特例:什么时候忽略这个问题?

在函数参数中传递整个对象,而不是只传递它的值(基础类型),可能会在两个类之间产生意料之外的依赖关系。