《重构》学习常用的重构手法 上
Posted RikkaTheWorld
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了《重构》学习常用的重构手法 上相关的知识,希望对你有一定的参考价值。
系列文章目录
1. 《重构》学习(1)拆分 statement 函数
2. 《重构》学习(2)拆分逻辑与多态使用
3. 《重构》学习(3)概述
4. 《重构》学习(4)常用的重构手法 上
文章目录
我这边学习直接跳过了《重构》第3、4、5章,原因是:
- 第三章,代码坏味道
比较公式化的原则,仅是记录,在开发中规避,养成习惯就好了 - 第四章, 构筑测试体系
我所在的公司对测试体系的要求比较高,入职以来做过不少于两次的单元测试培训,这方面的思维已经比较完善了,并且日常开发中是比较看重单元测试的,粗略翻了一遍,都是已经学过的,所以我跳过了这一章 - 第五章, 介绍重构名录
这个结合现实,没有什么必要性,所以我也跳过了
3. 第一组重构
3.1 提炼函数
提炼函数 (Extract Method), 和 内联函数 是反义词
图片示例:
来看下面代码:
fun printOwing(invoices: Invoices)
printBanner()
val outstanding = calculateOutStanding()
print("name:$invoices.customer \\n amount:$outstanding")
提炼成:
fun printOwing()
printBanner()
val outstanding = calculateOutStanding()
printDetails(outstanding)
fun printDetails(outstanding)
print("name:$invoices.customer \\n amount:$outstanding")
何时将代码放到独立的函数中呢?
最合理的解答应该是:“将意图和实现分开”,如果你需要花时间浏览一段代码才能弄清楚它是干什么,
就应该把它提炼到一个函数中,并根据它所做的事为其命名
一段代码超过了6行,便逐渐开始散发臭味,所以要养成提炼函数的习惯,函数的名字长度并不重要。
命名时,根据函数的意图来对其进行命名, 用 “做什么”来命名,而不是 “怎么做”
如果编程语言支持嵌套函数,则把新函数嵌套在源函数中,可以减少中间变量。
对于变量:
- 如果提炼后的代码访问到了源函数的变量,则把源函数的变量作为入参传进去。
- 如果这个变量只在提炼后的函数中用到,源函数用不到,那么把这个变量的创建(声明)也放到提炼函数中去
有时候,提炼部分被赋值的局部变量太多,这时最好先放弃这种提炼,这种情况下,优先使用别的重构手法,例如:
- 拆分变量
- 查询取代临时变量
以简化变量使用情况,然后再考虑提炼函数
查看其他代码是否有与被提炼的代码段相同或相似之处。如果有,考虑使用以函数调用取代内联代码令其调用提炼出的新函数。
3.1.1 抽出无关局部变量的代码
来看看代码:
fun printOwing(invoice: ExtractInvoices)
var outstanding = 0;
print("***********************")
print("**** Customer Owes ****")
print("***********************")
// calculate outstanding
invoice.orders.forEach o ->
outstanding += o.amount
val today = Date();
invoice.dueDate = Date(today.year, today.month, today.date + 30)
print("name: $invoice.customer")
print("amount: $outstanding")
print("due: $invoice.dueDate.time")
这是一个用于打印单据的函数,如果想要提炼其中无关局部变量的代码,简直易如反掌:
fun printOwing(invoice: ExtractInvoices)
var outstanding = 0
printBanner()
// calculate outstanding
invoice.orders.forEach o ->
outstanding += o.amount
val today = Date()
invoice.dueDate = Date(today.year, today.month, today.date + 30)
printDetails(invoice, outstanding)
private fun printBanner()
print("***********************")
print("**** Customer Owes ****")
print("***********************")
// 这里虽然和局部变量有关,但仅仅是入参,读取之,而不修改之
private fun printDetails(invoice: ExtractInvoices, outstanding: Int)
print("name: $invoice.customer")
print("amount: $outstanding")
print("due: $invoice.dueDate.time")
3.1.2 有局部变量的提炼
就如上面的 printDetails()
最简单的提炼就是:我的方法只需读取它,并不用修改它们。
如果局部变量是一个数据结构,而源函数又修改了这个结构中的数据,也要如法炮制,将 “设置日期” 这一逻辑同样提炼出来,如下所示:
fun printOwing(invoice: ExtractInvoices)
var outstanding = 0
printBanner()
// calculate outstanding
invoice.orders.forEach o ->
outstanding += o.amount
recordDueData(invoice)
printDetails(invoice, outstanding)
...
private fun recordDueData(invoices: ExtractInvoices)
val today = Date()
invoices.dueDate = Date(today.year, today.month, today.date + 30)
3.1.3 对局部变量再赋值
如果上面代码中, 如果 printOwing()
对局部变量再进行赋值,问题就变得复杂了。(也就是可变变量)
被赋值的临时变量也分两种情况:
- 比较简单的情况:这个变量只在被提炼代码中使用,如果是这样,可以将这个临时变量的声明移动到被提炼的代码段中,然后一起提炼出去
- 比较复杂的情况:提炼函数之外的代码也是用了这个变量,这样我们就不能把这个变量放到提炼函数中去了。此时我们需要返回修改后的值
如上面代码中的 outstanding
变量,它被声明后,又通过 forEach 循环,进行了累加,变化了值,所以我们不能把其声明放到某个提炼函数中去,所以这个时候需要提炼其修改的函数:
fun printOwing(invoice: ExtractInvoices)
printBanner()
val outstanding = calculateOutstanding(invoice)
recordDueData(invoice)
printDetails(invoice, outstanding)
private fun calculateOutstanding(invoice: ExtractInvoices): Int
return invoice.orders.fold(0) acc, extractOrder ->
acc + extractOrder.amount
这里提炼了 calculateOutstanding
函数,将计算放在这个函数中,并且把源函数的 outstanding
改成了 val
不可变变量,这样让函数更加的可控。
如果这个函数计算的不止一个值,而是好几个,如何返回多个值呢? 这里的做法有:
- 像 Kotlin,可以使用像
Pair
、Triple
这样的数据结构 - 可以使用小对象
- 比起上面这一步更好的办法,是回头重新处理局部变量,使用 查询取代临时变量 或 拆分变量
3.2 内联函数(Inline Function)
和 提炼函数 是反义词
fun getRating(driver: InlineDriver): Int
return if (moreThanFiveLateDeliveries(driver)) 2 else 1
private fun moreThanFiveLateDeliveries(driver: InlineDriver): Boolean
return driver.numberOfLateDeliveries > 5
这个函数应该内联成:
fun getRating(driver: InlineDriver): Int
return if (driver.numberOfLateDeliveries > 5) 2 else 1
3.2.1 使用内联函数的场景
重构经常以简短的函数表现动作意图,这样会使代码更清晰已读,但有时候会遇到某些函数,其内部代码和函数名同样清晰易读。 如果是这样,就应该去掉这个函数,直接使用其中的代码,间接性可能带来帮助。
另外有一个使用的场景是: 我手上有一群组织及其不合理的函数,可以将他们全部内联到一个大的函数中,再用自己喜欢的方式重新提炼小函数。
原因:如果代码有太多中间层,使得系统中的所有函数都似乎只是对另外一个函数的简单委托,造成我在这些委托动作之间晕头转向,那我们通常会使用内联函数。 当然,中间层有其价值,但不是所有的中间层都有价值,通过内联手法,可以找到有用的中间层,同时将无用的中间层消除。
3.2.2 使用内联函数的步骤
- 检查函数,确定它不具备多态性
如果这个 “简单委托函数” 已经属于一个类,并且有子类继承了这个函数,那么就无法内联 - 找出这个函数所有的调用点
- 将这个函数的所有调用点都替换为函数本题
- 每次替换之后,进行测试
这里不必一次性完成所有代码的内联操作,如果某些调用点比较难以内联,可以等到时机成熟再来处理 - 删除该函数的定义
如果在使用内联的时,遇上了别的问题,那么就不该使用内联的重构手法
3.2.3 范例
来看下面代码:
fun reportLines(customer: InlineCustomer): Map<String, String>
val map = mutableMapOf<String, String>()
gatherCustomerData(map, customer)
return map
private fun gatherCustomerData(map: MutableMap<String, String>, customer: InlineCustomer)
map["name"] = customer.name
map["location"] = customer.location
可以看到 reportLines()
这个函数的作用就是构造了一个 map并返回,其中还调用了 gatherCustomerData
来填充数据,这其实是多余的,gatherCustomerData()
给其增加了副作用,在平时开发时,我们应该多思考自己有没有写这种兜圈子的函数,如果出现了, 将其内联回去:
fun reportLines(customer: InlineCustomer): Map<String, String>
return mapOf("name" to customer.name, "location" to customer.location)
3.3 提炼变量
提炼变量 (Extract Variable)也叫 引入解释性变量(Introduce Explaining Variable), 反义重构是 内联变量
如下面代码
fun cal(order: ExtractVarOrder): Double
return order.quantity * order.itemPrice -
(0.0).coerceAtLeast(order.quantity - 500) * order.itemPrice * 0.05 +
(order.quantity * order.itemPrice * 0.1).coerceAtMost(100.0)
需要提炼变量 ,变成:
fun cal(order: ExtractVarOrder): Double
val basePrice = order.quantity * order.itemPrice
val quantityDiscount = (0.0).coerceAtLeast(order.quantity - 500) * order.itemPrice * 0.05
val shipping = (order.quantity * order.itemPrice * 0.1).coerceAtMost(100.0)
return basePrice - quantityDiscount + shipping
3.3.1 使用场景
表达式有可能非常难以阅读,在这种情况下,局部变量可以帮助我们将表达式分解为比较容易管理的形式。在面对这块复杂的逻辑时,局部变量可以让我给其命名,这样命名可以更好的理解这部分逻辑想要干什么。(也就是说,提炼变量一般用于简化表达式)
使用这样的变量也便于调试,方便打印。
如果使用提炼变量,就意味着要给代码中的一个表达式命名,一旦决定要这样做,就得考虑这个名字所处的上下文。如果这个名字只在当前的函数中有意义,那么提炼变量很舒服。 如果这个变量名会和同文件或上下文中其他变量名冲突(或者有意义),那么就要考虑将其暴露出来,通常以函数的形式。
3.3.2 步骤
- 确定提炼的表达式没有副作用
- 声明一个不可修改的变量(val变量),把你想要提炼的表达式复制一份,以该表达式的结果值给这个比爱你量复制
- 用这个新变量取代原来表达式
- 测试
- 如果表达式出现了多次,请将新变量逐一替换
3.3.4 示例
data class ExtractRecord(val quantity: Double, val itemPrice: Double)
private class Order(private val data: ExtractRecord)
fun getQuantity(): Double = data.quantity
fun getItemPrice(): Double = data.itemPrice
fun getPrice(): Double
return getQuantity() * getItemPrice() -
(0.0).coerceAtLeast(getQuantity() - 500) * getItemPrice() * 0.05 +
(getQuantity() * getItemPrice() * 0.1).coerceAtMost(100.0)
我要提炼的还是同样的变量,但我注意到,这些变量名所代表的概念,适用于整个 Order 类,比如由 getQuantity() * getItemPrice()
计算出来的 basePrice
,这个玩意它的含义是整个 Order 类其它地方或许也可以用到的,同理后面的表达式也是。
所以这个时候,更愿意将其提炼成方法,而不仅仅是变量了:
..
fun getBasePrice(): Double = getQuantity() * getItemPrice()
fun getQuantityDiscount(): Double = (0.0).coerceAtLeast(getQuantity() - 500) * getItemPrice() * 0.05
fun getShipping(): Double = (getQuantity() * getItemPrice() * 0.1).coerceAtMost(100.0)
fun getPrice(): Double
return getBasePrice() - getQuantityDiscount() + getShipping()
..
这是对象带来的一大好处,它们提供了合适的上下文,方便分享相关的逻辑和数据。 在如此简单的情况下,这方面的好处不太明显,但在一个更大的类当中,如果能找出可以共用的行为,应当及时赋予他独立的抽象概念,并且一个好的名字,对于使用对象的人会很有帮助。
3.4 内联变量
内联临时变量(Inline Temp),反义词是 提炼变量
例如下面代码:
fun basePriceMoreThan1000(order: InlineVarOrder): Boolean
val basePrice = order.basePrice
return basePrice > 1000
可以优化成:
fun basePriceMoreThan1000(order: InlineVarOrder): Boolean
return order.basePrice > 1000
原则上,这种简单易懂的表达式,就无须再提炼变量出来了, 因为它可能妨碍了阅读。这个时候需要将其内联,消除变量。
而且现在的 IDE 也会帮我们消除这种问题
3.5 改变函数声明
也叫函数改名(Rename Function),指的是修改函数的名称或增加/减少入参。
3.5.1 为什么要改变
- 好的命名可以不让别人去查看实现代码就知道函数在做什么
- 参数也是同样的道理, 函数的参数列表阐述了函数如何与外部世界共处
对命名这里我就不做示例了,它比较考验程序员对函数的理解和文学的功底。
3.5.2 示例:添加参数
假如我们现在有一个 图书馆系统, Book
类代表图书,它可以接收顾客(custom)的预定(reservation),代码如下:
class Book()
private val reservations = mutableListOf<InlineVarCustomer>()
fun addReservation(customer: InlineVarCustomer)
reservations.add(customer)
现在系统需要支持 “高优先级预定”,即需要给 addReservation
函数新增一个参数,表示这次预定是进入普通队列还是优先队列。 如果我们不可以直接修改这个函数的入参,我们可以采取下面的方式来解决。
步骤一:用提炼函数,把 addReservation
函数提炼出来,放进一个新函数,这个函数也叫 addReservation
,因为 Java、Kotlin 支持重载,
fun addReservation(customer: InlineVarCustomer)
addReservation(customer, false)
fun addReservation(customer: InlineVarCustomer, isPriority: Boolean)
reservations.add(customer)
...
这样,我们就可以对源函数使用内联函数,将其调用者转向使用新函数,这样以后可以每次只修改一个调用者了。
3.5.3 示例:把参数改成属性
下面来处理一个更复杂的情况, 我们有一个函数,用于判断顾客(custom)是否在新英格兰地区:
fun inNewEngland(customer: InlineVarCustomer): Boolean
val england = listOf("MA", "CT", "ME", "VT", "NH", "RI")
return england.contains(customer.address.state)
调用函数如下:
fun filterEnglanders(customers: List<InlineVarCustomer>)
val newEnglanders = customers.filter
inNewEngland(it)
inNewEngland
函数只用到了顾客所在的州(state)这项信息,基于这个信息来判断顾客是否在新英格兰地区。现在希望重构这个函数,使其接收州代码(state code)作为参数,这样就能去除对 custom
这个概念的依赖,使得这个函数在更多的上下文中使用。
在使用改变函数声明时,我们通常会先运用提炼函数变量,来提取我们想要的新参数:
fun inNewEngland(customer: InlineVarCustomer): Boolean
val stateCode = customer.address.state
return listOf("MA", "CT", "ME", "VT", "NH", "RI").contains(stateCode)
然后使用提炼函数来创建新函数:
fun inNewEngland(customer: InlineVarCustomer): Boolean
val stateCode = customer.address.state
return inNewEngland(stateCode)
fun inNewEngland(stateCode: String): Boolean
return listOf("MA", "CT", "ME", "VT", "NH", "RI").contains(stateCode)
3.6 封装变量
又称 自封装字段(Self-Encapsulate Field)
例如将:
val defaultOwner = EncapsulateVarOwner(firstName = "Rikka", lastName = "xie")
调整成:
private var defaultOwnerData = EncapsulateVarOwner(firstName = "Rikka", lastName = "xie")
fun getDefaultOwner(): EncapsulateVarOwner = defaultOwnerData
fun setDefaultOwner(owner: EncapsulateVarOwner)
defaultOwnerData = owner
3.6.1 为什么要封装变量
函数比较容易调整,因为函数只有一种用法,就是调用,在改名或者搬移函数时,我们总是可以比较容易的保留旧函数,将其作为转发函数。
而数据就比较麻烦,因为没办法设计这样的转发机制,如果把数据搬走,就必须同时修改所有引用该数据的代码,否则程序就不能运行。
如果数据的可访问范围很小,比如一个小函数内部的临时变量,那还好,但是如果可访问范围大,那么重构的难度会随之增大,这也就是说全局数据是大麻烦的原因。
所以,如果想要搬移一处被广泛使用的数据,最好的办法往往是先以函数形式封装所有对该数据的访问。这样就能把“重新组织数据”的困难转化为“重新组织函数”这个相对简单的任务。
同时,封装变量的好处还有:
- 提供一个清晰的观测点,由此监控数据的变化和使用, 数据的作用域越大,封装就越重要
这里我的理解是:
- 一般看到
public
字段时,就会考虑使用封装变量,因为面向对象里强调数据应该保持数据为私有private
- 封装数据很重要,但是不可变数据更重要,如果数据不能修改,就根本不需要数据更新前的验证或其他逻辑钩子。我们就可以放心的复制数据,而不用搬移原来的数据。
以上是关于《重构》学习常用的重构手法 上的主要内容,如果未能解决你的问题,请参考以下文章
入门实战资料《Android进阶解密》+《Android进阶之光》+《重构改善既有的代码第2版》电子资料学习
重构改善既有代码设计--重构手法16:Introduce Foreign Method (引入外加函数)&& 重构手法17:Introduce Local Extension (引入本