如果再复杂的代码结构中使用诸如:a、b、a1、b1等等大量随意命名的参数、变量、函数命名,则过一段时间以后,代码的作者也不一定明白这些变量、参数的含义。
整洁代码最重要的一环就是好的名字,所以我们要深思熟虑如何给函数、模块、变量和类命名,使它们能清晰地表明自己的功能和用法。
命名是编程中最难的两件事之一正因为如此,改名可能是最常用的重构手法,包括改变函数声明(124)(用于给函数改名)、变量改名(137)、字段改名(244)等。
如果我们想不出一个好名字,说明背后很可能潜藏着更深的设计问题。
如果在一个以上的地点看到相同的代码结构,那么可以肯定:设法将它们合而为一,程序会变得更好。
程序中出现重复代码的场景:
如果要修改重复代码,我们必须找出所有的副本来修改。
据我们的经验,活得最长、最好的程序,其中的函数都比较短。从进入编程年代开始,程序员们就已认识到:函数越长,就越难理解。小函数易于理解的关键还是在于良好的命名。如果你能给函数起个好名字,阅读代码的人就可以通过名字了解函数的作用,根本不必去看其中写了些什么。
百分之九十九的场合里,要把函数变短,只需使用提炼函数(106)。找到函数中适合集中在一起的部分,将它们提炼出来形成一个新函数。
如果函数内有大量的参数和临时变量,它们会对你的函数提炼形成阻碍。如果尝试运用提炼函数(106),最终就会把许多参数传递给被提炼出来的新函数,导致可读性几乎没有任何提升。此时,可以经常运用以查询取代临时变量(178)来消除这些临时元素。引入参数对象(140)和保持对象完整(319)则可以将过长的参数列表变得更简洁一些。
如何确定该提炼哪一段代码呢?一个很好的技巧是:寻找注释。它们通常能指出代码用途和实现手法之间的语义距离。如果代码前方有一行注释,就是在提醒:可以将这段代码替换成一个函数,而且可以在注释的基础上给这个函数命名。
条件表达式和循环常常也是提炼的信号。可以使用分解条件表达式(260)处理条件表达式。对于庞大的switch语句,其中的每个分支都应该通过提炼函数(106)变成独立的函数调用。如果有多个switch语句基于同一个条件进行分支选择,就应该使用以多态取代条件表达式(272)。
至于循环,应该将循环和循环内的代码提炼到一个独立的函数中。如果发现提炼出的循环很难命名,可能是因为其中做了几件不同的事。如果是这种情况,可以使用拆分循环(227)将其拆分成各自独立的任务。
如果函数传入的参数过多,尽量不要使用全局变量(全局数据很有可能变成邪恶的东西,因为你不知道什么时候被修改)
如果函数有几项参数总是同时出现,可以用引入参数对象(140)将其合并成一个对象。如果某个参数被用作区分函数行为的标记(flag),可以使用移除标记参数(314)。
使用类可以有效地缩短参数列表。如果多个函数有同样的几个参数,引入一个类就尤为有意义。可以使用函数组合成类(144),将这些共同的参数变成这个类的字段。
全局数据的问题在于,从代码库的任何一个角落都可以修改它,而且没有任何机制可以探测出到底哪段代码做出了修改。
首要的防御手段是封装变量(132),把全局数据用一个函数包装起来,至少你就能看见修改它的地方,并开始控制对它的访问。最好将这个函数(及其封装的数据)搬移到一个类或模块中,只允许模块内的代码使用它,从而尽量控制其作用域。
有少量的全局数据或许无妨,但数量越多,处理的难度就会指数上升。即便只是少量的数据,我们也将它封装起来,这是在软件演进过程中应对变化的关键所在。
对数据的修改经常导致出乎意料的结果和难以发现的bug。在一处更新数据,却没有意识到软件中的另一处期望着完全不同的数据。
函数式编程——完全建立在“数据永不改变”的概念基础上:如果要更新一个数据结构,就返回一份新的数据副本,旧的数据仍保持不变。
可以用封装变量(132)来确保所有数据更新操作都通过很少几个函数来进行,使其更容易监控和演进。如果一个变量在不同时候被用于存储不同的东西,可以使用拆分变量(240)将其拆分为各自不同用途的变量,从而避免危险的更新操作。使用移动语句(223)和提炼函数(106)尽量把逻辑从处理更新操作的代码中搬移出来,将没有副作用的代码与执行数据更新操作的代码分开。
如果可变数据的值能在其他地方计算出来,它会造成困扰和bug,而且毫无必要。解决办法很简单,使用以查询取代派生变量(248)即可。
如果变量作用域只有几行代码,即使其中的数据可变,也不是什么大问题;但随着变量作用域的扩展,风险也随之增大。可以用函数组合成类(144)或者函数组合成变换(149)来限制需要对变量进行修改的代码量。如果一个变量在其内部结构中包含了数据,通常最好不要直接修改其中的数据,而是用将引用对象改为值对象(252)令其直接替换整个数据结构。
如果某个模块经常因为不同的原因在不同的方向上发生变化,发散式变化就出现了;程序刚开发出来还在随着软件系统的能力不断演进时,如果存在这类模块,那么上下文边界通常不是那么清晰。
如果发生变化的两个方向自然地形成了先后次序就可以用拆分阶段(154)将两者分开,两者之间通过一个清晰的数据结构进行沟通。如果两个方向之间有更多的来回调用,就应该先创建适当的模块,然后用搬移函数(198)把处理逻辑分开。如果函数内部混合了两类处理逻辑,应该先用提炼函数(106)将其分开,然后再做搬移。如果模块是以类的形式定义的,就可以用提炼类(182)来做拆分。
霰弹式修改类似于发散式变化,但又恰恰相反:如果每遇到某种变化,都必须在许多不同的类内做出许多小修改。如果需要修改的代码散布四处,你不但很难找到它们,也很容易错过某个重要的修改。
使用搬移函数(198)和搬移字段(207)把所有需要修改的代码放进同一个模块里。如果有很多函数都在操作相似的数据,可以使用函数组合成类(144)。如果有些函数的功能是转化或者充实数据结构,可以使用函数组合成变换(149)。如果一些函数的输出可以组合后提供给一段专门使用这些计算结果的逻辑,这种时候常常用得上拆分阶段(154)。
面对霰弹式修改,一个常用的策略就是使用与内联(inline)相关的重构——如内联函数(115)或是内联类(186)——把本不该分散的逻辑拽回一处。
所谓模块化,就是力求将代码分出区域,最大化区域内部的交互、最小化跨区域的交互。如果一个函数跟另一个模块中的函数或者数据交流格外频繁,远胜于在自己所处模块内部的交流,这就是依恋情结的典型情况。
一个函数往往会用到几个模块的功能,那么它究竟该被置于何处呢?我们的原则是:判断哪个模块拥有的此函数使用的数据最多,然后就把这个函数和那些数据摆在一起。
设计模式中策略(Strategy)模式、访问者(Visitor)模式是为了对抗发散式变化。最根本的原则是:将总是一起变化的东西放在一块儿。数据和引用这些数据的行为总是一起变化的,但也有例外。如果例外出现,我们就搬移那些行为,保持变化只在一地发生。
数据项就像小孩子,喜欢成群结队地待在一块儿。两个类中相同的字段、许多函数签名中相同的参数。这些总是绑在一起出现的数据真应该拥有属于它们自己的对象。
找出这些数据以字段形式出现的地方,运用提炼类(182)将它们提炼到一个独立对象中。然后将注意力转移到函数签名上,运用引入参数对象(140)或保持对象完整(319)为它瘦身。这么做的直接好处是可以将很多参数列表缩短,简化函数调用。
大多数编程环境都大量使用基本类型,即整数、浮点数和字符串等。很多程序员不愿意创建对自己的问题域有用的基本类型,如钱、坐标、范围等。
字符串是这种坏味道的最佳培养皿,比如,电话号码不只是一串字符。一个体面的类型,至少能包含一致的显示逻辑,在用户界面上需要显示时可以使用。
可以运用以对象取代基本类型(174)将原本单独存在的数据值替换为对象,从而走出传统的洞窟,进入炙手可热的对象世界。如果想要替换的数据值是控制条件行为的类型码,则可以运用以子类取代类型码(362)加上以多态取代条件表达式(272)的组合将它换掉。
很多语言支持更复杂的switch语句在不同的地方反复使用同样的switch逻辑(可能是以switch/case语句的形式,也可能是以连续的if/else语句的形式)。
重复的switch的问题在于:每当你想增加一个选择分支时,必须找到所有的switch,并逐一更新。多态给了我们对抗这种黑暗力量的武器,使我们得到更优雅的代码库。