不會函數(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