本文为阅读Martin Fowler所著的重构(REFACTORING)的笔记,主要是代码的坏味道一部分所描述的24种可能需要进行重构的情况。虽然本书的某些内容在当下每日一新的计算机环境或许已经不再那么合适,但是本书所传达的重构的核心原则和基本方法仍然具有重要的价值和指导意义。这里仅做一部分代码坏味道的解释。并附有原书附录代码坏味道与重构手法的速查表,表后的数字为在书中此种重构手法所在的页数。
一、代码的坏味道
1.神秘命名(Mysterious Name)
整洁代码最重要的一环就是好的名字,所以我们必须考虑如何给函数、模块、变量和类命名,能使他们清晰的标明自己的功能和用法。
在此处通常使用的重构方法就是改变函数声明、变量改名、字段改名等,好的名字能节省大量用来猜谜的时间。
2.重复代码(Duplicated Code)
当遇到重复的代码时,可以仔细观察这一部分尝试重构降低代码的重复。
当“同一个类的两个函数含有相同的表达式”可以尝试采用提炼函数的方式提炼出重复的代码,当代码只是相似而不是相同时,可以考虑移动语句调整代码顺序尝试提炼出相同的逻辑,如果重复的代码位于同一超类的不同子类中可以考虑函数上移来避免在两个子类之间互相调用。
3.过长函数(Long Function)
分解代码的原则:每当感觉需要以注视来说明点什么的时候,我们就把需要说明的东西写进一个独立函数中,并以其用途命名。我们可以对一组甚至短短一行代码做这件事,哪怕替换后的函数调用动作比函数自身还长,只要函数名称能解释其用途,我们就该毫不犹豫的这么做。
通常情况下要使函数变短,只需要通过提炼函数。找到函数中适合写在一起的部分,将他们提炼出来。当函数内有大量的参数和临时变量,他们会对你的函数提炼形成障碍时,就可以尝试以查询取代临时变量来消除这些临时元素。引入参数对象和保持对象完整则可以将过长的参数列表变得简洁一些。当前面都已经完成仍然有太多的临时变量和对象,我们就可以尝试使用以命令取代函数的方式。
条件表达式和循环通常也是提炼的信号。你可以使用分解条件表达式处理条件表达式。对于庞大的switch语句,其中的每个分支都应该通过提炼函数变成独立的函数调用。如果有多个switch语句基于同一个条件进行分支选择,就应该尝试以多态取代条件表达式。循环的处理方式,你可以将循环和循环内的代码提炼到一个独立的函数中,如果此函数因为其中做了多件不同的事导致很难命名,可以尝试使用拆分循环将其拆分为各自独立的任务。
7.发散式变化(Divergent Change)
如果某个模块经常因为不同的原因在不同的方向上发生变化,发散式变化就出现了。以收银为例,假如每增加一种支付方式就将导致你要不得不修改多处方法/函数,而每增加一个新的商品或折扣又要修改几个方法或函数,那么就相当于出现了发散式变化。在设计模式中发散式变化可以尝试通过策略模式或者桥接模式来解决。而如果单纯谈论重构这部分代码的解决方式,如果发生变化的两个方向自然地形成了先后次序(比如先折扣在做收银逻辑),就可以用拆分阶段将两者分开,两者间通过一个清晰地数据结构进行沟通。如果两个方向之间有更多的来回调用,就应该先创建适当的模块,然后用搬移函数把处理逻辑分开。如果函数内部混合了两类处理逻辑,应该先用提炼函数将其分开,然后再做搬移。如果模块是以类的形式定义的,就可以用提炼类来做拆分。
8.霰弹式修改(Shotgun Surgery)
霰弹式修改的含义是当每次遇到某种变化的时候,都必须在许多不同的雷内做出许多的小修改的情况,当需要修改的代码散步四处,不仅难以找到,而且也很容易错过某些重要修改。
这种情况下,你应该使用搬移函数和搬移字段把所有需要修改的代码放进同一个模块里。如果有 很多函数都在操作相似的数据,可以使用函数组合成类 。如果有些函数的功能是转化或者充实数据结构,可以使用函数组合成变换。如果一些函数的输出可以组合后提供给一段专门使用这些计算结果的逻辑,这种时候常常用得上拆分阶段。
面对霰弹式修改,一个常用的策略就是使用与内联(inline)相关的重构——如内联函数(115)或是内联类 ——把本不该分散的逻辑拽回一处。
完成内联之后,你可能会闻到过长函数或者过大的类的味道,不过你总可以用与提炼相关的重构手法将其拆解成更合理的小块。即便如此钟爱小型的函数和类,我们也并不担心在重构的过程中暂时创建一些较大的程序单元。
10.数据泥团(Data Clumps)
指的是当某些参数时常绑定出现的时候,可以尝试运用提炼类将他们提炼到同一个对象中。然后将注意力转移到函数签名上,运用引入参数对象或保持对象完整为它瘦身 。这么做的直接好处是可以将很多参数列表缩短,简化函数调用。
评判方法:删掉众多数据中的一项。如果这么做,其他数据有没有因而失去意义?如果它们不再有意义,这就是一个明确信号:你应该为它们产生一个新对象。
11.基本类型偏执(Primitive Obsession)
大多数编程环境都大量使用基本类型,即整数、浮点数和字符串等。一些库会引入一些小对象,如日期。但我们发现一个很有趣的现象:很多程序员不愿意创建对自己的问题域有用的基本类型,如钱、坐标、范围等。于是,我们看到 了把钱当作普通数字来计算的情况、计算物理量时无视单位(如把英寸与毫米相加)的情况以及大量类似if (a < upper && a > lower)这样的代码。
字符串是这种坏味道的最佳培养皿,比如,电话号码不只是一串字符。一个体面的类型,至少能包含一致的显示逻辑,在用户界面上需要显示时可以使用。 “用字符串来代表类似这样的数据”是如此常见的臭味,以至于人们给这类变量专门起了一个名字,叫它们“类字符串类型”(stringly typed)变量。
你可以运用以对象取代基本类型将原本单独存在的数据值替换为对象,从而走出传统的洞窟,进入炙手可热的对象世界。如果想要替换的数据值是控制条件行为的类型码,则可以运用以子类取代类型码加上以多态取代条件表达式的组合将它换掉。
如果你有一组总是同时出现的基本类型数据,这就是数 据泥团的征兆,应该运用提炼类和引入参数对象来处理。
以BigDecimal为例,对象类型的数据类型,在其出现之前,如果使用普遍的double或者float常常引起的精度问题。
舍入误差:
float
和double
是基于二进制的浮点数表示,在进行一些算术运算时可能会导致舍入误差。例如,对于货币计算,0.1 加上 0.2 可能不等于 0.3,而是一个非常接近但不完全等于 0.3 的值。精度损失:在进行多次计算或涉及到很小或很大的数值时,可能会逐渐积累精度损失,导致最终结果与预期的精确金钱数值有偏差。
17.过长的消息链(Message Chains)
发生的场景通常是用户向一个对象请求另一个对象,然后再向后者请求另一个对象,然后再请求另一个对象……当这样的请求过多就构成了过长的消息链。过长消息链会导致客户端代码将与查找过程中的导航结构紧密耦合,一旦对象间的关系发生改变,客户端不得不作出相应修改。
这时候应该使用隐藏委托关系。你可以在消息链的不同位置采用这种重构手法。理论上,你可以重构消息链上的所有对象,但这么做就会把所有中间对象都变成“中间人” 。通常更好的选择是:先观察消息链最终得到的对象是用来干什么的,看看能否以提炼函数把使用该对象的代码提炼到一个独立的函数中,再运用搬移函数把这个函数推入消息链。如果还有许多客户端代码需要访问链上的其他对象,同样添加一个函数来完成此事。
19.内幕交易(Insider Trading)
笔者提到软件开发者为了避免增加模块间的耦合常常会在模块间筑起高墙,反感模块之间大量交换数据。也提到了在实际开发中,一定的数据交换不可避免,并建议尽量减少这种情况,并把这种交换都放到明面上来。(消息中间件)
当两个模块频繁交换数据,可以采用搬移函数和搬移字段减少它们的私下交流。假如不可避免,可以尝试新建一个模块,把共用数据放在一个管理良好的地方,或者用隐藏委托关系,把另一个模块变成两者的中介。
继承常会造成密谋,因为子类对超类的了解总是超过后者的主观愿望。如果你觉得该让这个孩子独立生活了,请运用以委托取代子类或以委托取代超类让它离开继承体系。
21.异曲同工的类(Alternative Class with Different Interfaces)
使用类的好处之一就在于可以替换:今天用这个类,未来可以换成用另一个类。但只有当两个类的接口一致时,才能做这种替换。可以用改变函数声明将函数签名变得一致。但这往往还不够,请反复运用搬移函数将某些行为移入类中,直到两者的协议一致为止。如果搬移过程造成了重复代码,或许可运用提炼超类补偿一 下。
23.被拒绝的遗赠(Refused Bequest)
子类应该继承超类的函数和数据。如果它们不需要继承,我们应该怎么做。可以为这个子类新建一个兄弟类,再运用函数下移和字段下移把所有用不到的函数下推给那个兄弟类。这样一来超类就支持有所有子类共享的东西。但是,如果子类复用了超类的行为(实现),却又不愿意支持超类的接口,我们可以通过运用以委托取代子类或者以委托取代超类测地划清界限。
24.注释(Comments)
笔者多次提到要尽量减少注释的出现,尽量使函数/方法能够见名知意,通过一个个小函数/方法的命名来直接的表明这一函数的意图。比如利用提炼函数;或者在函数已经提炼后,依旧需要注释来解释其行为的,采用改变函数声明为它改名;如果需要注释说明某些系统的需求规格,尝试引入断言。
如果你不知道该做什么,这才是注释的良好运用时机。 除了用来记述将来的打算之外,注释还可以用来标记你并无 十足把握的区域。你可以在注释里写下自己“为什么做某某 事” 。这类信息可以帮助将来的修改者,尤其是那些健忘的家伙。
二、坏味道与重构手法速查表
代码的坏味道 | 常用重构手法 |
异曲同工的类 | 改变函数声明(124),搬移函数(198),提炼超类(375) |
注释 | 提炼函数(106),改变函数声明(124),引入断言(302) |
纯数据类 | 封装记录(162),移除设值函数(331),搬移函数(198),提炼函数 (106),拆分阶段(154) |
数据泥团 | 提炼类(182),引入参数对象(140),保持对象完整(319) |
发散式变化 | 拆分阶段(154),搬移函数(198),提炼函数(106),提炼类(182) |
重复代码 | 提炼函数(106),移动语句(223),函数上移(350) |
依恋情节 | 搬移函数(198),提炼函数(106) |
全局数据 | 封装变量(132) |
内幕交易 | 搬移函数(198),搬移字段(207),隐藏委托关系(189),以委托取 代子类(381),以委托取代超类(399) |
过大的类 | 提炼类(182),提炼超类(375),以子类取代类型码(362) |
冗赘的元素 | 内联函数(115),内联类(186),折叠继承体系(380) |
过长函数 | 提炼函数(106),以查询取代临时变量(178),引入参数对象 (140),保持对象完整(319),以命令取代函数(337),分解条件表达式 (260),以多态取代条件表达式(272),拆分循环(227) |
过长参数列表 | 以查询取代参数(324),保持对象完整(319),引入参数对象 (140),移除标记参数(314),函数组合成类(144) |
循环语句 | 以管道取代循环(231) |
过长的消息链 | 隐藏委托关系(189),提炼函数(106),搬移函数(198) |
中间人 | 移除中间人(192),内联函数(115),以委托取代超类(399),以委 托取代子类(381) |
可变数据 | 封装变量(132),拆分变量(240),移动语句(223),提炼函数 (106),将查询函数和修改函数分离(306),移除设值函数(331),以查 询取代派生变量(248),函数组合成类(144),函数组合成变换(149), 将引用对象改为值对象(252) |
神秘代码 | 改变函数声明(124),变量改名(137),字段改名(244) |
基本类型偏执 | 以对象取代基本类型(174),以子类取代类型码(362),以多态取代条 件表达式(272),提炼类(182),引入参数对象(140) |
被拒绝的遗赠 | 函数下移(359),字段下移(361),以委托取代子类(381),以委托 取代超类(399) |
重复的Switch | 以多态取代条件表达式(272) |
霰弹式修改 | 搬移函数(198),搬移字段(207),函数组合成类(144),函数组合 成变换(149),拆分阶段(154),内联函数(115),内联类(186) |
夸夸其谈通用性 | 折叠继承体系(380),内联函数(115),内联类(186),改变函数声 明(124),移除死代码(237) |
临时字段 | 提炼类(182),搬移函数(198),引入特例(289) |