并不能给一个何时必须重构的精确衡量标准,只能给出一些迹象,它会指出“这里有一个可以用重构解决的问题”。比如一个类有多少变量算是太大,一个函数有多少代码才算太长。本文给出一些代码得坏味道,可以作为重构的切入点
1 神秘命名(Mysterious Name)
整洁代码最重要的一环就是好的明字,然而命名是编程中最难的两件事之一。正因为如此,改名可能是最常用的重构手法,包括改变方法名、变量名、字段名等。
如果我想不出一个好明字,说明背后很可能潜藏着更深的设计问题。个人理解好的命名应该是能清晰的表明自己的功能和用法。
2 重复代码(Duplicated Code)
如果在多个地方看到相同的代码结构,那么可以肯定,设法将她们合而为一,程序会变得更好。
- 最简单得重复代码就是,两个函数有相同的代码块,这个时候只需要把相同的代码块提出来就可以了。
- 如果重复代码只是相似,而不是完全不同,可以首先重新组织代码顺序,把相似的部分放在一起,以便提炼。
- 如果重复的代码位于同一个父类的多个子类中,可以把代码移动到父类中。
3 过长函数(Long Function)
根据经验,活得最长、最好的程序,其中的函数都比较短。初次接触到这种代码库的时候,往往会觉得程序里满是无穷无尽的委托调用,但是间接性带来的好处就是更好的阐释力、更易于分享、更多的选择。
小函数会给代码的阅读者带来一些负担,因为必须经常切换上下文,才能明白函数在做什么,但现代的开发环境,可以让你快速的跳转。小函数易于理解的关键还是在于良好的命名,如果你能给函数起个好明字,阅读代码的人就可以通过明字了解函数的作用,根本不必去看其中写了些什么。
我们遵循这样一条原则:每当感觉需要以注释来说明点什么的时候,就需要把说明的代码块,写进一个独立函数中,并以其用途命名。
如果遗留代码前方,有一行注释,就是在提醒你:可以将这段代码替换成一个函数,而且可以在注释的基础上给这个函数命名。
条件表达式和循环通常也是提炼的信号,对于庞大的switch语句,其中的每个分支都应该提炼成函数。如果你发现提炼出的循环很难命名,可能是因为其中做了几件不同的事,这种情况请勇敢的拆分循环,将其拆分成各自独立的任务。
4 过长参数列表(Long Parameter List)
如果可以向某个参数发起查询而获得另一个参数的值,那么就可以发起查询去掉这个参数传递。
如果有几项参数总是同时出现,可以用引入参数对象将其合并为一个对象。
如果多个函数有同样的几个参数,引入一个类就尤为有意义,可以将函数组合成类。
5 全局数据(Global Data)
如果数据在程序启动之后不再修改,这样的全局数据就算相对安全。
全局数据的问题在于,从代码库的任何一个角落都可以修改它,一次又一次,全局数据造成的那些诡异bug,问题的根源却在遥远的别处,想要找出错误的代码难于登天。全局数据最显而易见的形式就是全局变量,但变量和单例也有这样的问题。
首要的防御手段是封装变量,每当看到可能被各处代码污染的数据,就应该把变量封装到一个类或模块中,只允许模块内的代码使用它,其他地方只能通过封装的函数来访问这个全局变量,从而尽量控制其作用域。
良药与毒药的区别在于剂量,有少量的全局数据也无妨,但数量越多,处理的难度就会指数上升。
6 可变数据(Mutable Data)
我在一处更新数据,却没有意识到软件的另一处期望着完全不同的数据,于是一个功能失效了。可以用封装变量来确保所有数据更新操作,都通过有限的几个函数来进行。将查询函数和修改函数分离,确保调用者不会调到有副作用的代码。
如果可变数据的值能在其他地方计算出来,这就是一个特别刺鼻的坏味道。如果变量作用域只有几行代码,即使其中的数据可变,也不是什么大问题;但随着变量作用域的扩展,风险也随之增大。
7 发散式变化(Divergent Change)
TODO:区分霰弹式修改
我们希望软件能够容易被修改。一旦需要修改,我们希望只改动系统的某一点,只在该处做修改。如果不能做到这一点,这就是一个刺鼻的坏味道了。
如果新出现一种工具,我必须修改4个函数,这就是发散式变化的征兆。可以通过搬移函数、提炼函数、提炼类等方法,消除发散式变化。
8 霰弹式修改(Shortgun Surgery)
如果需要修改的代码散步四处,你不但很难找到他们,也很容易错过某个重要的修改。这种情况下,应该使用搬移函数和搬移字段把所有需要修改的代码放进同一个模块里。
面对霰弹式修改,一个常用的策略就是使用与内联相关的重构,把不应该分散的逻辑拽回一处。
9 依恋情结(Feature Envy)
所谓模块化,就是力求将代码分出区域,最大化区域内部的交互、最小化跨区域的交互。但有时你会发现,一个函数跟另一个模块中的函数或者数据交流格外频繁,远胜于在自己所处模块内部的交流,这就是依恋情结的典型情况。
疗法显而易见:这个函数想跟这些数据待在一起,就使用搬移函数把它移过去。当然,并非所有情况都这么简单,一个函数往往会用到几个模块的功能,那么它究竟该被置于何处呢,我们的原则是:判断哪个模块拥有的此函数使用的数据最多,然后就把这个函数和那些数据摆在一起。如果先以提炼函数将这个函数分解为数个较小的函数,并分别置于不同地点,上述步骤也就比较容易完成。
10 数据泥团(Data Clump)
数据项就像小孩子,喜欢成群结队地待在一块。你常常可以在很多地方看到相同的三四项数据:两个类中相同的字段、许多函数签名中相同的参数。这些总是绑在一起出现地数据,应该拥有他们自己地对象。
11 基本类型偏执(Primitive Obsession)
大多数编程环境都大量使用基本类型,即整数、浮点数和字符串等。我们经常看到把钱当作普通数字来计算地情况、计算物理量时无视单位地情况以及大量类似
if (a < upper ** a > lower)
这样地代码。可以用对象取待基本类型,将原本单独存在地数据值替换为对象,从而走出传统地洞窟,进入炙手可热地对象世界。
12 重复地switch(Repeated Switches)
在不同地地方反复使用同样地switch逻辑(可能是switch-case语句,也可能是连续地if-else语句)。重复的switch问题在于:每当你想增加一个选择分支时,必须找到所有地switch,并逐一更新。多态给了我们对抗这种黑暗力量地武器,使我们得到更优雅地代码库。
13 循环语句(Loops)
Java中地stream中各种filter、map操作,可以帮助我们更快地看清被处理地元素以及处理他们地动作。
14 冗赘地元素(Lazy Element)
可能有这样一个函数,她的名字就跟实现代码看起来一摸一样,也可能有这样一个类,根本就是一个简单的函数。这可能是因为,起初在编写这个函数时,程序员也许希望它将来有一天会变大、变复杂,但那一天从未到来;也可能是因为,这个类原本是有用的,但随着需求的迭代与重构的进行越来越小,最后只剩了一个函数。无论上述哪一种原因,请让这样的程序元素庄严赴义吧。
15 夸夸其谈通用性(Speculative Generality)
当有人说“我想我们总有一天需要做这事”,并因而企图以各式各样的钩子和特殊情况来处理一些非必要的事情,这种坏味道就出现了。这么做的结果往往造成系统更难理解和维护。
16 临时字段(Temporary Field)
有时你会看到这样的类:其内部某个字段仅为某种特定情况而设,这样的代码让人不易理解,因为你通常认为对象在所有时候需要它的所有字段。在字段未被使用的情况下猜测当初设置它的目的,会让你发疯。
请使用提炼类给这个可怜的孤儿创造一个家,然后用搬移函数把所有和这些字段相关的代码都放进这个新家。
17 过长的消息链(Message Chains)
如果你看到用户向一个对象A请求另一个对象B,然后再向B请求另一个对象C,然后再向C请求对象D......这就是消息链。在实际代码中你看到的可能是一长串取值函数或一长串临时变量。采用这种方式,意味客户端代码将与查找过程中的导航结构紧密耦合。一旦对象间的关系发生任何变化,客户端就不得不做出相应修改。
通常更好的选择是:先观察消息链最终得到的对象是用来干什么的,看看能否以提炼函数把使用该对象的代码提炼到一个独立的函数中,再运用搬移函数把这个函数推入消息链。如果还有许多客户端代码需要访问链上的其他对象,同样添加一个函数来完成此事。
18 中间人(Middle Man)
也许你会看到某个类的接口有一半的函数都委托给其他类,这就是过度运用,这时应该使用移除中间人,直接和真正负责的对象打交道。如果这样“不干实事”的函数只有少数几个,可以运用内联函数把他们放进调用段。
19 内幕交易(Insider Trading)
两个模块之间,一定的数据交换不可避免,但我们必须尽量减少这种情况,并把这种交换都放到明面上来。如果两个模块总是窃窃私语,就应该用搬移函数和搬移字段,减少她们的私下交流。如果两个模块有共同的兴趣,可以尝试再新建一个模块,把这些公用的数据放在一个管理良好的地方。
20 过大的类(Large Class)
如果想利用单个类做太多事情,其内往往就会出现太多字段。一旦如此,重复代码也就接踵而至了。通常,如果类内的数个变量有着相同的前缀或者后缀,这就意味着有机会把她们提炼到某个组件内。
观察一个大类的使用者,经常能找到如何拆分类的线索,看看使用者是否只用到了这个类所有功能的一个子集,每个这样的子集都可能拆分成一个独立的类。
21 异曲同工的类(Alternative Classes with Different Interfaces)
使用类的好处之一就在于可以替换:今天用这个类,未来可以换成用另一个类。但只有两个类的接口一致时,才能做这种替换。
22 纯数据类(Data Class)
所谓纯数据类是指,她们拥有一些字段,以及用于访问这些字段的函数,除此之外一无长物。这样的类只是一种不会说话的数据容器,她们几乎一定被其他类过分细琐的操控着。
纯数据类常常意味着行为被放在了错误的地方。也就是说,只要把处理数据的行为从客户端搬移到纯数据类里来,就能使情况大为改观。
但也要有例外情况,纯数据记录对象被用作函数调用的返回结果,这种结果数据对象有一个关键的特征:她是不可修改的,不可修改的字段无需封装。
23 被拒绝的遗赠(Refused Bequest)
子类应该继承父类的函数和数据。但如果她们不想或不需要继承,就意味着继承体系设计错误。你需要为这个子类新建一个兄弟类,把不需要的函数和数据,推给那个兄弟。这样一来,父类就只持有所有子类共享的东西。
24 注释(Comments)
别担心,我们并不是说不该写注释。常常会有这样的情况:你看到一段代码有着长长的注释,然后发现这些注释之所以存在,是因为代码很糟糕。
注释可以带我们找到前面提到的坏味道,然后以各种重构手法把坏味道去除,完成之后我们常常会发现:注释已经变得多余了,因为代码已经清楚得说明了一切。
网友评论