不會用“函式選項模式”的朋友看過來,這麼寫很優雅

王中陽Go1發表於2023-02-28

前言

透過這篇文章《為什麼說Go的函式是”一等公民“》,我們瞭解到了什麼是“一等公民”,以及都具備哪些特性,同時對函式的基本使用也更加深入。

本文重點介紹下Go設計模式之函式選項模式,它得益於Go的函式是“一等公民”,很好的一個應用場景,廣泛被使用。

什麼是函式選項模式

函式選項模式(Functional Options Pattern) ,也稱為選項模式(Options Pattern),是一種創造性的設計模式,允許你使用接受零個或多個函式作為引數的可變建構函式來構建複雜結構。我們將這些函式稱為選項,由此得名函式選項模式。

看概念有點太生硬難懂了,下面透過例子來講解下怎麼使用,由淺入深,通俗易懂。

怎麼使用函式選項模式

一般水平

先來一個簡單例子,這個Animal結構體,怎麼構造出一個例項物件

type Animal struct {
  Name   string
  Age    int
  Height int
}

通常的寫法

func NewAnimal(name string, age int, height int) *Animal {
  return &Animal{
    Name:   name,
    Age:    age,
    Height: height,
  }
}

a1 := NewAnimal("小白兔", 5, 100)

簡單易懂,結構體有哪些屬性欄位,那麼建構函式的引數,就相應做定義並傳入

帶來的問題

  1. 程式碼耦合度高:加屬性欄位,建構函式就得相應做修改,呼叫的地方全部都得改,勢必會影響現有程式碼;
  2. 程式碼靈活度低:屬性欄位不能指定預設值,每次都得明確傳入;

例如,現計劃新加3個欄位Weight體重CanRun是否會跑LegNum幾條腿,同時要指定預設值CanRun=true、LegNum=4

新結構體定義

type Animal struct {
  Name   string
  Age    int
  Height int
  Weight int
  CanRun bool
  LegNum int
}

程式碼實現(函式加新引數定義,但預設值貌似實現不了,得呼叫建構函式時,明確傳入):

func NewAnimal(name string, age int, height int, weight int, canRun bool, legNum int) *Animal {
  return &Animal{
    Name:   name,
    Age:    age,
    Height: height,
    Weight: weight,
    CanRun: canRun,
    LegNum: legNum,
  }
}

a1 := NewAnimal("小白兔", 5, 100, 120, true, 4)

後續逐步加新欄位,這個建構函式就會被撐爆了,如果呼叫的地方越多,那麼越傷筋動骨。

高階水平

既然常規寫法太low,難以實現新需求,那麼我們就來玩點高階的,引出主題:函式選項模式

首先,需要先定義一個函式型別OptionFunc

type OptionFunc func(*Animal)

然後,根據新結構體欄位,定義With開頭的函式,返回函式型別為OptionFunc的閉包函式,內部邏輯只需要實現更新對應欄位值即可

func WithName(name string) OptionFunc {
  return func(a *Animal) { a.Name = name }
}

func WithAge(age int) OptionFunc {
  return func(a *Animal) { a.Age = age }
}

func WithHeight(height int) OptionFunc {
  return func(a *Animal) { a.Height = height }
}

func WithWeight(weight int) OptionFunc {
  return func(a *Animal) { a.Weight = weight }
}

func WithCanRun(canRun bool) OptionFunc {
  return func(a *Animal) { a.CanRun = canRun }
}

func WithLegNum(legNum int) OptionFunc {
  return func(a *Animal) { a.LegNum = legNum }
}

再然後,最佳化建構函式的定義和實現(name作為必傳引數,其他可選,並且實現CanRunLegNum兩個欄位指定預設值)

func NewAnimal(name string, opts ...OptionFunc) *Animal {
  a := &Animal{Name: name, CanRun: true, LegNum: 4}
  for _, opt := range opts {
    opt(a)
  }
  return a
}

最後,呼叫最佳化後的建構函式,快速實現例項的初始化。想要指定哪個欄位值,那就呼叫相應的With開頭的函式,完全做到可配置化、可插拔;不指定還支援了預設值

