一個 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
去對非nil
的err
處理,fatal
會以非 0 狀態退出程序。而是使用 https://github.com/pkg/errors 對err
進行wrap
和cause
或者使用定義的業務相關的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 判斷和返回。每個操作也只有在err
爲nil
的情況下正常執行 。只有在三個操作都結束後才真正對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: 單行操作
這次改進是採用了裝飾器模式的思想把err
的check
邏輯和函數的執行操作都封裝在了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