Refactoring Command

指令套件 github.com/urfave/cli 算蠻好上手的。雖然好用,但似乎其他套件也不錯,如 Cobra 等。

目前 Command 實際處理任務的程式都是直接依賴 cli.Context,這樣會違反依賴反轉原則--應該依賴更抽象的參數,如 Predeclared Typeinterface 等。

會發現這個問題是因為在實作 HTTP Server 時,行為沒變,只有輸出改變而已,但卻必須要為 /generate 的回傳重新客製化寫法,而且這段程式碼與 command/generate.go 裡面的 generate 函式太像了,這是一個明顯的壞味道,必須要重新調整設計才行。

分析

首先先調整 command/generate.gogenerate 函式。它的任務是產生一堆假資料,所以我們應該可以先把數量這個參數先抽離出來:

func generate(count int, c *cli.Context) error {
res, _ := provider.ParseFile(c.GlobalString("provider"))

generator := provider.Create()
generator.Resource = res

for i := 0; i < count; i++ {
fmt.Println(generator.Name())
}

return nil
}

接著取得 res 是 provider 的任務,因此它不適合離開這個範圍,但取得檔案名稱的 c.GlobalString("provider"),是可以抽離的:

func generate(path string, count int) error {
res, _ := provider.ParseFile(path)

generator := provider.Create()
generator.Resource = res

for i := 0; i < count; i++ {
fmt.Println(generator.Name())
}

return nil
}

最後就是 fmt.Println(generator.Name()) 這是 cli 專屬的行為,因此把它抽出變成 Closure,同時把這個方法也公開:

type GenerateItemProcess func(item string)

func Generate(path string, count int, process GenerateItemProcess) error {
res, _ := provider.ParseFile(path)

generator := provider.Create()
generator.Resource = res

for i := 0; i < count; i++ {
process(generator.Name())
}

return nil
}

到目前為止,只要把上面的程式碼找個地方放即可。因為這有點類似 Facade Pattern,因此決定放在 facade/facade.go 裡。原本 CLI 呼叫的地方改成這樣:

func(c *cli.Context) error {
num, err := strconv.Atoi(c.String("num"))

if err != nil {
return err
}

fmt.Println("Generate " + strconv.Itoa(num))

return facade.Generate(c.GlobalString("provider"), num, func(item string) {
fmt.Println(item)
})
}

同時把不必要的程式碼刪除,import 的項目就不需要 provider 了,改成依賴 facade。HTTP Server 實作的部分也可以如法炮製:

func serve(c *cli.Context) error {
num := 10

server := gin.Default()
server.GET(`/generate`, func(g *gin.Context) {
var s []string

facade.Generate(c.GlobalString("provider"), num, func(item string) {
s = append(s, item)
})

g.JSON(200, s)
})

server.Run()

return nil
}

同樣的重構方法也可以用在 QueryCommand,後面就不贅述了。

另外兩個 Command:ServeCommand 目前不知道該怎麼拆出來好(因為裡面也有用到 Facade);StatusCommand 則是因為太簡單,所以沒拆出來的必要。

效能改善

以上已經把 Generate 的任務解耦合了,但 HTTP Server 的效能上是有問題的,當 num 到了一個極大的數如 10,000,000,執行會需要花 7 秒:

[GIN] 2018/01/01 - 23:37:19 | 200 |  7.610408045s |       127.0.0.1 |  GET     /generate

主要是因為 append 了一千萬次,我們把 Generate 程式改一下:

type GenerateItemProcess func(item string, index int)

func Generate(path string, count int, process GenerateItemProcess) error {
res, _ := provider.ParseFile(path)

generator := provider.Create()
generator.Resource = res

for i := 0; i < count; i++ {
process(generator.Name(), i)
}

return nil
}

讓 process 可以順便把目前處理的 index 也傳入 Closure,HTTP Server 可以改寫成這樣:

s := make([]string, num)

facade.Generate(c.GlobalString("provider"), num, func(item string, index int) {
s[index] = item
})

這樣算是用空間換時間,時間結果約為原本的 65%:

[GIN] 2018/01/01 - 23:41:34 | 200 |  4.963947486s |       127.0.0.1 |  GET     /generate

程式碼修改可以參考 PR Day 27

問題

Interface 沒認真去了解,還真不知道該怎麼做好。

明天把 Command 的參數加完後,後天就來研究 Interface !

參考資料