a2 := NewAnimal("大黃狗", WithAge(10), WithHeight(120))
fmt.Println(a2)
a3 := NewAnimal("大灰狼", WithHeight(200))
fmt.Println(a3)

輸出結果:
&{大黃狗 10 120 0 true 4}
&{大灰狼 0 200 0 true 4}

帶來的好處

  1. 高度的可配置化、可插拔,還支援預設值設定;
  2. 很容易維護和擴充套件;
  3. 容易上手,大幅降低新來的人試錯成本;

開源專案中的實踐案例

函式選項模式,不單單是我們業務程式碼中有使用,現在大量的標準庫和第三庫都在使用。

下面帶著大家一塊來看看,apollo配置中心客戶端第三庫shima-park/agollo,看看它是怎麼玩的,怎麼做配置初始化

核心程式碼

type Options struct {
  AppID                      string               // appid
  Cluster                    string               // 預設的叢集名稱,預設:default
  DefaultNamespace           string               // Get時預設使用的名稱空間,如果設定了該值,而不在PreloadNamespaces中,預設也會加入初始化邏輯中
  PreloadNamespaces          []string             // 預載入名稱空間,預設:為空
  ApolloClient               ApolloClient         // apollo HTTP api實現
  Logger                     Logger               // 日誌實現類,可以設定自定義實現或者透過NewLogger()建立並設定有效的io.Writer,預設: ioutil.Discard
  AutoFetchOnCacheMiss       bool                 // 自動獲取非預設以外的Namespace的配置,預設:false
  LongPollerInterval         time.Duration        // 輪訓間隔時間,預設:1s
  BackupFile                 string               // 備份檔案存放地址,預設:.agollo
  FailTolerantOnBackupExists bool                 // 伺服器連線失敗時允許讀取備份,預設:false
  Balancer                   Balancer             // ConfigServer負載均衡
  EnableSLB                  bool                 // 啟用ConfigServer負載均衡
  RefreshIntervalInSecond    time.Duration        // ConfigServer重新整理間隔
  ClientOptions              []ApolloClientOption // 設定apollo HTTP api的配置項
  EnableHeartBeat            bool                 // 是否允許兜底檢查,預設:false
  HeartBeatInterval          time.Duration        // 兜底檢查間隔時間,預設:300s
}

func newOptions(configServerURL, appID string, opts ...Option) (Options, error) {
  var options = Options{
    AppID:                      appID,
    Cluster:                    defaultCluster,
    ApolloClient:               NewApolloClient(),
    Logger:                     NewLogger(),
    AutoFetchOnCacheMiss:       defaultAutoFetchOnCacheMiss,
    LongPollerInterval:         defaultLongPollInterval,
    BackupFile:                 defaultBackupFile,
    FailTolerantOnBackupExists: defaultFailTolerantOnBackupExists,
    EnableSLB:                  defaultEnableSLB,
    EnableHeartBeat:            defaultEnableHeartBeat,
    HeartBeatInterval:          defaultHeartBeatInterval,
  }
  for _, opt := range opts {
    opt(&options)
  }

  //...省略

  return options, nil
}

type Option func(*Options)

//一系列函式作為選項
func PreloadNamespaces(namespaces ...string) Option {
  return func(o *Options) {
    o.PreloadNamespaces = append(o.PreloadNamespaces, namespaces...)
  }
}
func AutoFetchOnCacheMiss() Option {
  return func(o *Options) {
    o.AutoFetchOnCacheMiss = true
  }
}
//...

玩法

  1. 使用Options結構體,定義出apollo需要使用到的所有配置欄位
  2. 定義一系列函式作為選項,對配置欄位做初始化設定(例如,設定容災檔案路徑、預載入的namespace、輪訓間隔時間等等);
  3. 建構函式里初始化一個Options的例項物件,並且根據傳入的函式選項,進行配置欄位的更新,最終返回這個例項物件;
  4. 獲取到例項物件,呼叫相應的方法做相應的操作。

總結

由淺入深的講解了下例項物件初始化一般寫法和高階寫法。用好這個高階寫法(函式選項模式),讓程式碼更優雅。

還不會使用的Gopher,趕緊學起來,用起來。

文章首發在公眾號:程式設計師升職加薪之旅,歡迎大家關注,第一時間收到新內容。

相關文章