不會(huì)函數(shù)選項(xiàng)模式的Gopher看過(guò)來(lái)


本文重點(diǎn)介紹下Go設(shè)計(jì)模式之函數(shù)選項(xiàng)模式,它得益于Go的函數(shù)是“一等公民”,很好的一個(gè)應(yīng)用場(chǎng)景,廣泛被使用。

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

看概念有點(diǎn)太生硬難懂了,下面通過(guò)例子來(lái)講解下怎么使用,由淺入深,通俗易懂。

怎么使用函數(shù)選項(xiàng)模式
一般水平
先來(lái)一個(gè)簡(jiǎn)單例子,這個(gè)Animal結(jié)構(gòu)體,怎么構(gòu)造出一個(gè)實(shí)例對(duì)象

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)
簡(jiǎn)單易懂,結(jié)構(gòu)體有哪些屬性字段,那么構(gòu)造函數(shù)的參數(shù),就相應(yīng)做定義并傳入

帶來(lái)的問(wèn)題:

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

新結(jié)構(gòu)體定義:

type Animal struct {
  Name   string
  Age    int
  Height int
  Weight int
  CanRun bool
  LegNum int
}
代碼實(shí)現(xiàn)(函數(shù)加新參數(shù)定義,但默認(rèn)值貌似實(shí)現(xiàn)不了,得調(diào)用構(gòu)造函數(shù)時(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ù)逐步加新字段,這個(gè)構(gòu)造函數(shù)就會(huì)被撐爆了,如果調(diào)用的地方越多,那么越傷筋動(dòng)骨。






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

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

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

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

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

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

輸出結(jié)果:
&{大黃狗 10 120 0 true 4}
&{大灰狼 0 200 0 true 4}
帶來(lái)的好處:

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

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

核心代碼:

type Options struct {
  AppID                      string               // appid
  Cluster                    string               // 默認(rèn)的集群名稱,默認(rèn):default
  DefaultNamespace           string               // Get時(shí)默認(rèn)使用的命名空間,如果設(shè)置了該值,而不在PreloadNamespaces中,默認(rèn)也會(huì)加入初始化邏輯中
  PreloadNamespaces          []string             // 預(yù)加載命名空間,默認(rèn):為空
  ApolloClient               ApolloClient         // apollo HTTP api實(shí)現(xiàn)
  Logger                     Logger               // 日志實(shí)現(xiàn)類,可以設(shè)置自定義實(shí)現(xiàn)或者通過(guò)NewLogger()創(chuàng)建并設(shè)置有效的io.Writer,默認(rèn): ioutil.Discard
  AutoFetchOnCacheMiss       bool                 // 自動(dòng)獲取非預(yù)設(shè)以外的Namespace的配置,默認(rèn):false
  LongPollerInterval         time.Duration        // 輪訓(xùn)間隔時(shí)間,默認(rèn):1s
  BackupFile                 string               // 備份文件存放地址,默認(rèn):.agollo
  FailTolerantOnBackupExists bool                 // 服務(wù)器連接失敗時(shí)允許讀取備份,默認(rèn):false
  Balancer                   Balancer             // ConfigServer負(fù)載均衡
  EnableSLB                  bool                 // 啟用ConfigServer負(fù)載均衡
  RefreshIntervalInSecond    time.Duration        // ConfigServer刷新間隔
  ClientOptions              []ApolloClientOption // 設(shè)置apollo HTTP api的配置項(xiàng)
  EnableHeartBeat            bool                 // 是否允許兜底檢查,默認(rèn):false
  HeartBeatInterval          time.Duration        // 兜底檢查間隔時(shí)間,默認(rèn):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ù)作為選項(xiàng)
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結(jié)構(gòu)體,定義出apollo需要使用到的所有配置字段;
定義一系列函數(shù)作為選項(xiàng),對(duì)配置字段做初始化設(shè)置(例如,設(shè)置容災(zāi)文件路徑、預(yù)加載的namespace、輪訓(xùn)間隔時(shí)間等等);
構(gòu)造函數(shù)里初始化一個(gè)Options的實(shí)例對(duì)象,并且根據(jù)傳入的函數(shù)選項(xiàng),進(jìn)行配置字段的更新,最終返回這個(gè)實(shí)例對(duì)象;
獲取到實(shí)例對(duì)象,調(diào)用相應(yīng)的方法做相應(yīng)的操作。
總結(jié)
由淺入深的講解了下實(shí)例對(duì)象初始化一般寫法和高階寫法。用好這個(gè)高階寫法(函數(shù)選項(xiàng)模式),讓代碼更優(yōu)雅。還不會(huì)使用的Gopher,趕緊學(xué)起來(lái),用起來(lái)。

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



請(qǐng)前往:http://lygongshang.com/TeacherV2.html?id=365