一些實用的編程模式:Options模式
本文轉(zhuǎn)載自微信公眾號「網(wǎng)管叨bi叨」,作者沒玩游戲的網(wǎng)管。轉(zhuǎn)載本文請聯(lián)系網(wǎng)管叨bi叨公眾號。
今天開個新系列,講一些實用的編程模式,每個編程模式學(xué)完后,都能馬上在實戰(zhàn)中應(yīng)用起來,讓我們寫出更富表達(dá)力、易維護(hù)、好擴(kuò)展、優(yōu)雅億點點的代碼。
這些編程模式的示例我會用Go來演示,但其實這些模式大多與語言無關(guān),無論你平時主攻Go、Java還是JavaScript 我覺得都能用上。
為避免貼長篇代碼,我會適當(dāng)用一些偽代碼,大家理解思路后,可以在我的GitHub倉庫gocookbook找到完整可運行的代碼。
公眾號回復(fù) gocookbook 關(guān)鍵字獲取鏈接,打開后Ctrl+F搜"Options"。
系列第一篇要分享的編程模式是函數(shù)式編程里的Options模式。
Options模式解決什么問題
Options模式可以讓具有多個可選參數(shù)的函數(shù)或者方法更整潔和好擴(kuò)展,當(dāng)一個函數(shù)具有五六個甚至十個以上的可選參數(shù)時使用這種模式的優(yōu)勢會體現(xiàn)的很明顯,我們還是通過一些例子慢慢感受一下。
比如我們要在項目里封裝一個通用的發(fā)Http請求的工具函數(shù),它的參數(shù)可能會有哪些呢?因為是工具函數(shù),要做到通用就必然需要定義很多能配置HTTP客戶端的參數(shù),比如:
- func HttpRequest(method string, url string, body []byte, headers map[string]string, timeout time.Duration) ...
函數(shù)簽名里的返回值這里就省略了,太寬影響閱讀,這里大家注意一下。
上面這個工具函數(shù),如果只是做GET請求的話,很多HTTP客戶端的設(shè)置是不需要設(shè)置的,而且超時時間我們一般都會設(shè)置一個默認(rèn)的。如果還按普通定義函數(shù)的方法來實現(xiàn)的話,函數(shù)邏輯里勢必會有不少判斷空值的邏輯。
- if body != nil {
- // 設(shè)置請求體Data
- ......
- }
- if headers != nil {
- // 設(shè)置請求頭
- ......
- }
調(diào)用的時候,調(diào)用者的代碼也不得不傳一些零值給不需要自定義的配置參數(shù)。
- HttpRequest('GET', 'https://www.baidu.com', nil, nil, 2 * time.Second)
如果是Java的話,其實是可以通過方法的重載解決這個問題,但是如果可選的參數(shù)是十幾個,各個調(diào)用方對可選參數(shù)的順序要求不一樣的話,定義這個多重載方法顯然不是一個好的解決方案。
另外一種常用的解決方案是,工具函數(shù)的簽名定義時,不再定義各個可能需要配置的可選參數(shù),轉(zhuǎn)而定義一個配置對象。
- type HttpClientConfig struct {
- timeout time.Duration
- headers map[string]string
- body []byte
- }
- func HttpRequest(method string, url string, config *HttpClientConfig) ...
配置對象方案的問題
函數(shù)簽名里通過傳遞一個配置對象來聚合各種可能的可選參數(shù)這個方案,對調(diào)用者來說,比上一種方法看起來簡潔了不少,如果全都是默認(rèn)選項只需要給配置對象這個參數(shù)傳遞一個零值即可。
- HttpRequest('GET', 'https://www.baidu.com', nil)
但是對于函數(shù)的實現(xiàn)方來說,仍然少不了那些選項參數(shù)非零值的判斷,而且因為配置對象在函數(shù)外部可以改變,這就有一定幾率配置對象在函數(shù)內(nèi)部未被使用前被外部程序改變,真正發(fā)生了相關(guān)的BUG,排查起來會比較頭疼。
可變參數(shù)方案的問題
與配置對象方案類似,如果單純通過可變參數(shù)來解決這個問題,也會有不少問題。
- func HttpRequest(method string, url string, options ...interface{}) ...
雖然參數(shù)是可變的,但是實現(xiàn)方需要通過遍歷設(shè)置HTTP客戶端的不同選項,這就讓可變參數(shù)固定了傳遞順序,調(diào)用方如果想要設(shè)置某個可選項還得記住參數(shù)順序,切無法直接通過函數(shù)簽名就確定參數(shù)順序,貌似還不如咱們最原始的解決方案。
使用Options模式的方案
最后,我們來說一下使用Options模式怎么解決這個問題,其實如果你如果使用過gRPC的話,會發(fā)現(xiàn)gRPC的SDK里Options模式出現(xiàn)的幾率相當(dāng)高,比如它的客戶端方法可以傳遞不少以with開頭的閉包函數(shù)方法。
- client.cc, err = grpc.Dial(
- "127.0.0.1:12305",
- grpc.WithInsecure(),
- grpc.WithUnaryInterceptor(...),
- grpc.WithStreamInterceptor(...),
- grpc.WithAuthority(...)
- )
這些配置方法返回的都是一個名為DialOption的interface。
- type DialOption interface {
- apply(*dialOptions)
- }
- func WithInsecure() DialOption {
- ...
- }
現(xiàn)在我們就使用Options模式對我們的工具函數(shù)進(jìn)行一下改造,首先定義一個契約和配置對象。
- // 針對可選的HTTP請求配置項,模仿gRPC使用的Options設(shè)計模式實現(xiàn)
- type requestOption struct {
- timeout time.Duration
- data string
- headers map[string]string
- }
- type Option struct {
- apply func(option *requestOption)
- }
- func defaultRequestOptions() *requestOption {
- return &requestOption{ // 默認(rèn)請求選項
- timeout: 5 * time.Second,
- data: "",
- headers: nil,
- }
- }
接下來我們要定義的配置函數(shù),每個都會設(shè)置請求配置對象里的某一個配置。
- 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
- },
- }
- }
那么此時我們的工具函數(shù)的簽名就應(yīng)用上上面定義的接口契約。
- func HttpRequest(method string, url string, options ...*Option) ...
在其實現(xiàn)里我們只需要遍歷options這個可變參數(shù),調(diào)用每個Option對象的apply方法對配置對象進(jìn)行配置即可,不用在擔(dān)心可變參數(shù)的順序。
- func httpRequest(method string, url string, options ...*Option) {
- reqOpts := defaultRequestOptions() // 默認(rèn)的請求選項
- for _, opt := range options { // 在reqOpts上應(yīng)用通過options設(shè)置的選項
- opt.apply(reqOpts)
- }
- // 創(chuàng)建請求對象
- req, err := http.NewRequest(method, url, strings.NewReader(reqOpts.data))
- // 設(shè)置請求頭
- for key, value := range reqOpts.headers {
- req.Header.Add(key, value)
- }
- // 發(fā)起請求
- ......
- return
- }
總結(jié)
最后我們的HTTP工具函數(shù)的調(diào)用方式就變成了,下面這種更靈活更富表達(dá)力的方式。
- HttpRequest("GET", url)
- HttpRequest("POST", url, WithHeaders(headers)
- HttpRequest("POST", url, WithTimeout(timeout), WithHeaders(headers), WithData(data))
從實現(xiàn)方來看呢?如果后面要給配置對象里增加其他配置項,只需要擴(kuò)充類型的字段,在定義一個對應(yīng)的With方法即可,擴(kuò)展性完全在可接受范圍內(nèi)。
好了Options模式你學(xué)會沒,想不想趕快用起來,現(xiàn)在公眾號里回復(fù)關(guān)鍵字 gocookbook 就能獲得完整可運行的代碼示例(打開鏈接后記得Ctrl+F搜"Options")。下次再遇到類似的場景后記得把今天學(xué)到的用上呀。