坏味道代码
- 看不懂的命名(Mysterious Name)
- 重复的代码(Duplicated Code)
- 过长的函数(Long Function)
- 全局数据(Global Data)
- 可变数据(Mutable Data)
平衡: 性能与代码可读性之间的平衡问题,我的思考与作者的意向
小函数得有个好名字才行 PS: 那么命名不好的情况下,就用注释代替其作用吧
提炼函数
何时应该把代码放进独立的函数?
动机
- 将意图与实现分开:如果你需要花时间浏览一段代码才能弄清它到底在干什么,那么就应将其提炼到一个函数中
其他
- 一个函数应该能在一屏中显示
- 只要被用过不止一次的代码,就应该单独放进一个函数
- 只用过一次的代码则保持内联(inline)的状态
目的 以后再读到这段代码时,你一眼就能看到函数的用途,大多数时候根本不需要关心函数如何达成其用途(这是函数体内干的事)。
做法
- 创造一个新函数,根据这个函数的意图来对它命名(以它“做什么”来命名,而 不是以它“怎样做”命名)。
- 如果编程语言支持嵌套函数,就把新函数嵌套在源函数里,这能减少后面需要处理 的超出作用域的变量个数
内联函数(Inline Function)
动机
我手上有一群组织不甚合理的函数。可以将它们都内联到一个大型函数中,再以我喜欢的方式重新提炼出小函数。
目的
将不合理的函数代码初步处理成可以方便进行重构的代码
做法
- 检查函数,确定它不具多态性。 Tip 如果该函数属于一个类,并且有子类继承了这个函数,那么就无法内联。
- 找出这个函数的所有调用点。
- 将这个函数的所有调用点都替换为函数本体。
- 删除该函数的定义。
对于递归调用、多返回点、内联至另一个对象中而该对象并无访问函数等复杂情况:如果你遇到了这样的复杂情况,就不应该使用这个重构手法。
提炼变量(Extract Variable)
动机
表达式有可能非常复杂而难以阅读。这种情况下,局部变量可以帮助我们将表达式分解为比较容易管理的形式。
目的
这样的变量在调试时也很方便,它们给调试器和打印语句提供了便利的抓手,在阅读代码的时候也会容易理解其作用。它们提供了合适的上下文,方便分享相关的逻辑和数 据。在如此简单的情况下,这方面的好处还不太明显;但在一个更大的类当中,如果能找出可以共用的行为,赋予它独立的概念抽象,给它起一个好名字,对于使用对象的人会很有帮助。
做法
- 确认要提炼的表达式没有副作用。
- 声明一个不可修改的变量,把你想要提炼的表达式复制一份,以该表达式的结 果值给这个变量赋值。
- 用这个新变量取代原来的表达式。
案例
1function price(order) {2 return (3 order.quantity * order.itemPrice -4 Math.max(0, order.quantity - 500) * order.itemPrice * 0.05 +5 Math.min(order.quantity * order.itemPrice * 0.1, 100)6 );7}1function price(order) {2 const basePrice = order.quantity * order.itemPrice;3 const quantityDiscount = Math.max(0, order.quantity - 500) * order.itemPrice * 0.05;4 const shipping = Math.min(basePrice * 0.1, 100);5 return basePrice - quantityDiscount + shipping;6}内联变量(Inline Variable)
动机
有些时候,变量可能会妨碍重 构附近的代码。若果真如此,就应该通过内联的手法消除变量。
做法
- 检查确认变量赋值语句的右侧表达式没有副作用。
- 如果变量没有被声明为不可修改,先将其变为不可修改,并执行测试。 Tip 这是为了确保该变量只被赋值一次。
- 找到第一处使用该变量的地方,将其替换为直接使用赋值语句的右侧表达式。
- 测试。
- 重复前面两步,逐一替换其他所有使用该变量的地方。
- 删除该变量的声明点和赋值语句。
- 测试
改变函数声明(Change Function Declaration)
函数改名 修改函数签名
动机
函数是我们将程序拆分成小块的主要方式。函数声明则展现了如何将这些小块组合 在一起工作,系统的好 坏很大程度上取决于函数的好坏,好的函数使得给系统添加新部件很容易;而糟糕的函数则不断招致麻烦,让我们难以看清软件的行为,当需求变化时难以找到合适的地方进行修改.
做法
在很多时候,有必要以更渐进的方式 逐步迁移到达最终结果
- 修改函数的名称
- 移除或添加一个参数
- 把参数改为属性
封装变量(Encapsulate Variable)
封装字段
动机
重构的作用就是调整程序中的元素。函数相对容易调整一些,因为函数只有一种用 法,就是调用。在改名或搬移函数的过程中,总是可以比较容易地保留旧函数作为 转发函数(即旧代码调用旧函数,旧函数再调用新函数)。这样的转发函数通常不会存在太久,但的确能够简化重构过程。 数据就要麻烦得多,因为没办法设计这样的转发机制。如果我把数据搬走,就必须同时修改所有引用该数据的代码,否则程序就不能运行。如果数据的可访问范围很 小,比如一个小函数内部的临时变量,那还不成问题。但如果可访问范围变大,重构的难度就会随之增大,这也是说全局数据是大麻烦的原因。
对于所有可变的数据,只要它的作用域超出单个函数,我就会将其封装起来,只允许通过函数访问。数据的作用域越大,封装就越重要。
范例
1// 全局变量2let defaultOwner = { firstName: "Martin", lastName: "Fowler" };3
4// 使用全局变量5spaceship.owner = defaultOwner;6
7// 修改全局变量8defaultOwner = { firstName: "Rebecca", lastName: "Parsons" };1let defaultOwner = { firstName: "Martin", lastName: "Fowler" };2
3export function getDefaultOwner() {4 return defaultOwner;5}6
7export function setDefaultOwner(arg) {8 defaultOwner = arg;9}10
11// 使用全局变量12spaceship.owner = getDefaultOwner();13
14// 修改全局变量15setDefaultOwner({ firstName: "Rebecca", lastName: "Parsons" });变量改名(Rename Variable)
TODO:给这段描述简化
动机
好的命名是整洁编程的核心。变量可以很好地解释一段程序在干什么——如果变量 名起得好的话。但我经常会把名字起错——有时是因为想得不够仔细,有时是因为我对问题的理解加深了,还有时是因为程序的用途随着用户的需求改变了。
使用范围越广,名字的好坏就越重要。只在一行的 lambda 表达式中使用的变量, 跟踪起来很容易——像这样的变量,我经常只用一个字母命名,因为变量的用途在 这个上下文中很清晰。
同理,短函数的参数名也常常很简单。不过在 JavaScript 这样的动态类型语言中,我喜欢把类型信息也放进名字里(于是变量名可能叫 aCustomer)。
对于作用域超出一次函数调用的字段,则需要更用心命名。这是我最花心思的地方。
机制
- 如果变量被广泛使用,考虑运用封装变量(132)将其封装起来。
- 找出所有使用该变量的代码,逐一修改。
Tip 如果在另一个代码库中使用了该变量,这就是一个“已发布变量”(published variable),此时不能进行这个重构。 如果变量值从不修改,可以将其复制到一个新名字之下,然后逐一修改使用代码, 每次修改后执行测试。
引入参数对象(Introduce Parameter Object)
动机
一组数据项总是结伴同行,这样一组数据就 是所谓的数据泥团,可以用一个数据结构代之。
将数据组织成结构是一件有价值的事,因为这让数据项之间的关系变得明晰。
使用新的数据结构,参数的参数列表也能缩短。并且经过重构之后,所有使用该数据结 构的函数都会通过同样的名字来访问其中的元素,从而提升代码的一致性。
这项重构真正的意义在于,它会催生代码中更深层次的改变
做法
- 如果暂时还没有一个合适的数据结构,就创建一个。
- 使用改变函数声明(124)给原来的函数新增一个参数,类型是新建的数据结构。
- 调整所有调用者,传入新数据结构的适当实例。
- 用新数据结构中的每项元素,逐一取代参数列表中与之对应的参数项,然后删除原来的参数。
案例
1// 一组温度读数数据2const station = {3 name: "ZB1",4 readings: [5 { temp: 47, time: "2016-11-10 09:10" },6 { temp: 53, time: "2016-11-10 09:20" },7 { temp: 58, time: "2016-11-10 09:30" },8 { temp: 53, time: "2016-11-10 09:40" },9 { temp: 51, time: "2016-11-10 09:50" },10 ],11};12
13// 找到超出指定范围的温度读数14function readingsOutsideRange(station, min, max) {15 return station.readings.filter(r => r.temp < min || r.temp > max);1 collapsed line
16}1class NumberRange {2 constructor(min, max) {3 this._data = { min: min, max: max };4 }5 get min() {6 return this._data.min;7 }8 get max() {9 return this._data.max;10 }11}12
13function readingsOutsideRange(station, range) {14 return station.readings.filter(r => r.temp < range.min || r.temp > range.max);15}函数组合成类(Combine Functions into Class)
类,在大多数现代编程语言中都是基本的构造。所以很多情况都可以直接忽略。
如果发现一组函数形影不离地操作同一块数据(通常是将这块数据作为参数传递给 函数),我就认为,是时候组建一个类了。类能明确地给这些函数提供一个共用的环境.
1reading = { customer: "ivan", quantity: 10, month: 5, year: 2017 };2
3const aReading = acquireReading();4const base = baseRate(aReading.month, aReading.year) * aReading.quantity;5const taxableCharge = Math.max(0, base - taxThreshold(aReading.year));1class Reading {2 constructor(data) {3 this._customer = data.customer;4 this._quantity = data.quantity;5 this._month = data.month;6 this._year = data.year;7 }8 get customer() {9 return this._customer;10 }11 get quantity() {12 return this._quantity;13 }14 get month() {15 return this._month;13 collapsed lines
16 }17 get year() {18 return this._year;19 }20
21 get calculateBaseCharge() {22 return baseRate(this.month, this.year) * this.quantity;23 }24}25
26const rawReading = acquireReading();27const aReading = new Reading(rawReading);28const basicChargeAmount = aReading.calculateBaseCharge;函数组合成变换(Combine Functions into Transform)
动机
在软件中,经常需要把数据“喂”给一个程序,让它再计算出各种派生信息。 这些派生数值可能会在几个不同地方用到,因此这些计算逻辑也常会在用到派生数据的地方重复。 我更愿意把所有计算派生数据的逻辑收拢到一处,这样始终可以在固定的地方找到和更新这些逻辑,避免到处重复。
这种函数接受源数据作为输入,计算 出所有的派生数据,将派生数据以字段形式填入输出数据。有了变换函数,我就始 终只需要到变换函数中去检查计算派生数据的逻辑。
函数组合成变换的替代方案是函数组合成类,后者的做法是先用源数据创 建一个类,再把相关的计算逻辑搬移到类中。根据代码库中已有的编程风格来选择使用其中哪一个。两者有一个重要的区别:如果代码中会对源数据做更新,那么使用类要好得多;如果使用变换,派生数据会被存储在新生成的记录中,一旦源数据被修改,我就会遭遇数据不一致。
1function enrichReading(original) {2 const result = _.cloneDeep(original);3 result.baseCharge = calculateBaseCharge(result);4 result.taxableCharge = Math.max(5 0,6 result.baseCharge -- taxThreshold(result.year)7 );8 return result;9}10
11const rawReading = acquireReading();12const aReading = enrichReading(rawReading);13const taxableCharge = aReading.taxableCharge拆分阶段(Split Phase)
当看见一段代码在同时处理两件不同的事,查可以尝试把它拆分成各自独立的模块, 因为这样到了需要修改的时候,就可以单独处理每个主题,而不必同时在脑子里考虑两个不同的主题。
1function priceOrder(product, quantity, shippingMethod) {2 const basePrice = product.basePrice * quantity;3 const discount = Math.max(quantity - product.discountThreshold, 0) * product.basePrice * product.discountRate;4 const shippingPerCase = (basePrice > shippingMethod.discountThreshold)5 ? shippingMethod.discountedFee6 : shippingMethod.feePerCase;7 const shippingCost = quantity * shippingPerCase;8 const price = basePrice - discount + shippingCost;9 return price;10}1function priceOrder(product, quantity, shippingMethod) {2 const priceData = calculatePricingData(product, quantity);3 return applyShipping(priceData, shippingMethod);4}5
6function calculatePricingData(product, quantity) {7 const basePrice = product.basePrice * quantity;8 const discount = Math.max(quantity -- product.discountThreshold, 0)9 * product.basePrice * product.discountRate;10 return {basePrice: basePrice, quantity: quantity, discount:discount};11}12
13function applyShipping(priceData, shippingMethod) {14 const shippingPerCase = (priceData.basePrice > shippingMethod.discountThreshold)15 ? shippingMethod.discountedFee4 collapsed lines
16 : shippingMethod.feePerCase;17 const shippingCost = priceData.quantity * shippingPerCase;18 return priceData.basePrice -- priceData.discount + shippingCost;19}