Go 慣用模式:函數(shù)選項(xiàng)模式
作為 Golang 開發(fā)者,遇到的許多問題之一就是嘗試將函數(shù)的參數(shù)設(shè)置成可選項(xiàng)。這是一個(gè)十分常見的場(chǎng)景,您可以使用一些已經(jīng)設(shè)置默認(rèn)配置和開箱即用的對(duì)象,同時(shí)您也可以使用一些更為詳細(xì)的配置。
對(duì)于許多編程語言來說,這很容易。在 C 語言家族中,您可以提供具有同一個(gè)函數(shù)但是不同參數(shù)的多個(gè)版本;在 PHP 之類的語言中,您可以為參數(shù)提供默認(rèn)值,并在調(diào)用該方法時(shí)將其忽略。但是在 Golang 中,上述的做法都不可以使用。那么您如何創(chuàng)建具有一些其他配置的函數(shù),用戶可以根據(jù)他的需求(但是僅在需要時(shí))指定一些額外的配置。
有很多的方法可以做到這一點(diǎn),但是大多數(shù)方法都不是盡如人意,要么需要在服務(wù)端的代碼中進(jìn)行大量額外的檢查和驗(yàn)證,要么通過傳入他們不關(guān)心的其他參數(shù)來為客戶端進(jìn)行額外的工作。
下面我將會(huì)介紹一些不同的選項(xiàng),然后為其說明為什么每個(gè)選項(xiàng)都不理想,接著我們會(huì)逐步構(gòu)建自己的方式來作為最終的干凈解決方案:函數(shù)選項(xiàng)模式。
讓我們來看一個(gè)例子。比方說,這里有一個(gè)叫做 StuffClient 的服務(wù),它能夠勝任一些工作,同時(shí)還具有兩個(gè)配置選項(xiàng)(超時(shí)和重試)。
type StuffClient interface {
DoStuff() error
}
type stuffClient struct {
conn Connection
timeout int
retries int
}
這是個(gè)私有的結(jié)構(gòu)體,因此我們應(yīng)該為它提供某種構(gòu)造函數(shù):
func NewStuffClient(conn Connection, timeout, retries int) StuffClient {
return &stuffClient{
conn: conn,
timeout: timeout,
retries: retries,
}
}
嗯,但是現(xiàn)在我們每次調(diào)用 NewStuffClient 函數(shù)時(shí)都要提供 timeout 和 retries。因?yàn)樵诖蠖鄶?shù)情況下,我們只想使用默認(rèn)值,我們無法使用不同參數(shù)數(shù)量帶定義多個(gè)版本的 NewStuffClient ,否則我們會(huì)得到一個(gè)類似 NewStuffClient redeclared in this block 編譯錯(cuò)誤。
一個(gè)可選方案是創(chuàng)建另一個(gè)具有不同名稱的構(gòu)造函數(shù),例如:
func NewStuffClient(conn Connection) StuffClient {
return &stuffClient{
conn: conn,
timeout: DEFAULT_TIMEOUT,
retries: DEFAULT_RETRIES,
}
}
func NewStuffClientWithOptions(conn Connection, timeout, retries int) StuffClient {
return &stuffClient{
conn: conn,
timeout: timeout,
retries: retries,
}
}
但是這么做的話有點(diǎn)蹩腳。我們可以做得更好,如果我們傳入了一個(gè)配置對(duì)象呢:
type StuffClientOptions struct {
Retries int //number of times to retry the request before giving up
Timeout int //connection timeout in seconds
}
func NewStuffClient(conn Connection, options StuffClientOptions) StuffClient {
return &stuffClient{
conn: conn,
timeout: options.Timeout,
retries: options.Retries,
}
}
但是,這也不是很好的做法?,F(xiàn)在,我們總是需要?jiǎng)?chuàng)建 StuffClientOption 這個(gè)結(jié)構(gòu)體,即使不想在指定任何選項(xiàng)時(shí)還要傳遞它。另外我們也沒有自動(dòng)填充默認(rèn)值,除非我們?cè)诖a中的某處添加了一堆檢查,或者也可以傳入一個(gè) DefaultStuffClientOptions 變量(不過這么做也不好,因?yàn)樵谛薷哪骋惶幍胤胶罂赡軙?huì)導(dǎo)致其他的問題。)
所以,更好的解決方法是什么呢?解決這個(gè)難題最好的解決方法是使用函數(shù)選項(xiàng)模式,它利用了 Go 對(duì)閉包更加方便的支持。讓我們保留上述定義的 StuffClientOptions ,不過我們?nèi)孕枰獮槠涮砑右恍﹥?nèi)容。
type StuffClientOption func(*StuffClientOptions)
type StuffClientOptions struct {
Retries int //number of times to retry the request before giving up
Timeout int //connection timeout in seconds
}
func WithRetries(r int) StuffClientOption {
return func(o *StuffClientOptions) {
o.Retries = r
}
}
func WithTimeout(t int) StuffClientOption {
return func(o *StuffClientOptions) {
o.Timeout = t
}
}
泥土般芬芳, 不是嗎?這到底是怎么回事?基本上,我們有一個(gè)結(jié)構(gòu)來定義 StuffClient 的可用選項(xiàng)。另外,現(xiàn)狀我們還定義了一個(gè)叫做 StuffClientOption 的東西(次數(shù)是單數(shù)),它只是接受我們選項(xiàng)的結(jié)構(gòu)體作為參數(shù)的函數(shù)。我們還定義了另外兩個(gè)函數(shù) WithRetries 和 WithTimeout ,它們返回一個(gè)閉包,現(xiàn)在就是見證奇跡的時(shí)刻了!
var defaultStuffClientOptions = StuffClientOptions{
Retries: 3,
Timeout: 2,
}
func NewStuffClient(conn Connection, opts ...StuffClientOption) StuffClient {
options := defaultStuffClientOptions
for _, o := range opts {
o(&options)
}
return &stuffClient{
conn: conn,
timeout: options.Timeout,
retries: options.Retries,
}
}
現(xiàn)在,我們定義了一個(gè)額外和包含默認(rèn)選項(xiàng)的沒有導(dǎo)出的變量,同時(shí)我們已經(jīng)調(diào)整了構(gòu)造函數(shù),用來接收可變參數(shù)[1]。然后, 我們遍歷 StuffClientOption 列表(單數(shù)),針對(duì)每一個(gè)列表,將列表中返回的閉包使用在我們的 options 變量(需要記住,這些閉包接收一個(gè) StuffClientOptions 變量,僅需要在選項(xiàng)的值上做出少許修改)。
現(xiàn)在我們要做的事情就是使用它!
x := NewStuffClient(Connection{})
fmt.Println(x) // prints &{{} 2 3}
x = NewStuffClient(
Connection{},
WithRetries(1),
)
fmt.Println(x) // prints &{{} 2 1}
x = NewStuffClient(
Connection{},
WithRetries(1),
WithTimeout(1),
)
fmt.Println(x) // prints &{{} 1 1}
這看起來相當(dāng)不錯(cuò),已經(jīng)可以使用了!而且,它的好處是,我們只需要對(duì)代碼進(jìn)行很少的修改,就可以隨時(shí)隨地添加新的選項(xiàng)。
把這些修改放在一起,就是這樣:
var defaultStuffClientOptions = StuffClientOptions{
Retries: 3,
Timeout: 2,
}
type StuffClientOption func(*StuffClientOptions)
type StuffClientOptions struct {
Retries int //number of times to retry the request before giving up
Timeout int //connection timeout in seconds
}
func WithRetries(r int) StuffClientOption {
return func(o *StuffClientOptions) {
o.Retries = r
}
}
func WithTimeout(t int) StuffClientOption {
return func(o *StuffClientOptions) {
o.Timeout = t
}
}
type StuffClient interface {
DoStuff() error
}
type stuffClient struct {
conn Connection
timeout int
retries int
}
type Connection struct {}
func NewStuffClient(conn Connection, opts ...StuffClientOption) StuffClient {
options := defaultStuffClientOptions
for _, o := range opts {
o(&options)
}
return &stuffClient{
conn: conn,
timeout: options.Timeout,
retries: options.Retries,
}
}
func (c stuffClient) DoStuff() error {
return nil
}
如果你想自己嘗試一下,請(qǐng)?jiān)?Go Playground[2] 上查找。
但這也可以通過刪除 StuffClientOptions 結(jié)構(gòu)體進(jìn)一步簡(jiǎn)化,并將選項(xiàng)直接應(yīng)用在我們的 StuffClient 上。
var defaultStuffClient = stuffClient{
retries: 3,
timeout: 2,
}
type StuffClientOption func(*stuffClient)
func WithRetries(r int) StuffClientOption {
return func(o *stuffClient) {
o.retries = r
}
}
func WithTimeout(t int) StuffClientOption {
return func(o *stuffClient) {
o.timeout = t
}
}
type StuffClient interface {
DoStuff() error
}
type stuffClient struct {
conn Connection
timeout int
retries int
}
type Connection struct{}
func NewStuffClient(conn Connection, opts ...StuffClientOption) StuffClient {
client := defaultStuffClient
for _, o := range opts {
o(&client)
}
client.conn = conn
return client
}
func (c stuffClient) DoStuff() error {
return nil
}
從這里[3]就能夠開始嘗試。在我們的示例中,我們只是將配置直接應(yīng)用于結(jié)構(gòu)體中,如果中間有一個(gè)額外的結(jié)構(gòu)體是沒有意義的。但是,請(qǐng)注意,在許多情況下,您可能仍然想使用上一個(gè)示例中的 config 結(jié)構(gòu)。例如,如果您的構(gòu)造函數(shù)正在使用 config 選項(xiàng)執(zhí)行某些操作時(shí),但是并沒有將它們存儲(chǔ)到結(jié)構(gòu)體中,或者被傳遞到其他地方,配置結(jié)構(gòu)的變體是更通用的實(shí)現(xiàn)。
感謝 Rob Pike[4] 和 Dave Cheney[5] 推廣這種設(shè)計(jì)模式。
via: https://halls-of-valhalla.org/beta/articles/functional-options-pattern-in-go,54/
作者:ynori7[6]譯者:sunlingbot[7]校對(duì):unknwon[8]
本文由 GCTT[9] 原創(chuàng)編譯,Go 中文網(wǎng)[10] 榮譽(yù)推出
參考資料
可變參數(shù): https://gobyexample.com/variadic-functions
[2]Go Playground: https://play.golang.org/p/VcWqWcAEyz
[3]這里: https://play.golang.org/p/Z5P5Om4KDL
[4]Rob Pike: https://commandcenter.blogspot.de/2014/01/self-referential-functions-and-design.html
[5]Dave Cheney: https://dave.cheney.net/2014/10/17/functional-options-for-friendly-apis
[6]ynori7: https://halls-of-valhalla.org/beta/user/ynori7
[7]sunlingbot: https://github.com/sunlingbot
[8]unknwon: https://github.com/unknwon
[9]GCTT: https://github.com/studygolang/GCTT
[10]Go 中文網(wǎng): https://studygolang.com/
推薦閱讀
