Golang 常見設計模式之選項模式

撈起月亮的漁民發表於2022-09-08

Golang 常見設計模式之選項模式

熟悉 Python 開發的同學都知道,Python 有預設引數的存在,使得我們在例項化一個物件的時候,可以根據需要來選擇性的覆蓋某些預設引數,以此來決定如何例項化物件。當一個物件有多個預設引數時,這個特性非常好用,能夠優雅地簡化程式碼。

而 Go 語言從語法上是不支援預設引數的,所以為了實現既能透過預設引數建立物件,又能透過傳遞自定義引數建立物件,我們就需要透過一些程式設計技巧來實現。對於這些程式開發中的常見問題,軟體行業的先行者們總結了許多解決常見場景編碼問題的最佳實踐,這些最佳實踐後來成為了我們所說的設計模式。其中選項模式在 Go 語言開發中會經常用到。

通常我們有以下三種方法來實現透過預設引數建立物件,以及透過傳遞自定義引數建立物件:

  • 使用多個建構函式

  • 預設引數選項

  • 選項模式

透過多建構函式實現

第一種方式是透過多建構函式實現,下面是一個簡單例子:


package 
main


import "fmt"

const (
    defaultAddr = "127.0.0.1"
    defaultPort = 8000
)

type Server struct {
    Addr string
    Port int
}

func NewServer() * Server {
    return & Server{
        Addr: defaultAddr,
        Port: defaultPort,
   }
}

func NewServerWithOptions( addr string, port int) * Server {
    return & Server{
        Addr: addr,
        Port: port,
   }
}

func main() {
    s1 : = NewServer()
    s2 : = NewServerWithOptions( "localhost", 8001)
    fmt. Println( s1)   // &{127.0.0.1 8000}
    fmt. Println( s2)   // &{localhost 8001}
}

這裡我們為 Server 結構體實現了兩個建構函式:

  • NewServer:無需傳遞引數即可直接返回 Server 物件

  • NewServerWithOptions :需要傳遞 addr 和 port 兩個引數來構造 Server 物件

如果透過預設引數建立的物件即可滿足需求,不需要對 Server 進行定製時,我們可以使用 NewServer 來生成物件(s1)。而如果需要對 Server 進行定製時,我們則可以使用 NewServerWithOptions 來生成物件(s2)。

透過預設引數選項實現

另外一種實現預設引數的方案,是為要生成的結構體物件定義一個選項結構體,用來生成要建立物件的預設引數,程式碼實現如下:


package 
main


import "fmt"

const (
    defaultAddr = "127.0.0.1"
    defaultPort = 8000
)

type Server struct {
    Addr string
    Port int
}

type ServerOptions struct {
    Addr string
    Port int
}

func NewServerOptions() * ServerOptions {
    return & ServerOptions{
        Addr: defaultAddr,
        Port: defaultPort,
   }
}

func NewServerWithOptions( opts * ServerOptions) * Server {
    return & Server{
        Addr: opts. Addr,
        Port: opts. Port,
   }
}

func main() {
    s1 : = NewServerWithOptions( NewServerOptions())
    s2 : = NewServerWithOptions( & ServerOptions{
        Addr: "localhost",
        Port: 8001,
   })
    fmt. Println( s1)   // &{127.0.0.1 8000}
    fmt. Println( s2)   // &{localhost 8001}
}

我們為 Server 結構體專門實現了一個 ServerOptions 用來生成預設引數,呼叫 NewServerOptions 函式即可獲得預設引數配置,建構函式 NewServerWithOptions 接收一個 *ServerOptions 型別作為引數。所以我們可以透過以下兩種方式來完成功能:

  • 直接將呼叫 NewServerOptions 函式的返回值傳遞給 NewServerWithOptions 來實現透過預設引數生成物件(s1)

  • 透過手動構造 ServerOptions 配置來生成定製物件(s2)

透過選項模式實現

