設計模式 in Go: Builder

建造模式,處理與創建對象及對象實例化過程相關的問題,通常尋求以分離業務代碼和對象創建邏輯,或將複雜的構造邏輯封裝在可重用組件中的方式。

讓我們繼續來學習第 3 種創建型模式——Builder 模式(建造者模式)。

問題背景:

創建的對象有時比較複雜,可能需要按照特定順序對所有依賴項進行逐一構建,創建依賴項後再最終構建期望的對象。創建這些最終對象及依賴對象,需要很多參數。如果通過構造函數的參數列表傳入所有參數,那麼參數列表太長無法被理解、維護和不方便調用。

爲了提高可讀性和使用上的便捷,我們需要考慮將構造函數參數劃分成多個小部分或者採取其他方式來簡化創建過程。

將所有參數打包到一個名爲 BuildConfig 的結構體中,然後通過該結構體作爲參數傳遞,這樣確實可以縮短參數列表的長度,但並沒有解決真正存在的問題。

  1. 依賴可能是必需的或選擇性的,即一些參數可能是必須的,而另一些則不是。

  2. 當創建特定依賴項所需的參數因情況而異時,我們可以採取一些策略來確保這些參數能被正確地傳遞和處理。

解決方案:

使用 Builder 模式逐步構建最終的對象。不要通過複雜類自己的構造函數創建複雜對象,而是將創建邏輯移到一個單獨的 Builder 類中。這個構建器會逐步地構建複雜的對象。

此解決方案簡化了複雜的創建邏輯,消除長參數列表,使其易於理解和調用。

讓我們用建造一棟房子爲例。一棟房子包含地基、牆壁、屋頂、窗戶和門,等等。那就是所有的例子了。好了,在建造牆壁和屋頂之前,我們應該先建造地基並確保其足夠堅固,然後再來建造牆壁,需要留出空間供窗戶和門。然後是屋頂,最後才能建造窗戶和門。在這裏不需要裝飾設計。好了,那美麗的房子就完工啦😍

這是一個 UML 類圖,用來描繪建造過程。您可以後來通過源代碼並運行測試。

變體模式:
1. 使用 golang 中常見的 option patterns,options 列表通過函數傳入,可以是對構建參數的調整,也可以是一個構建動作,看具體怎麼實現。

優點:

  1. builder 能夠解決構建複雜對象的難題。
  2. 可維護性好,參數列表不會過於冗長。
  3. 靈活性,可以快速響應業務需求變更。
  4. 可擴展性, 可以自信地支持不同需求,需求即使再變也會侷限於某些個 phases 中,或者某段代碼中。

缺點:

  1. 維護 builder 對象本身也是一種複雜。
  2. 如果要構建的對象並不複雜,過度使用 builder 模式。
  3. 測試要爲每個構建子步驟設設計用例,需花更多時間來測試。

Source:

////////////////////////// builder.go /////////////////////////
package builder

type builder struct {
 house House
}

func NewBuilder() *builder {
 return &builder{}
}

func (b *builder) BuildRooves(style RoofStyle) *builder {
 // build the rooves according to the walls' info
 return b
}

func (b *builder) BuildWall(pos Pos, color ColorStyle) *Wall {
 return &Wall{
  Pos:   pos,
  Color: color,
 }
}

func (w *Wall) BuildWindow(pos Pos, height, width int, style GlassStyle) *Wall {
 w.Window = append(w.Window, Window{
  Pos:    pos,
  Height: height,
  Width:  width,
  Glass:  style,
 })
 return w
}

func (w *Wall) BuildDoor(pos Pos, height, width int, style MaterialStyle, color ColorStyle) *Wall {
 w.Door = append(w.Door, Door{
  Pos:      pos,
  Material: style,
  Height:   height,
  Width:    width,
  Color:    color,
 })
 return w
}

func (b *builder) BuildFloor(pos []Pos, style MaterialStyle) *builder {
 b.house.Floor = append(b.house.Floor, Floor{
  Vetexes:  pos,
  Material: style,
 })
 return b
}

func (b *builder) Build() House {
 // some other building stuffs
 return b.house
}

/////////////////////////// house.go ////////////////////////////
package builder

type House struct {
 Floor  []Floor
 Walls  []Wall
 Rooves []Roof
}

type Wall struct {
 Pos
 Window []Window
 Door   []Door
 Color  ColorStyle
}

type Floor struct {
 Vetexes  []Pos
 Material MaterialStyle
}

type Roof struct {
 Style RoofStyle
}

type Window struct {
 Pos
 Height int
 Width  int
 Glass  GlassStyle
}

type Door struct {
 Pos
 Material MaterialStyle
 Height   int
 Width    int
 Color    ColorStyle
}

type MaterialStyle int
type ColorStyle int
type GlassStyle int
type RoofStyle int

type Pos struct {
 X int
 Y int
 Z int
}

///////////////////////////// orderer.go /////////////////////////////
package builder

type Orderer struct {
 b       *builder
 Floors  []Floor
 Wall    []Wall
 Windows []Window
 Doors   []Door
}

func NewOrderer(b *builder) *Orderer {
 return &Orderer{b: b}
}

func (ord *Orderer) WantFloor(vetexes []Pos, style MaterialStyle) {
 ord.Floors = append(ord.Floors, Floor{
  Vetexes:  vetexes,
  Material: style,
 })
}

func (ord *Orderer) WantWindow(pos Pos, height, width int, style GlassStyle) {
 ord.Windows = append(ord.Windows, Window{
  Pos:    pos,
  Height: height,
  Width:  width,
  Glass:  style,
 })
}

func (ord *Orderer) WantWall(pos Pos, color ColorStyle) {
 ord.Wall = append(ord.Wall, Wall{
  Pos:    pos,
  Window: nil,
  Door:   nil,
  Color:  0,
 })
}

func (ord *Orderer) Build() House {
 // prepare the building elements, like which windows and doors belongs to the 1st wall, 2nd wall, etc.
 // ...

 // then:
 // - call ord.b.BuildFloor
 // - call ord.b.BuildWall with ord.b.BuildWindow and ord.b.BuildDoor
 // - call ord.b.BuildRooves
 // - call ord.b.Build() to get the final House

 return ord.b.Build()
}

現在來看下測試:

////////////////////////// builder_test.go ///////////////////////////
package builder_test

import (
 "fmt"
 "testing"

 "builder"
)

func Test_Creational_Builder(t *testing.T) {
 bd := builder.NewBuilder()
 bd.BuildFloor([]builder.Pos{{0, 0, 0}, {0, 10, 0}, {10, 10, 0}, {10, 0, 0}}, builder.MaterialStyle(0))
 bd.BuildWall(builder.Pos{0, 0, 0}, builder.ColorStyle(0)).
  BuildDoor(builder.Pos{0, 1, 0}, 2, 1, builder.MaterialStyle(0), builder.ColorStyle(0)).
  BuildWindow(builder.Pos{0, 2, 0}, 1, 1, builder.GlassStyle(0))
 //builder another 3 walls with doors and windows
 //bd.BuildWall(...).
 //  BuildDoor(...).
 //  Buildwindow(...)
 bd.BuildRooves(builder.RoofStyle(0))
 house := bd.Build()
 fmt.Printf("The house is built: %+v", house)
}

func Test_Creational_BuilderWithOrderer(t *testing.T) {
 // actually we should always limit the building order: floor -> wall -> rooves
 // we must build the house to avoid the house collapse.
 //
 // so we can import an orderer which limits the building order.
 ord := builder.NewOrderer(builder.NewBuilder())
 ord.WantFloor(...)
 ord.WantWall(...)
 ord.WantWindow(...)
 ord.WantDoor(...)
 
 house := ord.Build()
 fmt.Printf("The house is built: %+v", house)
}

ps:此處我們優先考慮可讀性,不會太關注編碼標準,如註釋、camelCase 類型名等。我們將多個文件的代碼組織到一個 codeblock 中僅僅是爲了方便閱讀,如果您想測試可以通過 git 下載源碼 github.com/hitzhangjie/go-patterns。

在本文中,我們學習了 Builder 模式如何解決創建複雜對象的問題——分多步完成。Builder 模式被廣泛使用,很多項目都可以看到它的身影。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/b9slmdJ6woBcNU0LKXcNDQ