Go 中常用的四大重構技術

大家好,我是程序員幽鬼。

Martin Fowler 在他的書中 [1] 將重構定義爲 “對軟件的內部結構進行的更改,以使其更易於理解,並且在不更改其可觀察到的行爲的情況下更低廉地進行修改”。本書包含大量重構技術,這些重構技術旨在在某些情況下應用,並旨在消除代碼壞味道 [2]。

重構是一個非常廣泛的話題,我發現重構在軟件開發過程中起着重要的作用。它們的相關性很高,因此它們是 TDD[3] 生命週期的重要組成部分。

由於它們的重要性,在這篇文章中,我想分享一下軟件開發人員中使用最多的 4 種重構技術。但是在開始之前,因爲可以自動應用重構技術(即某些 IDE 爲你提供了幫助,通過應用重構工具,只需單擊幾下鼠標和進行選擇,即可使你的生活更輕鬆),在這裏,我將通過使用 Go 語言手動重構進行描述,並嘗試將其作爲參考指南。我們的開發團隊意識到,在應用任何重構技術之前,應將可觀察到的功能包含在單元測試中,並通過所有測試。

01 提取方法

這是我常應用於代碼的技術。它包括提取一段按意圖分組的代碼,並轉移到新方法中。通過提取可以將一個長方法或函數拆分爲一些小方法,這些小方法將邏輯組合在一起。通常,小方法或函數的名稱可以更好地瞭解該邏輯是什麼。

下面的示例顯示了應用此重構技術之前和之後的情況。我的主要目標是通過將複雜度分爲不同的功能,這樣來抽象其複雜度。

func StringCalculator(exp string) int {
    if exp == "" {
        return 0
    }
    
    var sum int
    for _, number := range strings.Split(exp, ",") {
        n, err := strconv.Atoi(number)
        if err != nil {
            return 0
        }
        sum += n
    }
    return sum
}

重構爲:

func StringCalculator(exp string) int {
    if exp == "" {
        return 0
    }
 return sumAllNumberInExpression(exp)
}

func sumAllNumberInExpression(exp string) int {
    var sum int
    for _, number := range strings.Split(exp, ",") {
        sum += toInt(number)
    }
    return sum
}

func toInt(exp string) int {
    n, err := strconv.Atoi(exp)
    if err != nil {
        return 0
    }
    return n
}

StringCalculator 函數更簡單了,但是當添加了兩個新的函數時,它會增加複雜性。這是一個我願意做出慎重決定的犧牲,我將此作爲參考而不是規則,從某種意義上說,瞭解應用重構技術的結果可以很好地判斷是否應用重構技術。

02 移動方法

有時,在使用提取方法後,我發現了另一個問題:此方法應該屬於此結構或包嗎?Move Method 是一種簡單的技術,包括將方法從一個結構移動到另一個結構。我發現一個技巧,來確定某個方法是否應該屬於該結構:弄清楚該方法是否訪問了另一個結構依賴項的內部。看下面的例子:

type Book struct {
    ID    int
    Title string
}

type Books []Book

type User struct {
    ID    int
    Name  string
    Books Books
}

func (u User) Info() {
    fmt.Printf("ID:%d - Name:%s", u.ID, u.Name)
    fmt.Printf("Books:%d", len(u.Books))
    fmt.Printf("Books titles: %s", u.BooksTitles())
}

func (u User) BooksTitles() string {
    var titles []string
    for _, book := range u.Books {
        titles = append(titles, book.Title)
    }
    return strings.Join(titles, ",")
}

如你所見,User 的方法BooksTitles 使用了 books(具體是 Title)中的內部字段多於User,這表明該方法應歸於Books。應用這種重構技術將該方法移動到Books類型上,然後由用戶的Info方法調用。

func (b Books) Titles() string {
    var titles []string
    for _, book := range b {
        titles = append(titles, book.Title)
    }
    return strings.Join(titles, ",")
}

