Builder Pattern,一步一步的建造產品
Builder Pattern,一步一步的建造產品

Builder Pattern,一步一步的建造產品

Tags
Golang
Hey! Go Design Patterns
ithome 2021 ironman
Date
Sep 17, 2021

什麼是 Builder Pattern?

將建造物件的實作拆開,由使用者覺得要選擇建造什麼,來一步一步建造
舉例來說,當創建 PS5 時,
CreatePS5(hasCPU, hasGPU bool)
如果有許多額外的選項,
CreatePS5(CPUCores uint8, has大搖, has藍牙耳機 bool)
就會發現額外的選項需透過零值(zero value)來解決,
CreatePS5(4, false, true)
這使得建構 PS5 的 function 難以快速理解其中的意義。
所以需要額外將這些步驟拆除,再由使用者決定是否要執行,並且將這些步驟拆分,也使得建構 function 不至於過長,
CreatePS5(4).Set大搖(true).// Set藍牙耳機(true) 不執行就不會有藍牙耳機
優點:
  • 將必要的建構參數放在 create function,額外的參數放在 set function,能在程式彈性時也擁有可讀性
  • 不使建構 function 過於複雜
設計上通常有ProductDirectorBuilder InterfaceConcreteBuilder
  • Product: 實際產品
  • Director: 操作 Builder 來生產產品的物件
  • Builder Interface: 生產產品的物件的 interface,由於有此 interface,可以讓 Director 使用不同 ConcreteBuilder 來生產物件
  • ConcreteBuilder: 實際生產產品的物件

問題情境

使用者購買 PS5,並且其中有多個周邊可以購買,使用者自行決定要加購什麼周邊

解決方式

UML 圖後如下:
notion image
程式碼如下:
(相關的 code 在Github - go-design-patterns)
package main import ( "fmt" "strings" ) type PS5Builder interface { SetController(isBuy bool) PS5Builder SetBluetoothHeadphones(isBuy bool) PS5Builder Build() *PS5 } type PS5 struct { cpu string gpu string controller string bluetoothHeadphones string } func CreatePS5() *PS5 { return &PS5{ cpu: "cpu", gpu: "gpu", } } func (p PS5) PlayGame() { var ( accessories []string withAccessories string ) if p.controller != "" { accessories = append(accessories, p.controller) } if p.bluetoothHeadphones != "" { accessories = append(accessories, p.bluetoothHeadphones) } if len(accessories) != 0 { withAccessories = " with " + strings.Join(accessories, ", ") } fmt.Printf("loading...play%s!\n", withAccessories) } type PS5Director struct { Builder PS5Builder } func CreatePS5Director(concretePS5Builder *ConcretePS5Builder) *PS5Director { return &PS5Director{ Builder: concretePS5Builder, } } func (p PS5Director) Construct() *PS5 { return p.Builder. SetController(true). Build() } type ConcretePS5Builder struct { ps5 *PS5 } func CreateConcretePS5Builder() *ConcretePS5Builder { return &ConcretePS5Builder{ ps5: CreatePS5(), } } func (p *ConcretePS5Builder) SetController(isBuy bool) PS5Builder { if isBuy { p.ps5.controller = "FightingStick" } return p } func (p *ConcretePS5Builder) SetBluetoothHeadphones(isBuy bool) PS5Builder { if isBuy { p.ps5.bluetoothHeadphones = "BluetoothHeadphones" } return p } func (p *ConcretePS5Builder) Build() *PS5 { return p.ps5 } func main() { concretePS5Builder := CreateConcretePS5Builder() ps5Director := CreatePS5Director(concretePS5Builder) ps5 := ps5Director.Construct() ps5.PlayGame() }
CreateConcretePS5Builder()建立實際的 builder 後,丟入PS5Director{},而PS5Director{}會在透過Construct()來操作 builder 去 set ps5。
可以發現必填的欄位在CreateConcretePS5Builder()就已經填好,如果是有建構子(construct)的語言,例如 java 就會在建構子內做好,由於 golang 沒有,所以會用 create function 來達到此效果。而額外的欄位即在.SetXXX()function 設置。
另外在.SetXXX()function 後又會 return 自己的方式稱為 Fluent interface,此方法可以做出鏈式的寫法,即.SetXXX().SetXXX(),可以讓 set function 的使用更靈活。