以上兩種方式雖然都能夠完成功能,但卻有以下缺點:

  • 透過多建構函式實現的方案需要我們在例項化物件時分別呼叫不同的建構函式,程式碼封裝性不強,會給呼叫者增加使用負擔。

  • 透過預設引數選項實現的方案需要我們預先構造一個選項結構,當使用預設引數生成物件時程式碼看起來比較冗餘。

而選項模式可以讓我們更為優雅地解決這個問題。程式碼實現如下:


package 
main


import "fmt"

const (
    defaultAddr = "127.0.0.1"
    defaultPort = 8000
)

type Server struct {
    Addr string
    Port int
}

type ServerOptions struct {
    Addr string
    Port int
}

type ServerOption interface {
    apply( * ServerOptions)
}

type FuncServerOption struct {
    f func( * ServerOptions)
}

func ( fo FuncServerOption) apply( option * ServerOptions) {
    fo. f( option)
}

func WithAddr( addr string) ServerOption {
    return FuncServerOption{
        f: func( options * ServerOptions) {
            options. Addr = addr
       },
   }
}

func WithPort( port int) ServerOption {
    return FuncServerOption{
        f: func( options * ServerOptions) {
            options. Port = port
       },
   }
}

func NewServer( opts ... ServerOption) * Server {
    options : = ServerOptions{
        Addr: defaultAddr,
        Port: defaultPort,
   }

    for _, opt : = range opts {
        opt. apply( & options)
   }

    return & Server{
        Addr: options. Addr,
        Port: options. Port,
   }
}

func main() {
    s1 : = NewServer()
    s2 : = NewServer( WithAddr( "localhost"), WithPort( 8001))
    s3 : = NewServer( WithPort( 8001))
    fmt. Println( s1)   // &{127.0.0.1 8000}
    fmt. Println( s2)   // &{localhost 8001}
    fmt. Println( s3)   // &{127.0.0.1 8001}
}

乍一看我們的程式碼複雜了很多,但其實呼叫建構函式生成物件的程式碼複雜度是沒有改變的,只是定義上的複雜。

我們定義了 ServerOptions 結構體用來配置預設引數。因為 Addr 和 Port 都有預設引數,所以 ServerOptions 的定義和 Server 定義是一樣的。但有一定複雜性的結構體中可能會有些引數沒有預設引數,必須讓使用者來配置,這時 ServerOptions 的欄位就會少一些,大家可以按需定義。

同時,我們還定義了一個 ServerOption 介面和實現了此介面的 FuncServerOption 結構體,它們的作用是讓我們能夠透過 apply 方法為 ServerOptions 結構體單獨配置某項引數。

我們可以分別為每個預設引數都定義一個 WithXXX 函式用來配置引數,如這裡定義的 WithAddr 和 WithPort ,這樣使用者就可以透過呼叫 WithXXX 函式來定製需要覆蓋的預設引數。

此時預設引數定義在建構函式 NewServer 中,建構函式的接收一個不定長引數,型別為 ServerOption,在建構函式內部透過一個 for 迴圈呼叫每個傳進來的 ServerOption 物件的 apply 方法,將使用者配置的引數依次賦值給建構函式內部的預設引數物件 options 中,以此來替換預設引數,for 迴圈執行完成後,得到的 options 物件將是最終配置,將其屬性依次賦值給 Server 即可生成新的物件。

總結

透過 s2 和 s3 的列印結果可以發現,使用選項模式實現的建構函式更加靈活,相較於前兩種實現,選項模式中我們可以自由的更改其中任意一項或多項預設配置。

雖然選項模式確實會多寫一些程式碼,但多數情況下這都是值得的。比如 Google 的 gRPC 框架 Go 語言實現中建立 gRPC server 的建構函式 NewServer 就使用了選項模式,感興趣的同學可以看下其原始碼的實現思想其實和這裡的示例程式如出一轍、感興趣的朋友可以在3A伺服器上部署相關的環境,進行試驗。以上就是我關於 Golang 選項模式的一點經驗,希望今天的分享能夠給你帶來一些幫助。


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/70021806/viewspace-2913968/,如需轉載,請註明出處,否則將追究法律責任。

相關文章