func (u User) Info() {
    fmt.Printf("ID:%d - Name:%s", u.ID, u.Name)
    fmt.Printf("Books:%d", len(u.Books))
    fmt.Printf("Books titles: %s", u.Books.Titles())
}

應用此方法後,Books類型會更內聚,因爲它是唯一擁有控制權和對它的字段和內部屬性訪問權的人。同樣,這是在深思熟慮之前進行的思考過程,知道應用重構會帶來什麼結果。

03 引入參數對象

你見過多少像下面方法一樣,有很多參數的:

func (om *OrderManager) Filter(startDate, endDate time.Time, country, state, city, status string) (Orders, error) {
    ...

即使我們看不到函數內部的代碼,當我們看到大量這樣的參數時,我們也可以考慮它執行的大量操作。

有時,我發現這些參數之間高度相關,並在以後定義它們的方法中一起使用。這爲重構提供了一種使該場景更加面向對象的方式進行處理的方法,並且建議將這些參數分組爲一個結構,替換方法簽名以將該對象用作參數,並在方法內部使用該對象。

type OrderFilter struct {
    StartDate time.Time
    EndDate   time.Time
    Country   string
    State     string
    City      string
    Status    string
}

func (om *OrderManager) Filter(of OrderFilter) (Orders, error) {
    // use of.StartDate, of.EndDate, of.Country, of.State, of.City, of.Status.

看起來更乾淨,並且可以確定這些參數的身份,但是這將要求我更改調用此方法的所有引用,並且需要OrderFilter在傳遞給該方法之前創建一個新類型的對象作爲參數。同樣,在嘗試進行此重構之前,我會盡力思考並考慮後果。當你的代碼中的影響程度很低時,我認爲此技術非常有效。

04 用符號常量替換魔數

該技術包括用常數變量替換硬編碼值以賦予其意圖和意義。

func Add(input string) int {
    if input == "" {
        return 0
    }

    if strings.Contains(input, ";") {
        n1 := toNumber(input[:strings.Index(input, ";")])
        n2 := toNumber(input[strings.Index(input, ";")+1:])

        return n1 + n2
    }

    return toNumber(input)
}

func toNumber(input string) int {
    n, err := strconv.Atoi(input)
    if err != nil {
        return 0
    }
    return n
}

其中 ; 字符是什麼意思?如果答案對我來說不太明確,我可以創建一個臨時變量,並使用硬編碼字符設置該值,以賦予其意義。

func Add(input string) int {
    if input == "" {
        return 0
    }

    numberSeparator := ";"
    if strings.Contains(input, numberSeparator) {
        n1 := toNumber(input[:strings.Index(input, numberSeparator)])
        n2 := toNumber(input[strings.Index(input, numberSeparator)+1:])

        return n1 + n2
    }

    return toNumber(input)
}


func toNumber(input string) int {
    n, err := strconv.Atoi(input)
    if err != nil {
        return 0
    }
    return n
}

總結

感謝閱讀,希望對你有所幫助。重構是一個非常廣泛的話題,本文舉例說明了重構中使用最多的四個。不要將此處提到的內容視爲理所當然,自己嘗試一下。此處描述的重構技術僅用作指導原則,而未作爲規則遵循,意味着它們在需要時可以有針對性地進行調整。最後,我想說我們對所編寫的所有代碼和所使用的所有工具負責,我們的經驗和知識可以指導我們掌握在每種情況下最適合的技能,我認爲重構技術確實值得。

原文鏈接:https://wawand.co/blog/posts/four-most-refactoring-techniques-i-use/

參考資料

[1]

書中: https://martinfowler.com/books/refactoring.html

[2]

壞味道代碼: https://en.wikipedia.org/wiki/Code_smell

[3]

TDD: https://en.wikipedia.org/wiki/Test-driven_development#/media/File:TDD_Global_Lifecycle.png


歡迎關注「幽鬼」,像她一樣做團隊的核心。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/urdxZlq4nPassPwrQLzN3A