云原生技術分享不僅僅局限于Go
、Rust
、Python
、Istio
、containerd
、CoreDNS
、Envoy
、etcd
、Fluentd
、Harbor
、Helm
、Jaeger
、Kubernetes
、OpenPolicyAgent
、Prometheus
、Rook
、TiKV
、TUF
、Vitess
、Arg
o
、Buildpacks
、CloudEvents
、CNI
、Contour
、Cortex
、CRI-O
、Falco
、Flux
、gRPC
、KubeEdge
、Linkerd
、NATS
、Notary
、OpenTracing
、Operator
Framework
、SPIFFE
、SPIRE
和Thanos
等
Go 中的構建器模式
在 Operator
條件更新上應用 Go
風格的構建器模式的實際示例
建議我們在某個“框架”內進行編碼,即遵循一定的設計模式,這些模式是有效的、可復制的、被廣泛認可的、更容易理解和應用的。
為什么要設計模式
為了不那么抽象,我們從實踐中的一個例子開始。
通常,我們定義一個struct
,然后在使用它時對其進行初始化。
typeAstruct{
namestring
}
a:=A{name:“abc”}
這是常見的用法,但不適用于A
復雜的場景
- 多層嵌套字段
- 超過 5 個字段
- 不同的字段需要不同的默認值
- 多個可選字段
- 以上四種的組合
例如在Kubernetes operator
開發中,我們調用SetStatusAndCondition
來更新資源信息,其中不僅包含了metav1
的基本信息。條件,如狀態,原因,觀察生成等,但也傳遞回調函數,如OnSuccess
和OnError
。圍繞ConditionAndStatus
,我們可以添加其他邏輯,比如發送事件、處理不同狀態(成功或失敗)的邏輯,等等,然后定義一個類似如下的結構。
typeConditionAndStatusstruct{
Conditionmetav1.Condition
EventTypestring//eventtype
Recorderrecord.EventRecorder,//K8seventsrecorder
Forcebool,//isForceupdate
OnErrorfunc,//errhandler
OnSuccesfunc,//successhandler
}
它可以通過通過new
初始化這個ConditionAndStatus
來工作,但是當有超過5
個字段并且其中兩個是嵌套的時候,它是累贅和復雜的,這是對非調用者友好的,并且在代碼可讀性上很差。除非condition
和eventRecorder
被實例化,否則調用者不能實例化ConditionAndStatus
。調用者需要知道所有的實現細節,例如,他們應該知道在錯誤處理中更新條件時傳遞onSucc
方法,即使只有nil
。此外,不同的用戶在不同的地方執行初始化時,每次都需要傳入相同的onSucc
和onErr
。
那么我們該如何優化這段代碼呢?
Factory 模式
應用Factory
模式可能是我們想到的第一個想法,但它不適用于這種情況。
通過Factory
模式封裝一些創建方法。
//Createnofalse,nodefaulthandlers
func(cConditionAndStatus)Create(condmetav1.Condition,eventTypestring,recorderrecord.EventRecorder)ConditionAndStatus{
returncreate(cond,eventType,recorder,false,nil,nil)
}
//Createnodefaulthandlers
func(cConditionAndStatus)Create(condmetav1.Condition,eventTypestring,recorderrecord.EventRecorder,forcebool)ConditionAndStatus{
returncreate(cond,eventType,recorder,force,nil,nil)
}
//...morecreatefunctions
func(cConditionAndStatus)Create(condmetav1.Condition,eventTypestring,recorderrecord.EventRecorder,forcebool,onErrfunc,onSuccfunc)ConditionAndStatus{
returnConditionAndStatus{
condtion:cond,
eventType:eventType,
recorder:recorder,
force:force,
onErr:onErr,
onSucces:onSucc,
}
}
api
應該易于使用且不易誤用——來自Josh Bloch
然而,Factory
模式實現的api
并不是那么方便。
顯然,create
不是一個選項,因為它需要提供所有參數,傳入的參數越多,操作就越困難。此外,當多個參數為同一類型時,很容易出錯。
盡管其他Factory
方法可以通過提供一些默認值來減少傳入的參數來降低復雜性,但它們仍然缺乏靈活性。添加參數后,需要修改所有create *
方法。
Builder模式
Builder
模式是一種設計模式,旨在為面向對象編程中的各種對象創建問題提供靈活的解決方案
來自https://en.wikipedia.org/wiki/Builder_pattern。
構建器模式為靈活簡化復雜對象的創建鋪平了道路,同時也隱藏了嵌入式類型的一些初始化細節,大大提高了可讀性。
Builder接口
builder
接口是兩種builder
模式實現之一,buildxxx
用接口實現各個字段的方法,Builder
通過多態性確定具體的builder
。請參閱下面的 UML
流程圖。
讓我們“翻新”以前的ConditionAndStatus
.
typeConditionAndStatusBuilderinterface{
SetCondtion(condmetav1.Condition)ConditionAndStatusBuilder
SetEventType(evnetTypestring)ConditionAndStatusBuilder
SetRecorder(recorderrecord.EventRecorder)ConditionAndStatusBuilder
SetForce(forcebool)ConditionAndStatusBuilder
SetOnErr(onErrfunc())ConditionAndStatusBuilder
SetOnSuccess(onSuccfunc())ConditionAndStatusBuilder
Build()ConditionAndStatus
}
typeDefaultBuilderstruct{
conditionmetav1.Condition
eventTypestring//eventtype
recorderrecord.EventRecorder,//K8seventsrecorder
forcebool,//isForceupdate
onErrorfunc,//errhandler
onSuccesfunc,//successhandler
}
func(b*DefaultBuilder)SetCondtion(condmetav1.Condition)DefaultBuilder{
b.condition=cond
returnb
}
//...moresetfuncs
func(b*DefaultBuilder)Build()ConditionAndStatus{
//setsomedefaultvalues
b.force=true
returnConditionAndStatus{
condition:b.condtion,
//...
}
}
要創建ConditionAndStatus
,您可以使用注冊方法組成所有構建器,然后通過getByName
獲得特定的構建器。
不難得出結論,該模式與Factory
模式非常相似,因為每個構建器仍然需要創建所有字段或提供默認值。但它確實向前邁出了一步。
- 當字段確定時,它可以靈活地添加新的生成器,而不需要修改舊的生成器。
- 它可以控制創建不同字段的順序。如果字段是相互依賴的,它可以隱藏細節并防止調用者犯錯誤。
然而,它與Factory
模式有相同的缺點:一旦添加了字段(在Builder
接口中添加方法),就需要修改所有構建器。
Pipeline建設者
另一種構建器模式是管道構建器,它更常見。通過上面的接口builder
,你會發現多builder
的設計是多余的,而讓調用者控制相關字段的分配更合理:唯一的一個builder
管理所有字段初始化,并通過返回builder
本身來構建管道在每一步中,最后都組裝成我們想要的。
通用調用代碼的格式為obj.Withxxx().Withyyy().Withzzz().build()
. 更改ConditionAndStatus
如下。
typeBuilderstruct{
conditionmetav1.Condition
eventTypestring//eventtype
recorderrecord.EventRecorder,//K8seventsrecorder
forcebool,//isForceupdate
onErrorfunc,//errhandler
onSuccesfunc,//successhandler
}
func(b*Builder)WithCondition(condmetav1.Condition)Builder{
b.condition=cond
returnb
}
//...moreWithxxxfuncs
func(b*Builder)Build()ConditionAndStatus{
//setsomedefaultvalues
b.force=true
returnConditionAndStatus{
condition:b.condtion,
//...
}
}
Pipeline builder
巧妙地避免了添加新字段帶來的麻煩。只有一個builder
,它可以通過添加With*
方法輕松處理字段添加。
它對現有的調用者絕對更友好。如果參數是可選的,則不需要修改其他調用者的代碼。而你只有通過添加新的調用者并With*
在調用時插入方法來完成它;但是,當需要新參數而沒有默認值時,則需要修改所有調用者的代碼。
當然,沒有一種模式是沒有缺陷的,管道構建器也不是。
-
Withxxx()
一旦要構建許多字段,堆積的方法會給調用者帶來麻煩并降低可讀性。 - 無法控制字段的初始化順序。如果存在依賴關系,則需要出色的錯誤控制和文檔來避免錯誤。
-
代碼不是
Go
風格,而是更多Java
風格。
可選的構建器模式
如果我們進一步優化管道構建器會怎樣?正如Dave Cheney
在他的Practical Go
中提到的那樣,我們應該以更多 Go
的方式嘗試它。
首選 var args
到 []T
參數
深入挖掘,我們看到這里的大部分字段都是可選的,并且可以var args
自然地定義。如有傳入,申報;如果沒有,請忽略它。因此,builder/factory
當隱藏實現細節時,只需要一種方法來處理整個對象的創建。
讓我們一步一步地把這個想法付諸實踐。
將可選字段抽象到構建結構中,而不是將所有字段都放入。要將ConditionAndStatus
轉換為以下結構,其中配置包含所有可選字段。
typeConditionAndStatusstruct{
conditionmetav1.Condition
eventTypestring//eventtype
recorderrecord.EventRecorder,//K8seventsrecorder
configsconfigs//Optionalconfigs
}
typeconfigsstruct{
forcebool,//isForceupdate
onErrorfunc,//errhandler
onSuccesfunc,//successhandler
}
對于配置,使用func
選項接受一個*configs
并返回自身以集成到管道中。需要使用以下方法。
typeconfigsstruct{
forcebool,//isForceupdate
onErrorfunc,//errhandler
onSuccesfunc,//successhandler
}
typeOptionfunc(*configs)
funcForceUpdate(forcebool)Option{returnfunc(c*configs){c.force=force}}
funcOnErr(onErrfunc())Option{returnfunc(c*configs){c.onErr=onErr}}
funcOnSuccess(onSuccfunc())Option{returnfunc(c*configs){c.onSuccess=onSucc}}
然后是新的create
方法,包括必要字段和可選配置的初始化。因為所有可選的配置都是用func
類型的返回值初始化的,所以整個配置的賦值只能用一個循環來完成。超級簡潔!
funcCreate(conditionmetav1.Condition,eventTypestring,recorderrecord.EventRecorder,os...Option)error{
opts:=configs{
force:false,
onSuccess:func(){},
onError:nil,
}
//Applyalltheoptionalconfigs
for_,applyOption:=rangeos{
applyOption(&opts)
}
//checkrequiredfields
//updateconditionshere
//handleerr
ifopts.err!=nil{
returnopts.onError()
}
//eveutallycallsuccessfunc
opts.onSuccess()
}
調用方可以根據場景選擇可選配置,避免誤用。
setCondition(
metav1.Condition{
Type:apis.Ready,
Status:metav1.ConditionFalse,
Reason:apis.UpstreamUnavailable,
Message:fmt.Sprintf("Failedtosetresources%#v",resource),
},
"Update",
nil,
//onlyneedonErrfuncfromtheoptionalconfigs.
conditionAndStatus.ForOnErr(err),
)
Builder in Kubernetes
在Kubernetes
源代碼的幾乎每個角落都可以看到這種go
風格的代碼。幾乎所有的結構被*配置是建立在可選的建造者模式,如PodApplyConfiguration EventApplyConfiguration
和配置文檔你找到包裹。這些逐層嵌套配置獲得最終值與一個或多個方法類似于PodApplyConfiguration
提取。
最后
設計模式是經典的,盡管不是所有的模式都能在Go
中完美實現。Builder
無疑是其中最杰出的一個,我們應該最大限度地利用Optional
管道生成器模式來構建一些配置對象,特別是在設計公共模塊時。使用靈活、遵守代碼標準和擴展友好的api
,可以大大減輕升級壓力。
感謝你的閱讀!
原文標題:Go 中的構建器模式
文章出處:【微信公眾號:馬哥Linux運維】歡迎添加關注!文章轉載請注明出處。
-
源代碼
+關注
關注
96文章
2944瀏覽量
66673 -
設計模式
+關注
關注
0文章
53瀏覽量
8623 -
Struct
+關注
關注
0文章
31瀏覽量
10859 -
云原生
+關注
關注
0文章
242瀏覽量
7939
原文標題:Go 中的構建器模式
文章出處:【微信號:magedu-Linux,微信公眾號:馬哥Linux運維】歡迎添加關注!文章轉載請注明出處。
發布評論請先 登錄
相關推薦
評論