golang中的選項模式

slowquery發表於2022-09-22

索引

waterflow.link/articles/1663835071...

當我在使用go-zero時,我看到了好多像下面這樣的程式碼:

...

type (
    // RunOption defines the method to customize a Server.
    RunOption func(*Server)

    // A Server is a http server.
    Server struct {
        ngin   *engine
        router httpx.Router
    }
)

...

// AddRoutes add given routes into the Server.
func (s *Server) AddRoutes(rs []Route, opts ...RouteOption) {
    r := featuredRoutes{
        routes: rs,
    }
    for _, opt := range opts {
        opt(&r)
    }
    s.ngin.addRoutes(r)
}

...

// WithJwt returns a func to enable jwt authentication in given route.
func WithJwt(secret string) RouteOption {
    return func(r *featuredRoutes) {
        validateSecret(secret)
        r.jwt.enabled = true
        r.jwt.secret = secret
    }
}

我們可以把重點放在RouteOption上面。這裡就使用了選項模式。

什麼是選項模式?

選項模式是一種函數語言程式設計模式,用於為可用於修改其行為的函式提供可選引數。

如果你用過php的話,你肯定會看到過這樣的函式,畢竟PHP是世界上最好的語言:

public function cache(callable $callable, $duration = null, $dependency = null)

// 我們可以這樣呼叫
cache($callable);

// 也可以把後面的可選引數帶上
cache($callable, $duration);

這種能力在 API 設計中非常有用,因為

  • 允許使用者以最低配置使用方法,同時仍為有經驗的使用者提供充足的配置選項。
  • 允許開發者在不破壞向後相容性的情況下新增新選項。

然而,在 Golang 中,這是不可能的。該語言不提供新增可選引數的方法。

這就是“選項模式”的用武之地。它允許使用者在呼叫方法 時傳遞其他選項,然後可以相應地修改其行為。讓我們看一個小例子。

假設我們有一個結構體Server,它有 3 個屬性port, timeoutmaxConnections

type Server struct {
    port           string
    timeout        time.Duration
    maxConnections int
}

然後我們有個Server的工廠方法

func NewServer(port string, timeout time.Duration, maxConnections int) *Server {
    return &Server{
        port:           port,
        timeout:        timeout,
        maxConnections: maxConnections,
    }
}

但是現在我們希望只有埠號是必傳的,timeoutmaxConnections成為可選引數。在很多情況下,這些的預設值就足夠了。另外,我不想用這些不必要的配置來轟炸剛剛學習或試驗我的 方法 的新使用者。讓我們看看該怎麼去實現。

首先我們定義一個新的option結構體

type Option func(*Server)

Option是一個函式型別,它接受指向我們的Server. 這很重要,因為我們將使用這些選項修改我們的Server例項。

現在讓我們定義我們的選項。慣例是在我們的選項前面加上 With,但可以隨意選擇適合您的域語言的任何名稱

func WithTimeout(timeout time.Duration) Option {
    return func(s *Server) {
        s.timeout = timeout
    }
}

func WithMaxConnections(maxConn int) Option {
    return func(s *Server) {
        s.maxConnections = maxConn
    }
}

我們的兩個選項WithTimeoutWithMaxConnections,採用配置值並返回一個Option。這Option只是一個函式,它接受一個指向我們Server物件的指標並將所需的屬性設定為提供的值。例如,WithTimeout獲取超時持續時間,然後返回一個函式(其簽名與 Option相同)將我們伺服器的 timeout 屬性設定為提供的值。

在這裡,我們使用了一種幾乎所有現代語言(包括 Golang)都支援的稱為閉包的技術

我們的工廠方法Server現在需要修改以支援這種變化

func NewServer(port string, options ...Option) *Server {
    server := &Server{
        port: port,
    }

    for _, option := range options {
        option(server)
    }

    return server
}

現在我們就可以用上面世界上最好的語言的方式呼叫了

NewServer("8430")
NewServer("8430", WithTimeout(10*time.Second))
NewServer("8430", WithTimeout(10*time.Second), WithMaxConnections(10))

在這裡你可以看到我們的客戶端現在可以建立一個只有埠的最小伺服器,但如果需要也可以自由地提供更多的配置選項。

這種設計具有高度的可擴充套件性和可維護性,甚至比我們在 PHP 中看到的可選引數還要好。它允許我們新增更多選項,而不會膨脹我們的函式簽名,也不會觸及我們在工廠方法中的程式碼。

下面是完整程式碼:

package main

import "time"

type Option func(*Server)

type Server struct {
    port           string
    timeout        time.Duration
    maxConnections int
}

func NewServer(port string, options ...Option) *Server {
    server := &Server{
        port: port,
    }

    for _, option := range options {
        option(server)
    }

    return server
}

func WithTimeout(timeout time.Duration) Option {
    return func(s *Server) {
        s.timeout = timeout
    }
}

func WithMaxConnections(maxConn int) Option {
    return func(s *Server) {
        s.maxConnections = maxConn
    }
}

func main() {
    NewServer("8430")
    NewServer("8430", WithTimeout(10*time.Second))
    NewServer("8430", WithTimeout(10*time.Second), WithMaxConnections(10))
}
本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章