一些實用的編程模式 - Options 模式

今天開個新系列,講一些實用的編程模式,每個編程模式學完後,都能馬上在實戰中應用起來,讓我們寫出更富表達力、易維護、好擴展、優雅億點點的代碼。

這些編程模式的示例我會用 Go 來演示,但其實這些模式大多與語言無關,無論你平時主攻 Go、Java 還是 JavaScript 我覺得都能用上。

爲避免貼長篇代碼,我會適當用一些僞代碼,大家理解思路後,可以在我的 GitHub 倉庫gocookbook找到完整可運行的代碼。

公衆號回覆 gocookbook 關鍵字獲取鏈接,打開後 Ctrl+F 搜 "Options"

系列第一篇要分享的編程模式是函數式編程裏的Options模式

Options 模式解決什麼問題

Options 模式可以讓具有多個可選參數的函數或者方法更整潔和好擴展,當一個函數具有五六個甚至十個以上的可選參數時使用這種模式的優勢會體現的很明顯,我們還是通過一些例子慢慢感受一下。

比如我們要在項目裏封裝一個通用的發 Http 請求的工具函數,它的參數可能會有哪些呢?因爲是工具函數,要做到通用就必然需要定義很多能配置 HTTP 客戶端的參數,比如:

func HttpRequest(method string, url string, body []byte, headers map[string]string, timeout time.Duration) ...

函數簽名裏的返回值這裏就省略了,太寬影響閱讀,這裏大家注意一下。

上面這個工具函數,如果只是做 GET 請求的話,很多 HTTP 客戶端的設置是不需要設置的,而且超時時間我們一般都會設置一個默認的。如果還按普通定義函數的方法來實現的話,函數邏輯裏勢必會有不少判斷空值的邏輯。

if body != nil {
   // 設置請求體Data
  ......
}

if headers != nil {
  // 設置請求頭
  ......
}

調用的時候,調用者的代碼也不得不傳一些零值給不需要自定義的配置參數。

HttpRequest('GET''https://www.baidu.com', nil, nil, 2 * time.Second)

如果是Java的話,其實是可以通過方法的重載解決這個問題,但是如果可選的參數是十幾個,各個調用方對可選參數的順序要求不一樣的話,定義這個多重載方法顯然不是一個好的解決方案。

另外一種常用的解決方案是,工具函數的簽名定義時,不再定義各個可能需要配置的可選參數,轉而定義一個配置對象。

type HttpClientConfig struct {
  timeout time.Duration
  headers map[string]string
  body    []byte
}

func HttpRequest(method string, url string, config *HttpClientConfig) ...

配置對象方案的問題

函數簽名裏通過傳遞一個配置對象來聚合各種可能的可選參數這個方案,對調用者來說,比上一種方法看起來簡潔了不少,如果全都是默認選項只需要給配置對象這個參數傳遞一個零值即可。

HttpRequest('GET''https://www.baidu.com', nil)

但是對於函數的實現方來說,仍然少不了那些選項參數非零值的判斷,而且因爲配置對象在函數外部可以改變,這就有一定幾率配置對象在函數內部未被使用前被外部程序改變,真正發生了相關的BUG,排查起來會比較頭疼。

可變參數方案的問題

與配置對象方案類似,如果單純通過可變參數來解決這個問題,也會有不少問題

func HttpRequest(method string, url string, options ...interface{}) ...

雖然參數是可變的,但是實現方需要通過遍歷設置 HTTP 客戶端的不同選項,這就讓可變參數固定了傳遞順序,調用方如果想要設置某個可選項還得記住參數順序,切無法直接通過函數簽名就確定參數順序,貌似還不如咱們最原始的解決方案。

使用 Options 模式的方案

最後,我們來說一下使用 Options 模式怎麼解決這個問題,其實如果你如果使用過 gRPC 的話,會發現 gRPC 的 SDK 裏 Options 模式出現的幾率相當高,比如它的客戶端方法可以傳遞不少以with開頭的閉包函數方法.

client.cc, err = grpc.Dial(
 "127.0.0.1:12305",
 grpc.WithInsecure(),
 grpc.WithUnaryInterceptor(...),
 grpc.WithStreamInterceptor(...),
 grpc.WithAuthority(...)
)

這些配置方法返回的都是一個名爲DialOptioninterface

type DialOption interface {
 apply(*dialOptions)
}

func WithInsecure() DialOption {
 ...
}

現在我們就使用Options模式對我們的工具函數進行一下改造,首先定義一個契約和配置對象。

// 針對可選的HTTP請求配置項,模仿gRPC使用的Options設計模式實現
type requestOption struct {
 timeout time.Duration
 data    string
 headers map[string]string
}

type Option struct {
 apply func(option *requestOption)
}

func defaultRequestOptions() *requestOption {
 return &requestOption{ // 默認請求選項
  timeout: 5 * time.Second,
  data:    "",
  headers: nil,
 }
}

接下來我們要定義的配置函數,每個都會設置請求配置對象裏的某一個配置

func WithTimeout(timeout time.Duration) *Option {
 return &Option{
  apply: func(option *requestOption) {
   option.timeout = timeout
  },
 }
}

func WithData(data string) *Option {
 return &Option{
  apply: func(option *requestOption) {
   option.data = data
  },
 }
}

那麼此時我們的工具函數的簽名就應用上上面定義的接口契約

func HttpRequest(method string, url string, options ...*Option) ...

在其實現裏我們只需要遍歷options這個可變參數,調用每個Option對象的apply方法對配置對象進行配置即可,不用在擔心可變參數的順序。

func httpRequest(method string, url string, options ...*Option) {
 reqOpts := defaultRequestOptions() // 默認的請求選項
 for _, opt := range options {      // 在reqOpts上應用通過options設置的選項
  opt.apply(reqOpts)
 }
 // 創建請求對象
 req, err := http.NewRequest(method, url, strings.NewReader(reqOpts.data))

  // 設置請求頭
 for key, value := range reqOpts.headers {
   req.Header.Add(key, value)
 }
 // 發起請求
  ......

 return
}

總結

最後我們的HTTP工具函數的調用方式就變成了,下面這種更靈活更富表達力的方式。

HttpRequest("GET", url)

HttpRequest("POST", url, WithHeaders(headers)
            
HttpRequest("POST", url, WithTimeout(timeout), WithHeaders(headers), WithData(data))

從實現方來看呢?如果後面要給配置對象裏增加其他配置項,只需要擴充類型的字段,在定義一個對應的 With 方法即可,擴展性完全在可接受範圍內。

好了Options模式你學會沒,想不想趕快用起來,現在公衆號裏回覆關鍵字  gocookbook  就能獲得完整可運行的代碼示例**(****打開鏈接後記得 Ctrl+F 搜 "Options")**。下次再遇到類似的場景後記得把今天學到的用上呀。

喜歡網管的文章內容和寫作風格,記得把我安利給更多人。光看公衆號不過癮?可以加我的個人微信(微信號:fsg1233110)

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