You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
// base price(底价)、quantity discount(折扣)、shipping(运费)functionprice(order){// price is base price - quantity discount + shippingreturnorder.quantity*order.itemPrice-Math.max(0,order.quantity-500)*order.itemPrice*0.05+Math.min(order.quantity*order.itemPrice*0.1,100);}
重构这本书中,介绍的一些技巧、方法,对改善即有的代码设计有一定的帮助。另一方面,掌握了这些重构方法之后对你的编码能力也会有一个显著的提升。
孟子的《尽心章句下》中有这样一句话 :“尽信书,不如无书”,意思是读者要有独立思考精神,读书时应该加以分析,辩证的看待问题。在重构这本书中,介绍的重构方法多达将近上百种,不要一上来就被这么多的介绍给吓的退缩,找到适合于自己的才是最重要的,如果你是一个拥有多年编程经验的开发者,更要带着自己的理解去阅读。
为何重构
代码结构的流失具有累积效应
当人们只为短期目的修改代码时,可能并没有很好理解架构的整体设计,当越难看出代码所表达的设计意图时就越难保护其设计,代码结构也会逐渐流失。
例如,产品经理告诉你,现在客户提出了一个新需求,很着急,希望尽快完成并上线。做这块的人,也许是一个并不了解该模块原有功能设计的人,也许是一个老队友,无奈于 “时间紧”、“任务重” 怎么办呢?顾不上先设计在开发了,先完成再说,一旦有第一次,就很有可能会产生第二次,周而复始,会发现代码结构越来越难以维护。
我想这种例子,在你身边也许经历过,当面对一些遗留系统的代码时,可能心里也会想 “这写的都是什么啊!”
编程的对象只是计算机吗
我们编写代码,很大程度上都在与计算机对话,告诉它我们的代码指令以及该做出何种响应,那么我们编程的对象只是计算机吗?
不,这里往往忽略另外一个对象,我们编写的代码除了计算机外,还有其他读者。因为在一段时间后,会有其他编程人员来阅读这段代码并作出修改,我们每个人都可能成为这个未来的读者,这个是我们系统得以长久维护的一个重要对象。
为什么之前我没有考虑到
在每一次的产品新版本迭代时,可能会遇到以前的结构不能满足,需要做一些调整,有时我们会吐槽为什么之前没考虑到呢?现在却要花时间调整它。
在开始代码之前,我们需要先完成软件的设计与架构,这很重要但也不要 “过度设计”,软件永远不应该被视为 “完成”,每当有新功能时,软件就应该做出相应的改变,最后会发现重构是一个长期的过程。
重构意义
内部质量良好的模块划分,我们只需要了解代码库的一小部分,就可以容易的找出要修改的地方,是可以提高编程速度的。
当遇到一段糟糕的代码,你可能需要花费更多的时间去思考如何将新功能加入现有的代码库,一不小心还容易引入 bug,修复起来也困难,这份负担会拖累你的开发进度,并且以后的新功能也会越来越难以加入,最后实在难以维护 “我们就重构它吧”。
重构的意义在于让代码有着一个良好的模块划分,让未来的读者更易于理解、提高之后的开发效率。
何时重构
“重构风险太大,可能引入 bug”,如果你有这个担忧,是正常的,重构的目的是为了让代码更易于理解,但也不能破坏现有的运行状态。
大多数情况下,我们想重构,得先有可以自测试的代码,哪怕是一个看似很小的改动,这会让重构更加可靠,这也是为什么在开发中我们会强调单元测试的重要性。因此,一开始时,团队有必要投入一定的精力和时间在一些测试工作上,这对于新功能添加也会多一层安全保证。
关于重构,一部分人认为需要安排一段时间来专门做这件事,这个投入成本是很大的,需要团队达成共识,在大多数情况下你的项目也不会给你计划这么多时间允许你做。有这样一句话:“种一棵树最好的时间是十年前,其次是现在”,同样大多数的重构也可在添加新功能、修复 bug 时去做,重构不一定是推翻重来。
甄别坏代码
书中这一部分称为代码的坏味道,这些是我们在编程中需要警惕的一些规则,很容易造成代码结构流失,不易后期维护。
a
、b
这种随意的命名要避免。当你花了很长时间还是想不到一个好的名字,也许背后隐藏着更深的设计问题。重构方法
重构方法是本书的重点,每个重构方法都围绕动机、做法、范例三部分进行介绍,书中你会看到作者的做法很谨慎,分小步一点一点重构、验证,以至于有时会感觉些许 “啰嗦”,但这种小步前进确实会减少出错,不可否认它是一种好的工作方式。
下文我会摘选一些认为在以往编程经历中对自己有帮助的代码编写方法。
提炼函数
“提炼函数” 是最常用的重构之一,在一些面向对象编程语言中是 “提炼方法”。何时提炼?有三种不同的观点:
这里我们主要讨论 “将意图与实现分开”,如果你需要花时间浏览一段代码才能弄清楚它的意图时,那么就应该将其提炼到一个函数中,下次再读到这段代码时,根据调用的函数名一眼即可看到函数的用途,无需关心函数体内的具体实现是怎么样的。
优化前示例:
优化后示例:
创建一个新函数提炼我们的代码,根据函数的意图(做什么)来对它命名。原先需要在一段代码的顶部写一段注释描述它做什么,经过函数提炼之后,通过调用函数名称已经知道了它的意图,此时可以不用注释。
这里有个很多人都容易犯难的问题:“如何更好的命名?”,一个改进函数名称的好办法是:“先写一句注释描述函数的用途,再把这句注释变为函数名称”,变量命名也同样如此。但是英语也是一部分人的硬伤,总不能用中文给函数命名吧,建议先用一句简洁的中文描述,之后在借助一些工具翻译为英文,“这也不失为提升英语的一种方式”。
内联函数
“内联函数” 是 “提炼函数” 的反向重构,一个函数的内部实现和它的函数名称同样清晰可读,这种情况下直接使用其中的代码,去掉这个函数。
优化前示例:
优化后示例:
提升变量
当一个表达式非常复杂而难以阅读时,分解表达式、提炼变量也许是个不错的选择。
优化前示例:
优化后示例:
在一个函数内部,变量能给表达式提供有意义的名字,如上例所示,看起来清晰明了,函数内的注释也可以删除掉了。
内联变量
“内联变量” 是 “提炼变量” 的反向重构。变量是个好东西,但有时当表达式更具表现力时,无需在对表达式提炼变量。
优化前示例:
优化后示例:
封装变量
对于所有可变的数据,如果作用域超出单个函数,就可考虑将其封装为一个函数进行访问,优点是对变量的修改不会散落在各个地方,可监控数据的变化情况,添加一些修改前的验证或后续逻辑处理也是很方便的。数据作用域越大,封装就越重要,面向对象编程中强调数据的私有(private)背后也是同样的原理。
上面代码带来两个隐患:
以下是优化后示例:
替换算法
“重构” 可以把一些复杂的东西分解为较简单的小块。随着对问题的理解,会发现在原先做法之外,有更简单的解决方案,此时就可改变原先的算法。
拆分循环
经常会看到一些身兼多职的循环,只因可以只循环一次。拆分循环能让每个循环更容易理解,每次修改时也只需要理解修改的那块代码行为。
但这会让许多程序员感到不安的是它会迫使程序执行多次循环。在 “重构” 本书中的建议是先重构让代码结构变得清晰,再进行下一步优化。实际情况下即使处理的列表数据更多一些,循环本身也很少成为性能瓶颈。
优化前示例:
先移动语句微调代码顺序,让存在关联的东西一起出现,可以使代码更容易理解。
进一步优化,还可以提炼函数,将每个循环提炼到单独的函数中,这要根据实际的情况进行选择。以上示例相对简单, 还可以使用 “以管道取代循环” 做进一步重构,如今的编程语言都提供了更好的语言结构来处理迭代结果,不一定非要使用循环。集合管道就是这样的一种技术,允许使用一组运算来描述集合的迭代过程,JavaScript 中这类运算很多,常见的非
map
和filter
莫属,还有reduce
、find
、some
等。重新组织数据
数据结构对于帮助阅读者理解很重要,在本书中介绍了一种专门的 “重构” 手法:“重新组织数据”。
将一个变量用于多个不同的用途,这是催生混乱和 bug 的温床,遇到这种情况可通过 “拆分变量” 方式将不同的用途分开,一个变量只做一件事,同时记得给变量起一个有意义的名字。这个问题看似简单,也是一个常犯的错误。
可变数据是软件中最大的错误源头之一,应尽可能把可变数据限制在最小范围。例如,有些变量可以很容易的计算出来,此时就可考虑去掉这些变量,避免原数据更改时忘记更新派生变量。
简化条件逻辑
条件逻辑占据了程序的大部分,如果处理不当也会加大程序的复杂度。一个复杂的条件逻辑,应尽可能的去简化它,提高代码的可读性。这个重构手法称为 “简化条件逻辑”。
例如,要计算购买某样商品的总价(总价 = 数量 * 单价),而该商品在夏季单价计算同其它季节存在差别。下面是优化前代码示例:
当条件逻辑复杂不易理解时,可以采用 “分解条件表达式” 方式,提炼条件判断为一个新的函数并取一个有意义的名字。根据实际情况也可以提炼为一个变量,包括每一个条件分支都可以做提炼。
当发现有一串条件检查,最终行为都一致,这种情况下,应该使用 “逻辑或” 或 “逻辑与” 将它们合并为一个表达式。
使用条件逻辑,还应注意不必要的深层 if...else 嵌套,尽可能的扁平化处理,当该条件为真时立刻从函数中返回。这样能保持代码结构的清晰整洁,一口气就能看得出该函数是做什么的。
在一些复杂的条件逻辑编程中除了
if...else
你可能还会见到过使用**switch...case**
,在一组数据类型中,根据每个类型处理各自的条件逻辑,对于这种情况,“重构” 这本书中提出了一种 “以多态取代条件表达式” 的重构手法。多态是面向对象编程中的一个关键特性,这种重构手法是针对 switch 语句中的每个分支逻辑创建一个类,用多态这一特性来承载各个类型特有的行为。
这种重构手法在笔者以往的项目中是没有使用过的,但也不失为一种有趣的思路,可以探索下。
分离函数副作用
任何有返回值的函数,都不应该有看得到的副作用。保持了修改和查询的分离,当某个函数只返回一个值时,可以在任意地方调用它,后续对该函数的测试也会变的容易多。
以下示例,检查一群人中是否混进了恶棍(miscreant),如果是则返回恶棍名字并拉响警报。
工厂函数取代构造函数
当调用者需要一个新对象时,很多编程语言都提供了构造函数专门用于对象的初始化。也不是所有的场景都用构造函数就是好的,例如工厂函数相比构造函数有更多的灵活性。工厂函数的实现内部可以调用构造函数,也可以是其它的实现方式。
总结
以前当谈及 “重构” 时,想到的会是我们要推翻重来吗?读完本书后改了这一观点,重构不是推倒重来,它可以在不改变外部条件的情况下,小步前进,有条不紊的改善既有代码的设计。
如果你是一个经验丰富的程序,书中的一些重构方法会让你感到熟悉,例如,文中笔者列举的一些重构方法有些平常也在这样做,只是没想过原来它还可以有这样一个名字,当看到这些示例后也更坚定了自己的一些做法。也会有些不是很理解,它也需要你带着实践去理解。在不同的时间、环境下阅读都会带来不一样的启发。
The text was updated successfully, but these errors were encountered: