實踐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 個關鍵點:

  1. 為 ServiceProfile 定義一個 Builder 對象 serviceProfileBuild,通常我們將它設計為包內可見,來限制客戶端的濫用。
  2. 把需要構建的 ServiceProfile 作為 Builder 對象 serviceProfileBuild 的成員屬性,用來存儲構建過程中的狀態。
  3. 為 Builder 對象 serviceProfileBuild 定義用來構建 ServiceProfile 的一系列方法,上述代碼中我們使用了 WithXXX 的風格。
  4. 在構建方法中返回 Builder 對象指針本身,也即接收者指針,用來支持鏈式調用,提升客戶端代碼的簡潔性。
  5. 為 Builder 對象定義 Build() 方法,返回構建好的 ServiceProfile 實例,在鏈式調用的最後調用。
  6. 定義一個實例化 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 個待改進點:

  1. 我們額外新增了一個 Builder 對象,如果能夠把 Builder 對象省略掉,同時又能避免長長的入參列表就更好了。
  2. 熟悉 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 個關鍵點:

  1. 定義 Functional Option 類型 ServiceProfileOption,本質上是一個入參為構建對象 ServiceProfile 的指針類型。(注意必須是指針類型,值類型無法達到修改目的)
  2. 定義構建 ServiceProfile 的工廠方法,以 ServiceProfileOption 的可變參數作為入參。函數的可變參數就意味着可以不傳參,因此一些必須賦值的屬性建議還是定義對應的函數入參。
  3. 可為特定的屬性提供默認值,這種做法在 為配置對象賦值的場景 比較常見。
  4. 在工廠方法中,通過 for 循環利用 ServiceProfileOption 完成構建對象的賦值。
  5. 定義一系列的構建方法,以需要構建的屬性作為入參,返回 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 個關鍵點,大部分與傳統的建造者模式類似:

  1. 為 ServiceProfile 定義一個 Builder 對象 fluentServiceProfileBuilder
  2. 把需要構建的 ServiceProfile 設計為 Builder 對象 fluentServiceProfileBuilder 的成員屬性。
  3. 定義一系列構建屬性的 Fluent 接口,通過方法的返回值控制屬性的構建順序,這是實現 Fluent API 的關鍵。比如 WithId 方法的返回值是 typeBuilder 類型,表示緊隨其後的就是 WithType 方法。
  4. 定義一個 Fluent 接口(這裡是 endBuilder)返回完成構建的 ServiceProfile,在最後調用鏈的最後調用。
  5. 為 Builder 定義一系列構建方法,也即實現關鍵點 3 中定義的 Fluent 接口,並在構建方法中返回 Builder 對象指針本身。
  6. 定義一個實例化 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

 

點擊關注,第一時間了解華為雲新鮮技術~