低层级重构基本手法

什么是好代码的特征

评价代码好坏,如同审美,不仅关乎个人品味,而且存在客观标准。 好的代码直接了当,如果需要被修改,能让“修理工”轻易找到修改点,并且快速做出更改,同时不易引入其他错误。 检验标准可以归纳成 ETC(Easy To Change)原则,就是人们是否能轻而易举地修改它。

遗憾的是,我们面对不断变化的业务需求很难一步到位。只能快速实现,时时修缮,以期达到小步快跑的节奏感。

何为重构

在不改变软件可观察行为的前提下,调整其结构。使之无限接近 ETC。

何时重构

重构的挑战

尺度大一点

代码坏味道

1 神秘命名

2 重复代码

3 过长函数

4 过长参数列表

5 全局数据

6 可变数据

7 发散式变化

8 霰弹式修改

9 依恋情结

10 数据泥团

11 基本类型偏执

12 重复的 switch

13 循环语句

14 冗赘元素

15 夸夸其谈通用性

16 临时字段

17 过长的消息链

18 中间人

20 过大的类

21 异曲同工的类

22 纯数据类

23 被拒绝的馈赠

24 注释

重构的前置条件:测试体系

重构基本手法

1 提炼函数(Extract Function)

 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
}

3 提炼变量(Extract Variable)

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

a := height * width

优化后

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()

10 函数组合成变换(Combine Functions into Transform)

避免计算派生数据的逻辑到处重复,与 提炼函数 动机一样,但是孤立的函数常常很难找到,所以把函数和它们操作的数据放在一起,用起来才方便。 这么看来,跟 函数组合成类 又很相似,确实如此,一体两面。

11 拆分阶段(Split Phase)

简化条件逻辑

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"
}

总结