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