Go 中常用的四大重構技術
大家好,我是程序員幽鬼。
Martin Fowler 在他的書中[1]將重構定義為*“對軟件的內(nèi)部結構進行的更改,以使其更易于理解,并且在不更改其可觀察到的行為的情況下更低廉地進行修改”*。本書包含大量重構技術,這些重構技術旨在在某些情況下應用,并旨在消除代碼壞味道[2]。
重構是一個非常廣泛的話題,我發(fā)現(xiàn)重構在軟件開發(fā)過程中起著重要的作用。它們的相關性很高,因此它們是TDD[3]生命周期的重要組成部分。
由于它們的重要性,在這篇文章中,我想分享一下軟件開發(fā)人員中使用最多的 4 種重構技術。但是在開始之前,因為可以自動應用重構技術(即某些 IDE 為你提供了幫助,通過應用重構工具,只需單擊幾下鼠標和進行選擇,即可使你的生活更輕松),在這里,我將通過使用 Go 語言手動重構進行描述,并嘗試將其作為參考指南。我們的開發(fā)團隊意識到,在應用任何重構技術之前,應將可觀察到的功能包含在單元測試中,并通過所有測試。
01 提取方法
這是我常應用于代碼的技術。它包括提取一段按意圖分組的代碼,并轉(zhuǎn)移到新方法中。通過提取可以將一個長方法或函數(shù)拆分為一些小方法,這些小方法將邏輯組合在一起。通常,小方法或函數(shù)的名稱可以更好地了解該邏輯是什么。
下面的示例顯示了應用此重構技術之前和之后的情況。我的主要目標是通過將復雜度分為不同的功能,這樣來抽象其復雜度。
- 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 函數(shù)更簡單了,但是當添加了兩個新的函數(shù)時,它會增加復雜性。這是一個我愿意做出慎重決定的犧牲,我將此作為參考而不是規(guī)則,從某種意義上說,了解應用重構技術的結果可以很好地判斷是否應用重構技術。
02 移動方法
有時,在使用提取方法后,我發(fā)現(xiàn)了另一個問題:此方法應該屬于此結構或包嗎?Move Method 是一種簡單的技術,包括將方法從一個結構移動到另一個結構。我發(fā)現(xiàn)一個技巧,來確定某個方法是否應該屬于該結構:弄清楚該方法是否訪問了另一個結構依賴項的內(nèi)部??聪旅娴睦樱?/p>
- 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)中的內(nèi)部字段多于User,這表明該方法應歸于Books。應用這種重構技術將該方法移動到Books類型上,然后由用戶的Info方法調(diào)用。
- 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類型會更內(nèi)聚,因為它是唯一擁有控制權和對它的字段和內(nèi)部屬性訪問權的人。同樣,這是在深思熟慮之前進行的思考過程,知道應用重構會帶來什么結果。
03 引入?yún)?shù)對象
你見過多少像下面方法一樣,有很多參數(shù)的:
- func (om *OrderManager) Filter(startDate, endDate time.Time, country, state, city, status string) (Orders, error) {
- ...
即使我們看不到函數(shù)內(nèi)部的代碼,當我們看到大量這樣的參數(shù)時,我們也可以考慮它執(zhí)行的大量操作。
有時,我發(fā)現(xiàn)這些參數(shù)之間高度相關,并在以后定義它們的方法中一起使用。這為重構提供了一種使該場景更加面向?qū)ο蟮姆绞竭M行處理的方法,并且建議將這些參數(shù)分組為一個結構,替換方法簽名以將該對象用作參數(shù),并在方法內(nèi)部使用該對象。
- 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.
看起來更干凈,并且可以確定這些參數(shù)的身份,但是這將要求我更改調(diào)用此方法的所有引用,并且需要OrderFilter在傳遞給該方法之前創(chuàng)建一個新類型的對象作為參數(shù)。同樣,在嘗試進行此重構之前,我會盡力思考并考慮后果。當你的代碼中的影響程度很低時,我認為此技術非常有效。
04 用符號常量替換魔數(shù)
該技術包括用常數(shù)變量替換硬編碼值以賦予其意圖和意義。
- 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
- }
其中 ; 字符是什么意思?如果答案對我來說不太明確,我可以創(chuàng)建一個臨時變量,并使用硬編碼字符設置該值,以賦予其意義。
- 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
- }
總結
感謝閱讀,希望對你有所幫助。重構是一個非常廣泛的話題,本文舉例說明了重構中使用最多的四個。不要將此處提到的內(nèi)容視為理所當然,自己嘗試一下。此處描述的重構技術僅用作指導原則,而未作為規(guī)則遵循,意味著它們在需要時可以有針對性地進行調(diào)整。最后,我想說我們對所編寫的所有代碼和所使用的所有工具負責,我們的經(jīng)驗和知識可以指導我們掌握在每種情況下最適合的技能,我認為重構技術確實值得。
原文鏈接: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
本文轉(zhuǎn)載自微信公眾號「幽鬼」,可以通過以下二維碼關注。轉(zhuǎn)載本文請聯(lián)系幽鬼公眾號。