Go語言實踐模式 – 函數選項模式(Functional Options Pattern)

什麼是函數選項模式

大家好,我是小白,有點黑的那個白。

最近遇到一個問題,因為業務需求,需要對接三方平台.

而三方平台提供的一些HTTP(S)介面都有統一的密鑰生成規則要求.

為此我們封裝了一個獨立的包 xxx-go-sdk 以便維護和對接使用.

其中核心的部分是自定義HTTP Client,如下:

type Client struct {}

func (c *Client) do() {
      // 實現統一的加密和簽名邏輯
      // 統一調用net/http
}

// 訂單列表介面
func (c *Client) OrderList(){
      c.do()
}


// 訂單發貨介面
func (c *Client) OrderDelivery(){
      c.do()
}

// ... 其他介面

一些平台會要求appKey/appSecret等資訊,所以Client結構體就變成了這樣,這時參數還比較少, 而且是必填的參數,我們可以提供構造函數來明確指定。

type Client struct {
      AppKey     string
      AppSecret string
}

func NewClient(appKey string, appSecret string) *Client {
     c := new(Client)
     c.AppKey = appKey
     c.AppSecret = appSecret
     return c
}

看起來很滿足,但是當我們需要增加一個 Timeout 參數來控制超時呢?

或許你會說這還不簡單,像下面一樣再加一個參數唄

type Client struct {
      AppKey     string
      AppSecret string
      Timeout    time.Duration
}

func NewClient(appKey string, appSecret string, timeout time.Duration) *Client {
     c := new(Client)
     c.AppKey = appKey
     c.AppSecret = appSecret
     c.Timeout = timeout
     return c
}

那再加些其他的參數呢?那構造函數的參數是不是又長又串,而且每個參數不一定是必須的,有些參數我們有會考慮默認值的問題。

為此,勤勞但尚未致富的 gophers 們使用了總結一種實踐模式

首先提取所有需要的參數到一個獨立的結構體 Options,當然你也可以用 Configs 啥的.

type Options struct {
      AppKey       string
      AppSecret string
}

然後為每個參數提供設置函數

func WithAppKey(appKey string) func(*Options) {
      return func(o *Options) {
            o.AppKey = appKey
      }
}

func WithAppSecret(appSecret string) func(*Options) {
      return func(o *Options) {
            o.AppSecret = appSecret
      }
}

這樣我們就為每個參數設置了獨立的設置函數。返回值 func(*Options) 看著有點不友好,我們提取下定義為單個 Option 調整一下程式碼

type Option func(*Options)

func WithAppKey(appKey string) Option {
      return func(o *Options) {
            o.AppKey = appKey
      }
}

func WithAppSecret(appSecret string) Option {
      return func(o *Options) {
            o.AppSecret = appSecret
      }
}

當我們需要添加更多的參數時,只需要在 Options 添加新的參數並添加新參數的設置函數即可。

比如現在要添加新的參數 Timeout

type Options struct {
      AppKey       string
      AppSecret   string
      Timeout.     time.Duration // 新增參數
}

// Timeout 的設置函數
func WithTimeout(timeout time.Duration) Option {
      return func(o *Options) {
            o.Timeout = timeout
      }
}

這樣後續不管新增多少參數,只需要新增配置項並添加獨立的設置函數即可輕鬆擴展,並且不會影響原有函數的參數順序和個數位置等。

至此,每個選項是區分開來了,那麼怎麼作用到我們的 Client 結構體上呢?

首先,配置選項都被提取到了 Options 結構體中,所以我們需要調整一下 Client 結構體的參數

type Client struct {
      options *Options
}

其次,每一個選項函數返回 Option,那麼任意多個就是 …Option,我們調整一下構造函數 NewClient 的參數形式,改為可變參數,不在局限於固定順序的幾個參數。

func NewClient(options ...Option) *Client {
    c := new(Client)
    c.Options = ?
    return c
}

然後循環遍歷每個選項函數,來生成Client結構體的完整配置選項。

func NewClient(options ...Option) *Client {
     opts := new(Options)
     for _, o := range options {
          o(opts)
    }
    c := new(Client)
    c.Options = opts
    return c
}

那麼怎麼調用呢?對於調用方而已,直接在調用構造函數NewClient()的參數內添加自己需要的設置函數(WithXXX)即可

client := NewClient(
     WithAppKey("your-app-key"),
     WithAppSecret("your-app-secret"),
)

當需要設置超時參數,直接添加 WithTimeout即可,比如設置3秒的超時

client := NewClient(
     WithAppKey("your-app-key"),
     WithAppSecret("your-app-secret"),
     WithTimeout(3*time.Second),
)

配置選項的位置可以任意設置,不需要受常規的固定參數順序約束。

可以看到,這種實踐模式主要作用於配置選項,利用函數支援的特性來實現的,為此得名 Functional Options Pattern,優美的中國話叫做「函數選項模式」。

總結

最後, 我們總結回顧一下在Go語言中函數選項模式的優缺點

優點

  1. 支援多參數;
  2. 支援參數任意位置順序;
  3. 支援默認值設置;
  4. 向後兼容,擴展性極佳;
  5. 用戶使用行為一致, 體感良好.

缺點

這是特性,不是缺點 – -!

  1. 增加了Options結構和Option定義;
  2. 針對每個參數都有對應的設置函數,每個選項函數的實現程式碼量好像多了一些;
Tags: