低层级重构基本手法
什么是好代码的特征
评价代码好坏,如同审美,不仅关乎个人品味,而且存在客观标准。
好的代码直接了当,如果需要被修改,能让“修理工”轻易找到修改点,并且快速做出更改,同时不易引入其他错误。
检验标准可以归纳成 ETC(Easy To Change)原则,就是人们是否能轻而易举地修改它。
遗憾的是,我们面对不断变化的业务需求很难一步到位。只能快速实现,时时修缮,以期达到小步快跑的节奏感。
何为重构
在不改变软件可观察行为的前提下,调整其结构。使之无限接近 ETC。
何时重构
- 预备性重构:添加新功能很费劲
- 帮助理解的重构:代码看不懂
- 捡垃圾式重构:代码能看懂,但是烂
- 长期重构:每次改一点,就算改个名字也是好的
- 不重构:不需要理解则不必重构
重构的挑战
- 延缓新功能开发
- 遗留代码
- 代码所有权*
- 测试*
- 独立系统(数据库)*
尺度大一点
- 是不是不需要做架构了?
- 无限增加可读性会不会影响性能?
代码坏味道
1 神秘命名
2 重复代码
3 过长函数
4 过长参数列表
5 全局数据
- 特征:数据不符合预期的被改变,又找不到变动源头
- 原因:
- 不可变:相对安全
- 可变:作用域范围越大,被影响的概率越大,越难以探测影响源
- 改进:
6 可变数据
- 特征:一个变量拥有不同的语义
- 原因:
- 改进:
- 将一个(多功能)变量拆分成多个功能单一的变量
- 将没有副作用的代码和没有副作用的代码拆分开
- 函数式编程:如果要更新一个数据结构,就返回一份新的数据副本
7 发散式变化
- 特征:某个模块需经常因为不同的原因在不同的方向上发生变化
- 原因:随着软件功能越来越多,上下文边界变得不够清晰
- 改进:
- 如果有明显先后次序,拆分阶段
- 如果两个方向有更多来回调用,将拆分出两个独立的模块
8 霰弹式修改
- 特征:如果遇到某种变化,都必须在许多不同的结构内做出许多小修改
- 原因:模块拆分不合适
- 改进:先内联到一个大模块,再重新拆分
9 依恋情结
- 特征:
- 一个函数跟另一个模块的函数或者数据格外频繁,远胜于在自己所处模块内部的交流
- 某个函数为了计算某个值,从另外一个对象那儿调用几乎半打的取值函数。
- 改进:将函数提炼出来搬到那个对象里去
10 数据泥团
- 特征:数据项成群结队一起出入某个上下文,结构中的一坨字段,函数的一坨参数
- 改进:提炼出单独的结构
11 基本类型偏执
12 重复的 switch
- 特征:不同地方反复使用相同的 switch 逻辑(switch/case;if/else)
- 原因:每当想增加一个选择分支时,必须找到所有 switch, 并逐一更新
- 改进:以多态取代条件表达式
13 循环语句
14 冗赘元素
15 夸夸其谈通用性
16 临时字段
17 过长的消息链
- 如a.b.c().d(),链上任意类做修改都会影响整个调用
18 中间人
20 过大的类
- 有时也是难免的
- 观察对象的使用者
- 字段太多的结构拆成多个字段较少的结构
21 异曲同工的类
22 纯数据类
23 被拒绝的馈赠
24 注释
- 不要因为代码语义混乱而通过注释重复表达
- 注释的内容应该是 why 和 todo
重构的前置条件:测试体系
- 编程之前先写测试
- 编写一个测试case时,可以先让测试失败,再通过成功验证程序功能
- 遇到bug时,先添加一个单元测试复现这个bug
- 测试不能保证程序没有bug,编写测试样例也遵循82原则,当样例已经很多时,它带来的边际效果就没那么好了。应该更多考虑容易出错的边界条件,积极思考如何“破坏代码”。
重构基本手法
1
2
3
4
5
6
7
8
9
10
11
12
13
|
func printOwing(invoice Invoice) {
outstanding := 0
printBanner()
// calculate outstanding
for _, v := range invoice.Orders {
outstanding += v.Amount
}
recordDueDate(invoice)
printDetails(invoice, outstanding)
}
|
Step 1:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
func printOwing (invoice Invoice) {
printBanner()
// calculate outstanding
outstanding := 0
for _, v := range invoice.Orders {
outstanding += v.Amount
}
recordDueDate(invoice)
printDetails(invoice, outstanding)
}
|
Step 2:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
func printOwing (invoice Invoice) {
printBanner()
// calculate outstanding
outstanding := 0
for _, v := range invoice.Orders {
outstanding += v.Amount
}
recordDueDate(invoice)
printDetails(invoice, outstanding)
}
func calculateOutstanding (invoice Invoice) {
outstanding := 0
for _, v := range invoice.Orders {
outstanding += v.Amount
}
return outstanding
}
|
Step 3:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
func printOwing (invoice Invoice) {
printBanner()
outstanding := calculateOutstanding(invoice)
recordDueDate(invoice)
printDetails(invoice, outstanding)
}
func calculateOutstanding (invoice Invoice) {
outstanding := 0
for _, v := range invoice.Orders {
outstanding += v.Amount
}
return outstanding
}
|
Step 4:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
func printOwing (invoice Invoice) {
printBanner()
outstanding := calculateOutstanding(invoice)
recordDueDate(invoice)
printDetails(invoice, outstanding)
}
func calculateOutstanding (invoice Invoice) {
rest := 0
for _, v := range invoice.Orders {
rest += v.Amount
}
return rest
}
|
2 内联函数(Inline Function)
收敛零散的孤岛,中间过渡阶段,可以先将外部函数收敛成调用方中函数变量。
1
2
3
4
5
6
7
8
9
10
|
func getRating (driver) int {
if moreThanFiveLateDeliveries(driver) {
return 2
}
return 1
}
func moreThanFiveLateDeliveries (driver) bool {
return driver.NumberOfLateDeliveries > 5;
}
|
优化后
1
2
3
4
5
6
|
func getRating (driver) int {
if dirver.NumberOfLateDeliveries >5 {
return 2
}
return 1
}
|
1
2
3
4
5
|
func getProfit (order Order) float64 {
return order.quantity * order.itemPrice -
Math.Max(float64(0), order.quantity - 500) * order.itemPrice * 0.05 +
Math.Min(order.quantity * order.itemPrice * 0.1, float64(100)
}
|
优化后
1
2
3
4
5
6
|
func getProfit (order Order) float64 {
basePrice := order.quantity * order.itemPrice
quantityDiscount := Math.Max(float64(0), order.quantity - 500) * order.itemPrice * 0.05
shipping := Math.Min(order.quantity * order.itemPrice * 0.1, float64(100)
return basePrice - quantityDistcount + shipping
}
|
4 内联变量(Inline Variable)
1
2
3
4
|
func IsBigOrder (anOrder Order) bool {
basePrice := anOrder.BasePrice
return basePrice > 1000
}
|
优化后
1
2
3
|
func IsBigOrder (anOrder Order) bool {
return anOrder.BasePrice > 1000
}
|
5 改变函数声明(Change Function Declaration)
6 封装变量(Encapsulate Variable)
7 变量改名(Rename Variable)
There are only two hard things in Computer Science: cache invalidation and naming things. – Phil Karlton
优化后
area := height * width
8 引入参数对象(Introduce Parameter Object)
一组数据总是结伴同行,出没于一个又一个函数。可以用一个数据结构取代这坨数据泥土团。
这样不仅在语义上表现得更加收敛,还可以给收敛后的对象添加方法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
type Reading struct {
Temp int
Time string
}
type Station struct {
Name string
Readings []Reading
}
var station = Station{
Name: "tim",
Readings: []Reading{
{Temp: 41, Time: "2016-11-10 09:10:00"},
{Temp: 42, Time: "2016-11-11 09:10:00"},
{Temp: 43, Time: "2016-11-12 09:10:00"},
{Temp: 44, Time: "2016-11-13 09:10:00"},
{Temp: 45, Time: "2016-11-14 09:10:00"},
},
}
func readingsOutsideRange (station Station, min, max int) []Reading {
var rest []Reading
for _, v := range station.Readings {
if v.Temp < min || v.Temp > max {
rest = append(rest, v)
}
}
return rest
}
|
调用方代码:
1
2
3
|
alerts := readingsOutsideRang(station,
operatingPlan.TemperatureFloor,
operatingPlan.TemperatureCeiling)
|
优化之后
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
// 提取 min max 成一个结构
type NumberRange struct {
min int
max int
}
func NewNumberRange (min, max int) *NumberRange {
return &NumberRange {
min: min,
max: max,
}
}
func (r *NumberRange) Min () int {
return r.min
}
func (r *NumberRange) Max () int {
return r.max
}
// 变更 readingsOutsideRange 声明
func readingsOutsideRange (station Station, rg NumberRange) []Reading {
var rest []Reading
for _, v := range station.Readings {
if v.Temp < rg.Min() || v.Temp > rg.Max() {
rest = append(rest, v)
}
}
return rest
}
|
变更调用方代码:
1
2
|
rg := NewNumberRange(operatingPlant.TemperatureFloor, operatingPlant.TemperatureCeiling)
alerts := readingsOutsideRange(station, rg)
|
第一步已经完成,但是收益并不是很明显。另外一个收益是提高了扩展性。给新对象充血
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
// 增加一个方法,测试一个数是否在范围内
func (r *NumberRange) Contains (arg int) bool {
return arg >= r.min && arg <= r.max
}
// 同时变更 readingsOutsideRange 实现
readingsOutsideRange (station Station, rg NumberRange) []Reading {
var rest []Reading
for _, v := range station.Readings {
if !rg.Contains(v.Temp) {
rest = append(rest, v)
}
}
return rest
}
|
9 函数组合成类(Combine Functions into Class)
封装包级函数为变量级方法
1
2
3
4
5
6
7
8
9
10
11
12
|
type Reading struct {
Coustomer string
Quantity int
Month int
Year int
}
reading := Reading{
Coustomer: "tim",
Quantity: 10,
Month: 5,
Year: 2019,
}
|
Client 1:
1
2
|
aReading := acquireReading()
baseCharge := baseRate(aReading.Month, aReading.Year) * aReading.Quantity
|
Client 2:
1
2
3
|
aReading := acquireReading()
base := baseRate(aReading.Month, aReading.Year) * aReading.Quantity
taxableCharge := Math.Max(0, base - taxThreshold(aReading.Year))
|
发现了重复,想要提炼函数,但是……
Client 3:
1
2
3
4
5
6
|
aReading := acquireReading()
baseChargeAmount := calculateBaseCharge(aReading)
func calculateBaseCharge (aReading Reading){
baseRate(aReading.Month, aReading.Year) * aReading.Quantity
}
|
将 Client1 与 Client2 中的代码替换成 Client3 中的函数是不是就可以了?
为了更容易找到某批数据的相关操作,让结构更收敛一点,包级函数===>变量级方法。
优化之后
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
// 补充方法
func (r *Reading) BaseCharge () {
return baseRate(r.Month, r.Year) * r.Quantity
}
func (r *Reading) TaxableCharge () {
return Math.Max(0, r.BaseCharge() - taxThreshold(r.Year))
}
// 变更 Client 1
aReading := acquireReading()
baseCharge := aReading.BaseCharge()
// 变更 Client 2 & Client 3
aReading := acquireReading()
taxableCharge := aReading.TaxableCharge()
|
避免计算派生数据的逻辑到处重复,与 提炼函数 动机一样,但是孤立的函数常常很难找到,所以把函数和它们操作的数据放在一起,用起来才方便。
这么看来,跟 函数组合成类 又很相似,确实如此,一体两面。
11 拆分阶段(Split Phase)
- 编译器
- 中间件
- Controller-Logic-Dao
简化条件逻辑
1 分解条件表达式
提炼函数的一种应用场景
1
2
3
4
5
|
if (!aData.IsBefore(plan.SummerStart) && !aData.IsAfter(plan.SummerEnd)) {
charge = quantity * plan.SummerRate
} else {
charge = quantity * plan.RegularRate + plan.RegularServiceCharge
}
|
优化后
1
2
3
4
5
|
if (summer()) {
charge = summerCharge()
} else {
charge = regularCharge()
}
|
2 合并条件表达式
1
2
3
|
if (anEmployee.Seniority < 2) return 0
if (anEmployee.MonthsDisabled > 12) return 0
if (amEmployee.IsPartTime) return 0
|
优化后
1
2
3
4
5
|
if (isNotEligibleForDisability()) return 0
func isNotEligibleForDisality() {
return ((anEmployee.Seniority < 2) || (amEmployee.MonthsDisabled >12) || (anEmployee.IsPartTime))
}
|
3 以卫语句取代条件表达式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
func getPayAmount() int {
var rest int
if (isDead) {
rest = deadAmount()
} else {
if (isSeparated) {
rest = separatedAmount()
} else {
if (isRetired) {
rest = retiredAmount()
} else {
rest = normalPayAmount()
}
}
}
return rest
}
|
优化后
1
2
3
4
5
6
7
8
9
10
11
12
|
func getPayAmount() int {
if (isDead) {
return deadAmount()
}
if (isSeparated) {
return separatedAmount()
}
if (isRetired) {
return retiredAmount()
}
return normalPayAmount()
}
|
4 以多态取代条件表达式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
switch (bird.Type) {
case "EuropeanSwallow":
return "average"
case "AficanSwallow":
if (bird.NumberOfCoconuts > 2) {
return "thired"
}
return "average"
case "NorwegianBlueParrot":
if (bird.Voltage > 100) {
return "second"
}
return "beautiful"
default :
return "unknown"
}
|
优化后
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
type EuropeanSwallow struct{}
func (e EuropeanSwallow) Plumage() {
return "average"
}
type AfricanSwallow struct{}
func (a AfricanSwallow) Plumage() {
if (bird.NumberOfCoconuts > 2) {
return "thired"
}
return "average"
}
type NorwegianBlueParrot struct{}
func (n NorwegianBlueParrot) Plumage(){
if (bird.Voltage > 100) {
return "second"
}
return "beautiful"
}
|
总结