良药与毒药的区别在于剂量,当你感觉到代码中右一些坏味道,开始腐烂,但是你却并不知道到底哪里出了问题,何时去拯救?
坏味道感觉一: 神秘命名
整洁代码最重要的一环就是好的名字,所以我们会深思熟虑如何给函数、模块、变量和类命名,使它们能清晰地表明自己的功能和用法 “命名是编程中最难的两件事之一。正因为如此,改名可能是最常用的重构手法,包括改变函数声明(用于给函数改名)、变量改名、字段 改名等。很多人经常不愿意给程序元素改名,觉得不值得费这个劲,但好的名字能节省未来用在猜谜上的大把时间。
改名不仅仅是修改名字而已。如果你想不出一个好名字,说明背后很可能潜藏着更深的设计问题。为一个恼人的名字所付出的纠结,常常能推动我们对代码进行精简。
坏味道感觉二:重复代码
最单纯的重复代码就是“同一个类的两个函数含有相同的表达式”。这时候你需要做的就是采用提炼函数提炼出重复的代码,然后让这两个地点都调用被提炼出来的那一段代码。
如果重复代码只是相似而不是完全相同,请首先尝试用移动语句重组代码顺序,把相似的部分放在一起以便提炼。如果重复的代码段位于同一个超类的不同子类中,可以使用函数上移来避免在两个子类之间互相调用。
坏味道感觉三: 过长函数
据我们的经验,活得最长、最好的程序,其中的函数都比较短。初次接触到这种代码库的程序员常常会觉得“计算都没有发生”——程序里满是无穷无尽的委托调用。但和这样的程序共处几年之后,你就会明白这些小函数的价值所在。间接性带来的好处——更好的阐释力、更易于分享、更多的选择——都是由小函数来支持的。
早在编程的洪荒年代,程序员们就已认识到:函数越长,就越难理解。在早期的编程语言中,子程序调用需要额外开“销,这使得人们不太乐意使用小函数。现代编程语言几乎已经完全免除了进程内的函数调用开销。固然,小函数也会给代码的阅读者带来一些负担,因为你必须经常切换上下文,才能看明白函数在做什么。但现代的开发环境让你可以在函数的调用处与声明处之间快速跳转,或是同时看到这两处,让你根本不用来回跳转。不过说到底,让小函数易于理解的关键还是在于良好的命名。如果你能给函数起个好名字,阅读代码的人就可以通过名字了解函数的作用,根本不必去看其中写了些什么。
最终的效果是:你应该更积极地分解函数。我们遵循这样一条原则:每当感觉需要以注释来说明点什么的时候,我们就把需要说明的东西写进一个独立函数中,并以其用途(而非实现手法)命名。我们可以对一组甚至短短一行代码做这件事。哪怕替换后的函数调用动作比函数自身还长,只要函数名称能够解释其用途,我们也该毫不犹豫地那么做。关键不在于函数的长度,而在于函数“做什么”和“如何做”之间的语义距离。
百分之九十九的场合里,要把函数变短,只需使用提炼函数(106)。找到函数中适合集中在一起的部分,将它们提炼出来形成一个新函数。如果函数内有大量的参数和临时变量,它们会对你的函数提炼形成阻碍。如果你尝试运用提炼函数(106),最终就会把许多参数传递给被提炼出来的新函数,导致可读性几乎没有任何提升。此时,你可以经常运用以查询取代临时变量(178)来消除这些临时元素。引入参数对象(140)和保持对象完整(319)则可以将过长的参数列表变得更简洁一些。
如果你已经这么做了,仍然有太多临时变量和参数,那就应该使出我们的杀手锏——以命令取代函数(337) 如何确定该提炼哪一段代码呢?一个很好的技巧是:寻找注释。它们通常能指出代码用途和实现手法之间的语义距离。如果代码前方有一行注释,就是在提醒你:可以将这段代码替换成一个函数,而且可以在注释的基础上给这个函数命名。就算只有一行代码,如果它需要以注释来说明,那也值得将它提炼到独立函数中去。
条件表达式和循环常常也是提炼的信号。你可以使用分解条件表达式(260)处理条件表达式。对于庞大的switch语句,其中的每个分支都应该通过提炼函数(106)变成独立的函数“调用。如果有多个switch语句基于同一个条件进行分支选择,就应该使用以多态取代条件表达式(272)至于循环,你应该将循环和循环内的代码提炼到一个独立的函数中。如果你发现提炼出的循环很难命名,可能是因为其中做了几件不同的事。如果是这种情况,请勇敢地使用拆分循环(227)将其拆分成各自独立的任务。
坏味道感觉四:过长参数列表
刚开始学习编程的时候,老师教我们:把函数所需的所有东西都以参数的形式传递进去。这可以理解,因为除此之外就只能选择全局数据,而全局数据很快就会变成邪恶的东西。但过长的参数列表本身也经常令人迷惑。如果可以向某个参数发起查询而获得另一个参数的值,那么就可以使用以查询取代参数(324)去掉这第二个参数。如果你发现自己正在从现有的数据结构中抽出很多数据项,就可以考虑使用保持对象完整(319)手法,直接传入原来的数据结构。如果有几项参数总是同时出现,可以用引入参数对象(140)将其合并成一个对象。如果某个参数被用作区分函数行为的标记(flag),可以使用移除标记参数(314)。
使用类可以有效地缩短参数列表。如果多个函数有同样的几个参数,引入一个类就尤为有意义。你可以使用函数组合成类(144),将这些共同的参数变成这个类的字段。如果戴上函数式编程的帽子,我们会说,这个重构过程创造了一组部分应用函数(partially applied function。
坏味道感觉五: 全局数据
刚开始学软件开发时,我们就听说过关于全局数据的惊悚故事——它们是如何被来自地狱第四层的恶魔发明出来,胆敢使用它们的程序员如今在何处安息。就算这些烈焰与硫黄的故事不那么可信,全局数据仍然是最刺鼻的坏味道之一。全局数据的问题在于,从代码库的任何一个角落都可以修改它,而且没有任何机制可以探测出到底哪段代码做出了修改。一次又一次,全局数据造成了那些诡异的bug,而问题的根源却在遥远的别处,想要找到出错的代码难于登天。全局数据最显而易见的形式就是全局变量,但类变量和单例(singleton)也有这样的问题。首要的防御手段是封装变量(132),每当我们看到可能被各处的代码污染的数据,这总是我们应对的第一招。你把全局数据用一个函数包装起来,至少你就能看见修改它的地方,并开始控制对它的访问。随后,最好将这个函数(及其封装的数据)搬移到一个类或模块中,只允许模块内的代码使用它,从而尽量控制其作用域。“可以被修改的全局数据尤其可憎。如果能保证在程序启动之后就不再修改,这样的全局数据还算相对安全,不过得有编程语言提供这样的保证才行。全局数据印证了帕拉塞尔斯的格言:良药与毒药的区别在于剂量。有少量的全局数据或许无妨,但数量越多,处理的难度就会指数上升。即便只是少量的数据,我们也愿意将它封装起来,这是在软件演进过程中应对变化的关键所在。
坏味道感觉六: 可变数据
对数据的修改经常导致出乎意料的结果和难以发现的bug。我在一处更新数据,却没有意识到软件中的另一处期望着完全不同的数据,于是一个功能失效了——如果故障只在很罕见的情况下发生,要找出故障原因就会更加困难。因此,有一整个软件开发流派——函数式编程——完全建立在“数据永不改变”的概念基础上:如果要更新一个数据结构,就返回一份新的数据副本,旧的数据仍保持不变。
不过这样的编程语言仍然相对小众,大多数程序员使用的编程语言还是允许修改变量值的。即便如此,我们也不应该忽视不可变性带来的优势——仍然有很多办法可以用于约束对数据的更新,降低其风险。可以用封装变量(132)来确保所有数据更新操作都通过很少几个函数来进行,使其更容易监控和演进。如果一个变量在不同时候被用于存储不同的东西,可以使用拆分变量(240)将其拆分为各自不同用途的变量,从而避免危险的更“操作。使用移动语句(223)和提炼函数(106)尽量把逻辑从处理更新操作的代码中搬移出来,将没有副作用的代码与执行数据更新操作的代码分开。设计API时,可以使用将查询函数和修改函数分离(306)确保调用者不会调到有副作用的代码,除非他们真的需要更新数据。我们还乐于尽早使用移除设值函数(331)——有时只是把设值函数的使用者找出来看看,就能帮我们发现缩小变量作用域的机会。
如果可变数据的值能在其他地方计算出来,这就是一个特别刺鼻的坏味道。它不仅会造成困扰、bug和加班,而且毫无必要。消除这种坏味道的办法很简单,使用以查询取代派生变量(248)即可。 如果变量作用域只有几行代码,即使其中的数据可变,也不是什么大问题;但随着变量作用域的扩展,风险也随之增大。可以用函数组合成类(144)或者函数组合成变换(149)来限制需要对变量进行修改的代码量。如果一个变量在其内部结构中包含了数据,通常最好不要直接修改其中的数据,而是用将引用对象改为值对象(252)令其直接替换整个数据结构。
坏味道感觉七:发散式变化
我们希望软件能够更容易被修改——毕竟软件本来就该是“软”的。一旦需要修改,我们希望能够跳到系统的某一点,只在该处做修改。如果不能做到这一点,你就嗅出两种紧密相关的刺鼻味道中的一种了。如果某个模块经常因为不同的原因在不同的方向上发生变化,发散式变化就出现了。当你看着一个类说:“呃,如果新加入一个数据库,我必须修改这3个函数;如果新出现一种金融工具,我必须修改这4个函数。”这就是发散式变化的征兆。数据库交互和金融逻辑处理是两个不同的上下文,将它们分别搬移到各自独立的模块中,能让程序变得更好:每当要对某个上下文做修改时,我们只需要理解这个上下文,而不必操心另一个。“每次只关心一个上下文”这一点一直很重要,在如今这个信息爆炸、脑容量不够用的年代就愈发紧要。往往只有在加入新数据库或新金融工具后,你才能发现这个坏味道。在程序刚开发出来还在随着软件系统的能力不断演进时,上下文边界通常不是那么清晰。如果发生变化的两个方向自然地形成了先后次序(比如说,先从数据库取出数据,再对其进行金融逻辑处理),就可以用拆分阶段(154)将两者分开,两者之间通过一个清晰的数据结构进行沟通。如果两个方向之间有更多的来回调用,就应该先创建适当的模块,然后用搬移函数(198)把处理逻辑分开。如果函数内部混合了两类处理逻辑,应该先用提炼函数(106)将其分开,然后再做搬移。如果模块是以类的形式定义的,就可以用提炼类(182)来做拆分。
坏味道感觉八:霰弹式修改
霰弹式修改类似于发散式变化,但又恰恰相反。如果每遇到某种变化,你都必须在许多不同的类内做出许多小修改,你所面临的坏味道就是霰弹式修改。如果需要修改的代码散布四处,你不但很难找到它们,也很容易错过某个重要的修改。
这种情况下,你应该使用搬移函数(198)和搬移字段(207)把所有需要修改的代码放进同一个模块里。如果有很多函数都在操作相似的数据,可以使用函数组合成类(144)。如果有些函数的功能是转化或者充实数据结构,可以使用函数组合成变换(149)。如果一些函数的输出可以组合后提供给一段专门使用这些计算结果的逻辑,这种时候常常用得上拆分阶段(154)。面对霰弹式修改,一个常用的策略就是使用与内联(inline)相关的重构——如内联函数(115)或是内联类(186)——把本不该分散的逻辑拽回一处。完成内联之后,你可能会闻到过长函数或者过大的类的味道,不过你总可以用与提炼相关的重构手法将其拆解成更合理的小块。即便如此钟爱小型的函数和类,我们也并不担心在重构的过程中暂时创建一些较大的程序单元。
坏味道感觉九: 依恋情节
所谓模块化,就是力求将代码分出区域,最大化区域内部的交互、最小化跨区域的交互。但有时你会发现,一个函数跟另一个模块中的函数或者数据交流格外频繁,远胜于在自己所处模块内部的交流,这就是依恋情结的典型情况。无数次经验里,我们看到某个函数为了计算某个值,从另一个对象那儿调用几乎半打的取值函数。疗法显而易见:这个函数想跟这些数据待在一起,那就使用搬移函数(198)把它移过去。有时候,函数中只有一部分受这种依恋之苦,这时候应该使用提炼函数(106)把这一部分提炼到独立的函数中,再使用搬移函数(198)带它去它的梦想家园。当然,并非所有情况都这么简单。一个函数往往会用到几个模块的功能,那么它究竟该被置于何处呢?“我们的原则是:判断哪个模块拥有的此函数使用的数据最多,然后就把这个函数和那些数据摆在一起。如果先以提炼函数(106)将这个函数分解为数个较小的函数并分别置放于不同地点,上述步骤也就比较容易完成了。立刻跳入我的脑海,Kent Beck的Self Delegation模式[Beck SBPP]也在此列。使用这些模式是为了对抗发散式变化这一坏味道。最根本的原则是:将总是一起变化的东西放在一块儿。数据和引用这些数据的行为总是一起变化的,但也有例外。如果例外出现,我们就搬移那些行为,保持变化只在一地发生。策略模式和和访问者模式使你得以轻松修改函数的行为,因为它们将少量需被覆写的行为隔离开来——当然也付出了“多一层间接性”的代价。
结语
如果你觉得此文对你有一丁点帮助,点个赞,鼓励一下作者。
如果你想和获得React交流的机会,和数万React 开发者一起交流学习**,获得100本精选电子书籍PDF 点击这里** ---> React沸点
如果你是有其他目的的,别加我,我不想跟你交朋友,我只想简简单单学习前端,不想搞一些有的没的!!!