Go 慣用模式:函數選項模式

作爲 Golang 開發者,遇到的許多問題之一就是嘗試將函數的參數設置成可選項。這是一個十分常見的場景,您可以使用一些已經設置默認配置和開箱即用的對象,同時您也可以使用一些更爲詳細的配置。

對於許多編程語言來說,這很容易。在 C 語言家族中,您可以提供具有同一個函數但是不同參數的多個版本;在 PHP 之類的語言中,您可以爲參數提供默認值,並在調用該方法時將其忽略。但是在 Golang 中,上述的做法都不可以使用。那麼您如何創建具有一些其他配置的函數,用戶可以根據他的需求(但是僅在需要時)指定一些額外的配置。

有很多的方法可以做到這一點,但是大多數方法都不是盡如人意,要麼需要在服務端的代碼中進行大量額外的檢查和驗證,要麼通過傳入他們不關心的其他參數來爲客戶端進行額外的工作。

下面我將會介紹一些不同的選項,然後爲其說明爲什麼每個選項都不理想,接着我們會逐步構建自己的方式來作爲最終的乾淨解決方案:函數選項模式。

讓我們來看一個例子。比方說,這裏有一個叫做 StuffClient 的服務,它能夠勝任一些工作,同時還具有兩個配置選項(超時和重試)。

type StuffClient interface {
    DoStuff() error
}

type stuffClient struct {
    conn    Connection
    timeout int
    retries int
}

這是個私有的結構體,因此我們應該爲它提供某種構造函數:

func NewStuffClient(conn Connection, timeout, retries int) StuffClient {
    return &stuffClient{
        conn:    conn,
        timeout: timeout,
        retries: retries,
    }
}

嗯,但是現在我們每次調用 NewStuffClient 函數時都要提供 timeoutretries。因爲在大多數情況下,我們只想使用默認值,我們無法使用不同參數數量帶定義多個版本的 NewStuffClient ,否則我們會得到一個類似 NewStuffClient redeclared in this block 編譯錯誤。

一個可選方案是創建另一個具有不同名稱的構造函數,例如:

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,
    }
}

但是這麼做的話有點蹩腳。我們可以做得更好,如果我們傳入了一個配置對象呢:

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,
    }
}

但是,這也不是很好的做法。現在,我們總是需要創建 StuffClientOption 這個結構體,即使不想在指定任何選項時還要傳遞它。另外我們也沒有自動填充默認值,除非我們在代碼中的某處添加了一堆檢查,或者也可以傳入一個 DefaultStuffClientOptions 變量(不過這麼做也不好,因爲在修改某一處地方後可能會導致其他的問題。)

所以,更好的解決方法是什麼呢?解決這個難題最好的解決方法是使用函數選項模式,它利用了 Go 對閉包更加方便的支持。讓我們保留上述定義的 StuffClientOptions ,不過我們仍需要爲其添加一些內容。

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
    }
}

泥土般芬芳, 不是嗎?這到底是怎麼回事?基本上,我們有一個結構來定義 StuffClient 的可用選項。另外,現狀我們還定義了一個叫做 StuffClientOption 的東西(次數是單數),它只是接受我們選項的結構體作爲參數的函數。我們還定義了另外兩個函數 WithRetriesWithTimeout ,它們返回一個閉包,現在就是見證奇蹟的時刻了!

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,
    }
}

現在,我們定義了一個額外和包含默認選項的沒有導出的變量,同時我們已經調整了構造函數,用來接收可變參數 [1]。然後, 我們遍歷 StuffClientOption 列表 (單數),針對每一個列表,將列表中返回的閉包使用在我們的 options 變量(需要記住,這些閉包接收一個 StuffClientOptions 變量,僅需要在選項的值上做出少許修改)。

現在我們要做的事情就是使用它!

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}

這看起來相當不錯,已經可以使用了!而且,它的好處是,我們只需要對代碼進行很少的修改,就可以隨時隨地添加新的選項。

把這些修改放在一起,就是這樣:

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
}

如果你想自己嘗試一下,請在 Go Playground[2] 上查找。

但這也可以通過刪除 StuffClientOptions 結構體進一步簡化,並將選項直接應用在我們的 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] 就能夠開始嘗試。在我們的示例中,我們只是將配置直接應用於結構體中,如果中間有一個額外的結構體是沒有意義的。但是,請注意,在許多情況下,您可能仍然想使用上一個示例中的 config 結構。例如,如果您的構造函數正在使用 config 選項執行某些操作時,但是並沒有將它們存儲到結構體中,或者被傳遞到其他地方,配置結構的變體是更通用的實現。

感謝 Rob Pike[4] 和 Dave Cheney[5] 推廣這種設計模式。


via: https://halls-of-valhalla.org/beta/articles/functional-options-pattern-in-go,54/

作者:ynori7[6] 譯者:sunlingbot[7] 校對:unknwon[8]

本文由 GCTT[9] 原創編譯,Go 中文網 [10] 榮譽推出

參考資料

[1] 可變參數: 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 中文網: https://studygolang.com/

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