實踐GoF的23種設計模式:建造者模式
摘要:針對這種對象成員較多,創建對象邏輯較為繁瑣的場景,非常適合使用建造者模式來進行優化。
本文分享自華為雲社區《【Go實現】實踐GoF的23種設計模式:建造者模式》,作者: 元閏子。
簡述
在程序設計中,我們會經常遇到一些複雜的對象,其中有很多成員屬性,甚至嵌套着多個複雜的對象。這種情況下,創建這個複雜對象就會變得很繁瑣。對於 C++/Java 而言,最常見的表現就是構造函數有着長長的參數列表:
MyObject obj = new MyObject(param1, param2, param3, param4, param5, param6, ...)
對於 Go 語言來說,最常見的表現就是多層的嵌套實例化:
obj := &MyObject{ Field1: &Field1 { Param1: &Param1 { Val: 0, }, Param2: &Param2 { Val: 1, }, ... }, Field2: &Field2 { Param3: &Param3 { Val: 2, }, ... }, ... }
上述的對象創建方法有兩個明顯的缺點:(1)對使用者不友好,使用者在創建對象時需要知道的細節太多;(2)代碼可讀性很差。
針對這種對象成員較多,創建對象邏輯較為繁瑣的場景,非常適合使用建造者模式來進行優化。
建造者模式的作用有如下幾個:1、封裝複雜對象的創建過程,使對象使用者不感知複雜的創建邏輯。
2、可以一步步按照順序對成員進行賦值,或者創建嵌套對象,並最終完成目標對象的創建。
3、對多個對象復用同樣的對象創建邏輯。
其中,第1和第2點比較常用,下面對建造者模式的實現也主要是針對這兩點進行示例。
UML 結構
代碼實現
示例
在簡單的分佈式應用系統(示例代碼工程)中,我們定義了服務註冊中心,提供服務註冊、去註冊、更新、 發現等功能。要實現這些功能,服務註冊中心就必須保存服務的信息,我們把這些信息放在了 ServiceProfile
這個數據結構上,定義如下:
// demo/service/registry/model/service_profile.go // ServiceProfile 服務檔案,其中服務ID唯一標識一個服務實例,一種服務類型可以有多個服務實例 type ServiceProfile struct { Id string // 服務ID Type ServiceType // 服務類型 Status ServiceStatus // 服務狀態 Endpoint network.Endpoint // 服務Endpoint Region *Region // 服務所屬region Priority int // 服務優先級,範圍0~100,值越低,優先級越高 Load int // 服務負載,負載越高表示服務處理的業務壓力越大 } // demo/service/registry/model/region.go // Region 值對象,每個服務都唯一屬於一個Region type Region struct { Id string Name string Country string } // demo/network/endpoint.go // Endpoint 值對象,其中ip和port屬性為不可變,如果需要變更,需要整對象替換 type Endpoint struct { ip string port int }
實現
如果按照直接實例化方式應該是這樣的:
// 多層的嵌套實例化 profile := &ServiceProfile{ Id: "service1", Type: "order", Status: Normal, Endpoint: network.EndpointOf("192.168.0.1", 8080), Region: &Region{ // 需要知道對象的實現細節 Id: "region1", Name: "beijing", Country: "China", }, Priority: 1, Load: 100, }
雖然 ServiceProfile
結構體嵌套的層次不多,但是從上述直接實例化的代碼來看,確實存在對使用者不友好和代碼可讀性較差的缺點。比如,使用者必須先對 Endpoint
和 Region
進行實例化,這實際上是將 ServiceProfile
的實現細節暴露給使用者了。
下面我們引入建造者模式對代碼進行優化重構:
// demo/service/registry/model/service_profile.go // 關鍵點1: 為ServiceProfile定義一個Builder對象 type serviceProfileBuild struct { // 關鍵點2: 將ServiceProfile作為Builder的成員屬性 profile *ServiceProfile } // 關鍵點3: 定義構建ServiceProfile的方法 func (s *serviceProfileBuild) WithId(id string) *serviceProfileBuild { s.profile.Id = id // 關鍵點4: 返回Builder接收者指針,支持鏈式調用 return s } func (s *serviceProfileBuild) WithType(serviceType ServiceType) *serviceProfileBuild { s.profile.Type = serviceType return s } func (s *serviceProfileBuild) WithStatus(status ServiceStatus) *serviceProfileBuild { s.profile.Status = status return s } func (s *serviceProfileBuild) WithEndpoint(ip string, port int) *serviceProfileBuild { s.profile.Endpoint = network.EndpointOf(ip, port) return s } func (s *serviceProfileBuild) WithRegion(regionId, regionName, regionCountry) *serviceProfileBuild { s.profile.Region = &Region{Id: regionId, Name: regionName, Country: regionCountry} return s } func (s *serviceProfileBuild) WithPriority(priority int) *serviceProfileBuild { s.profile.Priority = priority return s } func (s *serviceProfileBuild) WithLoad(load int) *serviceProfileBuild { s.profile.Load = load return s } // 關鍵點5: 定義Build方法,在鏈式調用的最後調用,返回構建好的ServiceProfile func (s *serviceProfileBuild) Build() *ServiceProfile { return s.profile } // 關鍵點6: 定義一個實例化Builder對象的工廠方法 func NewServiceProfileBuilder() *serviceProfileBuild { return &serviceProfileBuild{profile: &ServiceProfile{}} }
實現建造者模式有 6 個關鍵點:
- 為
ServiceProfile
定義一個 Builder 對象serviceProfileBuild
,通常我們將它設計為包內可見,來限制客戶端的濫用。 - 把需要構建的
ServiceProfile
作為 Builder 對象serviceProfileBuild
的成員屬性,用來存儲構建過程中的狀態。 - 為 Builder 對象
serviceProfileBuild
定義用來構建ServiceProfile
的一系列方法,上述代碼中我們使用了WithXXX
的風格。 - 在構建方法中返回 Builder 對象指針本身,也即接收者指針,用來支持鏈式調用,提升客戶端代碼的簡潔性。
- 為 Builder 對象定義 Build() 方法,返回構建好的
ServiceProfile
實例,在鏈式調用的最後調用。 - 定義一個實例化 Builder 對象的工廠方法
NewServiceProfileBuilder()
。
那麼,使用建造者模式實例化邏輯是這樣的:
// 建造者模式的實例化方法 profile := NewServiceProfileBuilder(). WithId("service1"). WithType("order"). WithStatus(Normal). WithEndpoint("192.168.0.1", 8080). WithRegion("region1", "beijing", "China"). WithPriority(1). WithLoad(100). Build()
當使用建造者模式來進行對象創建時,使用者不再需要知道對象具體的實現細節(這裡體現為無須預先實例化 Endpoint
和 Region
對象),代碼可讀性、簡潔性也更好了。
擴展
Functional Options 模式
進一步思考,其實前文提到的建造者實現方式,還有 2 個待改進點:
- 我們額外新增了一個 Builder 對象,如果能夠把 Builder 對象省略掉,同時又能避免長長的入參列表就更好了。
- 熟悉 Java 的同學應該能夠感覺出來,這種實現具有很強的「Java 風格」。並非說這種風格不好,而是在 Go 中理應有更具「Go 風格」的建造者模式實現。
針對這兩點,我們可以通過 Functional Options 模式 來優化。Functional Options 模式也是用來構建對象的,這裡我們也把它看成是建造者模式的一種擴展。它利用了 Go 語言中函數作為一等公民的特點,結合函數的可變參數,達到了優化上述 2 個改進點的目的。
使用 Functional Options 模式的實現是這樣的:
// demo/service/registry/model/service_profile_functional_options.go // 關鍵點1: 定義構建ServiceProfile的functional option,以*ServiceProfile作為入參的函數 type ServiceProfileOption func(profile *ServiceProfile) // 關鍵點2: 定義實例化ServiceProfile的工廠方法,使用ServiceProfileOption作為可變入參 func NewServiceProfile(svcId string, svcType ServiceType, options ...ServiceProfileOption) *ServiceProfile { // 關鍵點3: 可為特定的字段提供默認值 profile := &ServiceProfile{ Id: svcId, Type: svcType, Status: Normal, Endpoint: network.EndpointOf("192.168.0.1", 80), Region: &Region{Id: "region1", Name: "beijing", Country: "China"}, Priority: 1, Load: 100, } // 關鍵點4: 通過ServiceProfileOption來修改字段 for _, option := range options { option(profile) } return profile } // 關鍵點5: 定義一系列構建ServiceProfile的方法,在ServiceProfileOption實現構建邏輯,並返回ServiceProfileOption func Status(status ServiceStatus) ServiceProfileOption { return func(profile *ServiceProfile) { profile.Status = status } } func Endpoint(ip string, port int) ServiceProfileOption { return func(profile *ServiceProfile) { profile.Endpoint = network.EndpointOf(ip, port) } } func SvcRegion(svcId, svcName, svcCountry string) ServiceProfileOption { return func(profile *ServiceProfile) { profile.Region = &Region{ Id: svcId, Name: svcName, Country: svcCountry, } } } func Priority(priority int) ServiceProfileOption { return func(profile *ServiceProfile) { profile.Priority = priority } } func Load(load int) ServiceProfileOption { return func(profile *ServiceProfile) { profile.Load = load } }
實現 Functional Options 模式有 5 個關鍵點:
- 定義 Functional Option 類型
ServiceProfileOption
,本質上是一個入參為構建對象ServiceProfile
的指針類型。(注意必須是指針類型,值類型無法達到修改目的) - 定義構建
ServiceProfile
的工廠方法,以ServiceProfileOption
的可變參數作為入參。函數的可變參數就意味着可以不傳參,因此一些必須賦值的屬性建議還是定義對應的函數入參。 - 可為特定的屬性提供默認值,這種做法在 為配置對象賦值的場景 比較常見。
- 在工廠方法中,通過
for
循環利用ServiceProfileOption
完成構建對象的賦值。 - 定義一系列的構建方法,以需要構建的屬性作為入參,返回
ServiceProfileOption
對象,並在ServiceProfileOption
中實現屬性賦值。
Functional Options 模式 的實例化邏輯是這樣的:
// Functional Options 模式的實例化邏輯 profile := NewServiceProfile("service1", "order", Status(Normal), Endpoint("192.168.0.1", 8080), SvcRegion("region1", "beijing", "China"), Priority(1), Load(100))
相比於傳統的建造者模式,Functional Options 模式的使用方式明顯更加的簡潔,也更具「Go 風格」了。
Fluent API 模式
前文中,不管是傳統的建造者模式,還是 Functional Options 模式,我們都沒有限定屬性的構建順序,比如:
// 傳統建造者模式不限定屬性的構建順序 profile := NewServiceProfileBuilder(). WithPriority(1). // 先構建Priority也完全沒問題 WithId("service1"). ... // Functional Options 模式也不限定屬性的構建順序 profile := NewServiceProfile("service1", "order", Priority(1), // 先構建Priority也完全沒問題 Status(Normal), ...
但是在一些特定的場景,對象的屬性是要求有一定的構建順序的,如果違反了順序,可能會導致一些隱藏的錯誤。
當然,我們可以與使用者的約定好屬性構建的順序,但這種約定是不可靠的,你很難保證使用者會一直遵守該約定。所以,更好的方法應該是通過接口的設計來解決問題, Fluent API 模式 誕生了。
下面,我們使用 Fluent API 模式進行實現:
// demo/service/registry/model/service_profile_fluent_api.go type ( // 關鍵點1: 為ServiceProfile定義一個Builder對象 fluentServiceProfileBuilder struct { // 關鍵點2: 將ServiceProfile作為Builder的成員屬性 profile *ServiceProfile } // 關鍵點3: 定義一系列構建屬性的fluent接口,通過方法的返回值控制屬性的構建順序 idBuilder interface { WithId(id string) typeBuilder } typeBuilder interface { WithType(svcType ServiceType) statusBuilder } statusBuilder interface { WithStatus(status ServiceStatus) endpointBuilder } endpointBuilder interface { WithEndpoint(ip string, port int) regionBuilder } regionBuilder interface { WithRegion(regionId, regionName, regionCountry string) priorityBuilder } priorityBuilder interface { WithPriority(priority int) loadBuilder } loadBuilder interface { WithLoad(load int) endBuilder } // 關鍵點4: 定義一個fluent接口返回完成構建的ServiceProfile,在最後調用鏈的最後調用 endBuilder interface { Build() *ServiceProfile } ) // 關鍵點5: 為Builder定義一系列構建方法,也即實現關鍵點3中定義的Fluent接口 func (f *fluentServiceProfileBuilder) WithId(id string) typeBuilder { f.profile.Id = id return f } func (f *fluentServiceProfileBuilder) WithType(svcType ServiceType) statusBuilder { f.profile.Type = svcType return f } func (f *fluentServiceProfileBuilder) WithStatus(status ServiceStatus) endpointBuilder { f.profile.Status = status return f } func (f *fluentServiceProfileBuilder) WithEndpoint(ip string, port int) regionBuilder { f.profile.Endpoint = network.EndpointOf(ip, port) return f } func (f *fluentServiceProfileBuilder) WithRegion(regionId, regionName, regionCountry string) priorityBuilder { f.profile.Region = &Region{ Id: regionId, Name: regionName, Country: regionCountry, } return f } func (f *fluentServiceProfileBuilder) WithPriority(priority int) loadBuilder { f.profile.Priority = priority return f } func (f *fluentServiceProfileBuilder) WithLoad(load int) endBuilder { f.profile.Load = load return f } func (f *fluentServiceProfileBuilder) Build() *ServiceProfile { return f.profile } // 關鍵點6: 定義一個實例化Builder對象的工廠方法 func NewFluentServiceProfileBuilder() idBuilder { return &fluentServiceProfileBuilder{profile: &ServiceProfile{}} }
實現 Fluent API 模式有 6 個關鍵點,大部分與傳統的建造者模式類似:
- 為
ServiceProfile
定義一個 Builder 對象fluentServiceProfileBuilder
。 - 把需要構建的
ServiceProfile
設計為 Builder 對象fluentServiceProfileBuilder
的成員屬性。 - 定義一系列構建屬性的 Fluent 接口,通過方法的返回值控制屬性的構建順序,這是實現 Fluent API 的關鍵。比如
WithId
方法的返回值是typeBuilder
類型,表示緊隨其後的就是WithType
方法。 - 定義一個 Fluent 接口(這裡是
endBuilder
)返回完成構建的ServiceProfile
,在最後調用鏈的最後調用。 - 為 Builder 定義一系列構建方法,也即實現關鍵點 3 中定義的 Fluent 接口,並在構建方法中返回 Builder 對象指針本身。
- 定義一個實例化 Builder 對象的工廠方法
NewFluentServiceProfileBuilder()
,返回第一個 Fluent 接口,這裡是idBuilder
,表示首先構建的是Id
屬性。
Fluent API 的使用與傳統的建造者實現使用類似,但是它限定了方法調用的順序。如果順序不對,在編譯期就報錯了,這樣就能提前把問題暴露在編譯器,減少了不必要的錯誤使用。
// Fluent API的使用方法 profile := NewFluentServiceProfileBuilder(). WithId("service1"). WithType("order"). WithStatus(Normal). WithEndpoint("192.168.0.1", 8080). WithRegion("region1", "beijing", "China"). WithPriority(1). WithLoad(100). Build() // 如果方法調用不按照預定的順序,編譯器就會報錯 profile := NewFluentServiceProfileBuilder(). WithType("order"). WithId("service1"). WithStatus(Normal). WithEndpoint("192.168.0.1", 8080). WithRegion("region1", "beijing", "China"). WithPriority(1). WithLoad(100). Build() // 上述代碼片段把WithType和WithId的調用順序調換了,編譯器會報如下錯誤 // NewFluentServiceProfileBuilder().WithType undefined (type idBuilder has no field or method WithType)
典型應用場景
建造者模式主要應用在實例化複雜對象的場景,常見的有:
- 配置對象。比如創建 HTTP Server 時需要多個配置項,這種場景通過 Functional Options 模式就能夠很優雅地實現配置功能。
- SQL 語句對象。一些 ORM 框架在構造 SQL 語句時也經常會用到 Builder 模式。比如 xorm 框架中構建一個 SQL 對象是這樣的:
builder.Insert().Into("table1").Select().From("table2").ToBoundSQL()
- 複雜的 DTO 對象。
- …
優缺點
優點
1、將複雜的構建邏輯從業務邏輯中分離出來,遵循了單一職責原則。
2、可以將複雜對象的構建過程拆分成多個步驟,提升了代碼的可讀性,並且可以控制屬性構建的順序。
3、對於有多種構建方式的場景,可以將 Builder 設計為一個接口來提升可擴展性。
4、Go 語言中,利用 Functional Options 模式可以更為簡潔優雅地完成複雜對象的構建。
缺點
1、傳統的建造者模式需要新增一個 Builder 對象來完成對象的構造,Fluent API 模式下甚至還要額外增加多個 Fluent 接口,一定程度上讓代碼更加複雜了。
與其他模式的關聯
抽象工廠模式和建造者模式類似,兩者都是用來構建複雜的對象,但前者的側重點是構建對象/產品族,後者的側重點是對象的分步構建過程。
參考
[1] 【Go實現】實踐GoF的23種設計模式:SOLID原則, 元閏子
[2] Design Patterns, Chapter 3. Creational Patterns, GoF
[3] GO 編程模式:FUNCTIONAL OPTIONS, 酷殼 CoolShell
[4] Fluent API: Practice and Theory, Ori Roth
[5] XORM BUILDER, xorm
[6] 生成器模式, refactoringguru.cn