代码坏味道的变迁

发布时间 2023-07-07 11:58:58作者: 一杯清酒邀明月

  2018年,Martin Fowler的《重构》第二版出版,距离第一版,已经19年了。为什么作者要出新版?通过分析两个版本的变化,可以探知端倪。这些变化,一方面体现了作者多年的思考和总结,另一方面也体现了技术潮流的演进。本文先从讨论坏味道的变迁开始。

  《重构》第一版中有22种坏味道,第二版中有24种。相对于第一版,第二版中完全保留了16种。变化的部分分为三种情况:新增、删除和改名。下面分析一下这几类变化。

1 新增的坏味道

  第二版中新增了4种坏味道。

1.1 全局数据(Global Data)

  全局数据,主要是可变的全局数据,“自古以来”就被认为是有害的。第一版中没有出现,反而是一件奇怪的事情。究其原因,大概是因为那时用的是Java。Java是所谓(形式上)纯粹的面向对象语言,不太容易写出全局数据。而第二版中用的是JavaScript。在这种松散的语言中,全局数据就很容易出现了。第二版中增加这个坏味道,算是对第一版中遗漏的弥补。

  为什么第二版要用JavaScript而不是Java呢?作者在书中说是因为JavaScript日益普及。但除此之外,我们还可以看出一个隐含的原因。

  在第一版出版的时候(1999年),我们可以看到,Martin Fowler是一个“面向对象原教旨主义者”,这在那个时代是很普遍的,因此第一版很强调面向对象编程。到了第二版,作者已经成长为一名“编程范式中立主义者”。因此作者会更强调在结构化、面向对象、函数式等编程范式中找到共性的东西。JavaScript的多范式特点恐怕是第二版中采用这一语言的另一个重要原因。

1.2 循环语句(Loops)

  对于同时支持命令式和函数式编程的语言来说的,当可以用 Stream 加 λ 表达式等方式简化代码时,用传统的循环语句就显得臃肿了。在第一版时,Java还不支持 λ 表达式,而作者当时可能也没有关注到函数式编程。在新版中,作者更加注重函数式编程,因此增加了这个坏味道。

  不过,用函数式编程简化循环时要注意一些通用的规则,比如说 λ 表达式应简短,不要有副作用等。

1.3 可变数据 (Mutable Data)

  这个坏味道首先仍然和函数式编程有关,因为函数式编程中理论上是没有“变”量的。

  其次,即使不考虑函数式编程,在能够使用不变数据时,也应该尽量不变。Java中的有一种建议是尽可能用final修饰属性和方法参数,用Collentions.ummodifiableList()来返回不可变列表,都体现了这一思路。在当前的JavaScript中,可以用闭包模拟不可变数据。

1.4 神秘命名 (Mysterious Name)

  编程中,命名是最难的问题之一,即使对于编程老手也是如此。反而新手常常忽略这一点。这个坏味道没有出现在第一版,只能理解为遗漏了。

2 改名的坏味道

  第二版中有4中坏味道进行了改名。

2.1 内幕交易(Insider Trading)

  原名:押昵关系(Inappropriate Intimacy)

  这是应该唯一一个与技术无关的改动。Intemacy的桃色意味较浓,在书中显得不雅而又突兀。改成“Insider Trading”就好多了。

  Insider Trading指模块之间过多的使用了对方的私有部分。以下是几种常见的情形:

  • C++中过度使用“友元”机制
  • 类中的字段设置成了public的,被其他类大量使用
  • 尽管类的字段不是public的,但有大量公用的setter和getter,事实上造成了与上一条类似的后果,这种情况也常常体现为 Feature Envy
  • 子类大量使用父类的protected字段

2.2 冗赘的元素(Lazy Element)

  原名:冗赘的类(Lazy Class)

  改动的原因仍然是“编程范式中立”。当把眼光放开后,不仅类会冗赘,其他元素,如函数、(Python等语言中的)模块等都可能冗赘。因此改为“冗赘的元素”是恰当的。

2.3 过长的函数(Long Function)

  原名:过长的方法(Long Method)

  仍然是因为“编程范式中立”。函数比方法范围更广。方法可以看作特殊的函数。因此第二版提“函数”而不是“方法”。这一现象也出现在多个重构手法的改名中。

2.4 重复的Switch(Repeated Switches)

  原名:Switch语句(Switch Statements)

  这里的Switch语句是switch、 case、 if...elseif...等形式的统称。

  仅从技术角度来说,任何switch语句总可以被多态所代替。在早期的面向对象社区中,有人认为一切swithch语句都应该用多态代替,否则就不够面向对象,不符合“开闭原则”了。从现在的观点来看,这显得有些矫枉过正。有时switch语句的使用是合理的。

  实际上,Martin Fowler在第一版中已经说过,重复的switch才会带来明显的问题。但由于这个坏味道的名字命名为"Switch Statement",因此会让望文生义的人陷入过度设计。所以第二版干脆改成“Repeated Switches”。这样意思就很明确了。

  这里的解决方案常常是策略模式、状态模式或模板模式。要注意的是,如果一个实体类,因为某些属性或状态而有不同的算法,那么一般来说,应该针对这些属性或算法采用上述设计模式,而不是对实体类本身采用多态。

3 删除的坏味道

  第二版中删除了2种坏味道。

3.1 平行的继承体系 (Parallel Inheritance Hierarchies)

  在第一版中已经说过,平行的继承体系是霰弹式修改的特例。而且这一坏味道仅局限于面向对象编程,与“编程范式中立”相悖。大概是因为这些原因,在第二版中干脆删除了。

3.2 不完美的库类 (Incomplete Library Class)

  在第一版中,这个坏味道涉及的问题是,当一个类库不能完全满足需要时的处理的技巧。

  尽管这些技巧是有用的,但这不应该算作坏味道。因为坏味道关注的应该是我们自己掌控范围内的,从而能够用重构技术进行完善的代码。而不完美的类库本身不在我们的掌控范围内,因此在第二版中应该删除。

4 总结

  第二版中对坏味道的修改可归纳为以下几种原因:

  编程范式中立

  这是最主要的原因,影响到的坏味道包括“循环语句”、“可变数据”、“冗赘的元素”、“过长的函数”。其中“可变数据”也可理解为一种遗漏。

  补充遗漏

  包括“全局数据”、“神秘命名”。其中“全局数据”也认为是由于编程范式中立而引发的。

  避免矫枉过正

  包括“重复的Switch”。

  更恰当的命名

  包括“内幕交易”。

  删除重复或不属于坏味道的内容

  包括“并行的继承体系”和“不完美的类库”。