一個 Go 方法,五種變換

今天土撥鼠帶大家來一段代碼的變換之旅,代碼篇幅較多,主要參考自one-way-to-do-it-six-variations,土撥鼠在這裏加了一些自己的理解。

文章: https://phlatphrog.medium.com/one-way-to-do-it-six-variations-cd58602ac06d

代碼倉庫: https://github.com/pdk/oneway

我們先來模擬一段開車去商場購物的代碼。主要經歷三個階段:開車去商店、到商店購物、然後購物完成開車回家。每個過程都需要去check其中返回的err錯誤並返回shopper對象。

// 開車去商店 
shopper, err := shopper.Drive(FuelNeededToGetToStore)
if nil != err {
    log.Fatalf("could not complete shopping: %s", err)
}
// 買雞蛋 
shopper, err = shopper.BuyEggs(EggsRequired)
if nil != err {
    log.Fatalf("could not complete shopping: %s", err)
}
// 買完雞蛋開車回家 
shopper, err = shopper.Drive(FuelNeededToGetHome)
if nil != err {
    log.Fatalf("could not complete shopping: %s", err)
}

變換 1:err 集中判斷

針對這段代碼,錯誤處理代碼較多,可以將三個重複代碼塊抽離成一個代碼塊。代碼如下:

func main(){
    shopper, err := shopper.Drive(FuelNeededToGetToStore)
    FatalIfErrNotNil(err)
    shopper, err = shopper.BuyEggs(EggsRequired)
    FatalIfErrNotNil(err)
    shopper, err = shopper.Drive(FuelNeededToGetHome)
    FatalIfErrNotNil(err)
}  

func FatalIfErrNotNil(err error) {
    if nil != err {
        log.Fatalf("could not complete shopping: %s", err)
    }
}

不過在生產項目中,正常業務我們一般不會使用fatal去對非nilerr處理,fatal會以非 0 狀態退出程序。而是使用 https://github.com/pkg/errors 對err進行wrapcause 或者使用定義的業務相關的err並返回上一層做處理。這裏也可以嘗試使用bool判斷來對err處理(不過之前土撥鼠之前羣裏也提出這樣的想法,大家都說多此一舉,尷尬)。

func main(){
    shopper, err := shopper.Drive(FuelNeededToGetToStore)
    if ErrorHandled(err) {
      return ...
    }
}

func ErrorHandled(err) bool {
  if nil != err {
    return true
  }
  // 也可以對error進行其他判斷操作
  ...
  return false
}

變換 2: 方法中包括 err 處理

是不是覺得上面的err處理還是不夠優雅,所以咱們這裏考慮把err的處理移出到主線之外。每個方法裏都包含了對err的非 nil 判斷和返回。每個操作也只有在errnil的情況下正常執行 。只有在三個操作都結束後才真正對err進行處理。

func main(){
  shopper, err := shopper.Drive(FuelNeededToGetToStore, nil)
    shopper, err = shopper.BuyEggs(EggsRequired, err)
    shopper, err = shopper.Drive(FuelNeededToGetHome, err)
    if nil != err {
        log.Fatalf("could not complete shopping: %s", err)
    }
}

func (s Shopper) Drive(fuelRequired int, err error) (Shopper, error) {
    if nil != err {
        return s, err
    }
   // 業務邏輯處理
    ... 
}

變換 3: 使用函數對 err 分解

下面這個變換,拋棄了之前Shopper作爲receiver的做法,而是將Shopper作爲函數的參數。其實跟變換 2 很相似。這兒是單獨抽離出一個公共的函數ErrCheckFunc進行err處理和函數執行。

func main(){
    drive := ErrCheckFunc(Drive)
    buy := ErrCheckFunc(BuyEggs)
    err, shopper := drive(nil, shopper, FuelNeededToGetToStore)
    err, shopper = buy(err, shopper, EggsRequired)
    err, shopper = drive(err, shopper, FuelNeededToGetHome)
    if nil != err {
        log.Fatalf("could not complete shopping: %s", err)
    }
}

func Drive(s Shopper, fuelRequired int) (Shopper, error) {
    if nil != err {
        return s, err
    }
   ...
}

func ErrCheckFunc(f func(Shopper, int) (Shopper, error)) func(error, Shopper, int) (error, Shopper) {
    return func(err error, s Shopper, arg int) (error, Shopper) {
        if nil != err {
            return err, s
        }
        s, err = f(s, arg)
        return err, s
    }
}

變換 4: 單行操作

這次改進是採用了裝飾器模式的思想把errcheck邏輯和函數的執行操作都封裝在了ErrCheckFunc一個方法中。相比變換 3,是將ErrCheckFunc中的返回值中的函數入參arg轉移到了ErrCheckFunc入參中的arg參數。

func main(){
    driveToStore := ErrCheckFunc(Drive, FuelNeededToGetToStore)
    buyEggs := ErrCheckFunc(BuyEggs, EggsRequired)
    driveHome := ErrCheckFunc(Drive, FuelNeededToGetHome)
  
    err, shopper := driveHome(buyEggs(driveToStore(nil, shopper)))
    if nil != err {
        log.Fatalf("could not complete shopping: %s", err)
    }
}

func ErrCheckFunc(f func(Shopper, int) (Shopper, error), arg int) func(error, Shopper) (error, Shopper) {
    return func(err error, s Shopper) (error, Shopper) {
        if nil != err {
            return err, s
        }
        s, err = f(s, arg)
        return err, s
    }
}

變換 5: 迭代變換

終極變換來了,這次變換採用了迭代的思想。ProcessSteps這裏使用的是一個可變的函數參數。

func main(){
    driveToStore := Flavorize(Drive, FuelNeededToGetToStore)
    buyEggs := Flavorize(BuyEggs, EggsRequired)
    driveHome := Flavorize(Drive, FuelNeededToGetHome)
  
    shopper, err := ProcessSteps(shopper,
        driveToStore,
        buyEggs,
        driveHome,
    )
    if nil != err {
        log.Fatalf("could not complete shopping: %s", err)
    }
}

func ProcessSteps(s Shopper, steps ...func(Shopper) (Shopper, error)) (Shopper, error) {
    for _, step := range steps {
        var err error 
        s, err = step(s)
        if nil != err {
            return s, err
        }
    }
    return s, nil
}

func Flavorize(f func(Shopper, int) (Shopper, error), arg int) func(Shopper) (Shopper, error) {
 return func(s Shopper) (Shopper, error) {
  return f(s, arg)
 }
}

小結

土撥鼠今天拿這個例子來展示給大家,主要是因爲在項目中也會經常遇到這樣的代碼邏輯場景,就想着看看別人是怎麼去重構改進的,學習怎麼去優化重構,使代碼變得更好維護、易於擴展、複用性高、可讀性高。希望大家也可以借鑑學習,如果你有更好的變換和想法歡迎大家留言討論。

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