不會函數(shù)選項模式的Gopher看過來


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

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

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

怎么使用函數(shù)選項模式
一般水平
先來一個簡單例子,這個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)
簡單易懂,結構體有哪些屬性字段,那么構造函數(shù)的參數(shù),就相應做定義并傳入

帶來的問題:

代碼耦合度高:加屬性字段,構造函數(shù)就得相應做修改,調用的地方全部都得改,勢必會影響現(xiàn)有代碼;
代碼靈活度低:屬性字段不能指定默認值,每次都得明確傳入;
例如,現(xiàn)計劃新加3個字段Weight體重、CanRun是否會跑、LegNum幾條腿,同時要指定默認值CanRun=true、LegNum=4

新結構體定義:

type Animal struct {
  Name   string
  Age    int
  Height int
  Weight int
  CanRun bool
  LegNum int
}
代碼實現(xiàn)(函數(shù)加新參數(shù)定義,但默認值貌似實現(xiàn)不了,得調用構造函數(shù)時,明確傳入):

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)
后續(xù)逐步加新字段,這個構造函數(shù)就會被撐爆了,如果調用的地方越多,那么越傷筋動骨。






高階水平
既然常規(guī)寫法太low,難以實現(xiàn)新需求,那么我們就來玩點高階的,引出主題:函數(shù)選項模式。

首先,需要先定義一個函數(shù)類型OptionFunc

type OptionFunc func(*Animal)
然后,根據(jù)新結構體字段,定義With開頭的函數(shù),返回函數(shù)類型為OptionFunc的閉包函數(shù),內(nèi)部邏輯只需要實現(xiàn)更新對應字段值即可

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 }
}
再然后,優(yōu)化構造函數(shù)的定義和實現(xiàn)(name作為必傳參數(shù),其他可選,并且實現(xiàn)CanRun和LegNum兩個字段指定默認值)

func NewAnimal(name string, opts ...OptionFunc) *Animal {
  a := &Animal{Name: name, CanRun: true, LegNum: 4}
  for _, opt := range opts {
    opt(a)
  }
  return a
}
最后,調用優(yōu)化后的構造函數(shù),快速實現(xiàn)實例的初始化。想要指定哪個字段值,那就調用相應的With開頭的函數(shù),完全做到可配置化、可插拔;不指定還支持了默認值

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}
帶來的好處:

高度的可配置化、可插拔,還支持默認值設定;
很容易維護和擴展;
容易上手,大幅降低新來的人試錯成本;
開源項目中的實踐案例
函數(shù)選項模式,不單單是我們業(yè)務代碼中有使用,現(xiàn)在大量的標準庫和第三庫都在使用。

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

核心代碼:

type Options struct {
  AppID                      string               // appid
  Cluster                    string               // 默認的集群名稱,默認:default
  DefaultNamespace           string               // Get時默認使用的命名空間,如果設置了該值,而不在PreloadNamespaces中,默認也會加入初始化邏輯中
  PreloadNamespaces          []string             // 預加載命名空間,默認:為空
  ApolloClient               ApolloClient         // apollo HTTP api實現(xiàn)
  Logger                     Logger               // 日志實現(xiàn)類,可以設置自定義實現(xiàn)或者通過NewLogger()創(chuàng)建并設置有效的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)

//一系列函數(shù)作為選項
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
  }
}
//...
玩法:

使用Options結構體,定義出apollo需要使用到的所有配置字段;
定義一系列函數(shù)作為選項,對配置字段做初始化設置(例如,設置容災文件路徑、預加載的namespace、輪訓間隔時間等等);
構造函數(shù)里初始化一個Options的實例對象,并且根據(jù)傳入的函數(shù)選項,進行配置字段的更新,最終返回這個實例對象;
獲取到實例對象,調用相應的方法做相應的操作。
總結
由淺入深的講解了下實例對象初始化一般寫法和高階寫法。用好這個高階寫法(函數(shù)選項模式),讓代碼更優(yōu)雅。還不會使用的Gopher,趕緊學起來,用起來。

相關資料
[1]
http://github.com/shima-park/agollo: https://link.juejin.cn?target=http%3A%2F%2Fgithub.com%2Fshima-park%2Fagollo



請前往:http://lygongshang.com/TeacherV2.html?id=365