設計模式學習-使用go實現建造者模式

Rick.lz發表於2021-11-06

建造者模式

定義

Builder 模式,中文翻譯為建造者模式或者構建者模式,也有人叫它生成器模式。

建造者模式(Builder Pattern)使用多個簡單的物件一步一步構建成一個複雜的物件。這種型別的設計模式屬於建立型模式,它提供了一種建立物件的最佳方式。

一個 Builder 類會一步一步構造最終的物件。該 Builder 類是獨立於其他物件的。

看了定義還是暈暈的,這裡來個栗子

這裡按照設計模式之美中的資源池的例子來進行討論

假設有這樣一道設計面試題:我們需要定義一個資源池配置類ResourcePoolConfig。這裡的資源池,你可以簡單理解為執行緒池、連線池、物件池等。在這個資源池配置類中,有以下幾個成員變數,也就是可配置項。現在,請你編寫程式碼實現這個ResourcePoolConfig類。

成員變數 解釋 是否必填 預設值
name 資源名稱 沒有
maxTotal 最大資源數量 10
maxIdle 最大空閒資源數量 10
minIdle 最小空閒資源數量 1

很簡單,來看下程式碼實現

const (
	defaultMaxTotal = 10
	defaultMaxIdle  = 10
	defaultMinIdle  = 1
)

// ResourcePoolConfig ...
type ResourcePoolConfig struct {
	name     string
	maxTotal int
	maxIdle  int
	minIdle  int
}

func NewResourcePoolConfig(name string, maxTotal, maxIdle, minIdle *int) (*ResourcePoolConfig, error) {
	rc := &ResourcePoolConfig{
		maxTotal: defaultMaxTotal,
		maxIdle:  defaultMaxIdle,
		minIdle:  defaultMinIdle,
	}
	if name == "" {
		return nil, errors.New("name is empty")
	}
	rc.name = name

	if maxTotal != nil {
		if *maxTotal <= 0 {
			return nil, errors.New("maxTotal should be positive")
		}
		rc.maxTotal = *maxTotal
	}

	if maxIdle != nil {
		if *maxIdle <= 0 {
			return nil, errors.New("maxIdle should not be negative")
		}
		rc.maxIdle = *maxIdle
	}

	if minIdle != nil {
		if *minIdle <= 0 {
			return nil, errors.New("minIdle should not be negative")
		}
		rc.minIdle = *minIdle
	}

	return rc, nil
}

我們接著討論,如果需要傳入的引數過多,我們可以使用 set() 函式來給成員變數賦值,以替代冗長的建構函式。

const (
	defaultMaxTotal = 10
	defaultMaxIdle  = 10
	defaultMinIdle  = 1
)

// ResourcePoolConfig ...
type ResourcePoolConfig struct {
	name     string
	maxTotal int
	maxIdle  int
	minIdle  int
}

func NewResourcePoolConfigSet(name string) (*ResourcePoolConfig, error) {
	if name == "" {
		return nil, errors.New("name is empty")
	}

	return &ResourcePoolConfig{
		maxTotal: defaultMaxTotal,
		maxIdle:  defaultMaxIdle,
		minIdle:  defaultMinIdle,
		name:     name,
	}, nil
}

// SetMinIdle ...
func (rc *ResourcePoolConfig) SetMinIdle(minIdle int) error {
	if minIdle < 0 {
		return fmt.Errorf("min idle cannot < 0, input: %d", minIdle)
	}
	rc.minIdle = minIdle
	return nil
}

// SetMaxIdle ...
func (rc *ResourcePoolConfig) SetMaxIdle(maxIdle int) error {
	if maxIdle < 0 {
		return fmt.Errorf("max idle cannot < 0, input: %d", maxIdle)
	}
	rc.maxIdle = maxIdle
	return nil
}

// SetMaxTotal ...
func (rc *ResourcePoolConfig) SetMaxTotal(maxTotal int) error {
	if maxTotal <= 0 {
		return fmt.Errorf("max total cannot <= 0, input: %d", maxTotal)
	}
	rc.maxTotal = maxTotal
	return nil
}

到這裡,我們還是沒有用上建造者模式,我們來接著分析上面的栗子

1、上面的 name 欄位是必填的,如果必填欄位很多,那麼我們的函式中又會出現引數很長的情況。當然必填項是不能放在set中設定的,因為如果對應的set沒加,我們不能判斷該引數必填的邏輯。

2、比如依賴關係,比如,如果使用者設定了maxTotal、maxIdle、minIdle其中一個,就必須顯式地設定另外兩個;或者配置項之間有一定的約束條件,比如,maxIdle和minIdle要小於等於maxTotal。所以我們就需要一開始就知道所有的引數,才能進行對應校驗。

3、如果我們希望ResourcePoolConfig類物件是不可變物件,也就是說,物件在建立好之後,就不能再修改內部的屬性值。要實現這個功能,我們就不能在ResourcePoolConfig類中暴露set()方法。

這時候建造者模式就登場了

const (
	defaultMaxTotal = 10
	defaultMaxIdle  = 10
	defaultMinIdle  = 1
)

// ResourcePoolConfig ...
type ResourcePoolConfig struct {
	name     string
	maxTotal int
	maxIdle  int
	minIdle  int
}

// ResourcePoolConfigBuilder ...
type ResourcePoolConfigBuilder struct {
	name     string
	maxTotal int
	maxIdle  int
	minIdle  int
}

// SetName ...
func (rb *ResourcePoolConfigBuilder) SetName(name string) error {
	if name == "" {
		return fmt.Errorf("name can not be empty")
	}
	rb.name = name
	return nil
}

// SetMinIdle ...
func (rb *ResourcePoolConfigBuilder) SetMinIdle(minIdle int) error {
	if minIdle < 0 {
		return fmt.Errorf("max total cannot < 0, input: %d", minIdle)
	}
	rb.minIdle = minIdle
	return nil
}

// SetMaxIdle ...
func (rb *ResourcePoolConfigBuilder) SetMaxIdle(maxIdle int) error {
	if maxIdle < 0 {
		return fmt.Errorf("max total cannot < 0, input: %d", maxIdle)
	}
	rb.maxIdle = maxIdle
	return nil
}

// SetMaxTotal ...
func (rb *ResourcePoolConfigBuilder) SetMaxTotal(maxTotal int) error {
	if maxTotal <= 0 {
		return fmt.Errorf("max total cannot <= 0, input: %d", maxTotal)
	}
	rb.maxTotal = maxTotal
	return nil
}

// Build ...
func (rb *ResourcePoolConfigBuilder) Build() (*ResourcePoolConfig, error) {
	if rb.name == "" {
		return nil, errors.New("name can not be empty")
	}

	// 設定預設值
	if rb.minIdle == 0 {
		rb.minIdle = defaultMinIdle
	}

	if rb.maxIdle == 0 {
		rb.maxIdle = defaultMaxIdle
	}

	if rb.maxTotal == 0 {
		rb.maxTotal = defaultMaxTotal
	}

	if rb.maxTotal < rb.maxIdle {
		return nil, fmt.Errorf("max total(%d) cannot < max idle(%d)", rb.maxTotal, rb.maxIdle)
	}

	if rb.minIdle > rb.maxIdle {
		return nil, fmt.Errorf("max idle(%d) cannot < min idle(%d)", rb.maxIdle, rb.minIdle)
	}

	return &ResourcePoolConfig{
		name:     rb.name,
		maxTotal: rb.maxTotal,
		maxIdle:  rb.maxIdle,
		minIdle:  rb.minIdle,
	}, nil
}

建造者模式,避免了無效狀態的存在,因為是設定構建者的變數,構建的變數符合條件之後,一次性的建立物件,這樣建立的物件就一直處於有效狀態了。

不過 go 中函式傳值可以這樣使用,一般公共庫的時候都會選擇這中方式,方便後期的擴充套件

const (
	defaultMaxTotal = 10
	defaultMaxIdle  = 10
	defaultMinIdle  = 1
)

// ResourcePoolConfig ...
type ResourcePoolConfig struct {
	name     string
	maxTotal int
	maxIdle  int
	minIdle  int
}

type Param func(*ResourcePoolConfig)

func NewResourcePoolConfigOption(name string, param ...Param) (*ResourcePoolConfig, error) {
	if name == "" {
		return nil, errors.New("name is empty")
	}
	ps := &ResourcePoolConfig{
		maxIdle:  defaultMinIdle,
		minIdle:  defaultMinIdle,
		maxTotal: defaultMaxTotal,
		name:     name,
	}

	for _, p := range param {
		p(ps)
	}

	if ps.maxTotal < 0 || ps.maxIdle < 0 || ps.minIdle < 0 {
		return nil, fmt.Errorf("args err, option: %v", ps)
	}

	if ps.maxTotal < ps.maxIdle || ps.minIdle > ps.maxIdle {
		return nil, fmt.Errorf("args err, option: %v", ps)
	}

	return ps, nil
}

func MaxTotal(maxTotal int) Param {
	return func(o *ResourcePoolConfig) {
		o.maxTotal = maxTotal
	}
}

func MaxIdle(maxIdle int) Param {
	return func(o *ResourcePoolConfig) {
		o.maxIdle = maxIdle
	}
}

func MinIdle(minIdle int) Param {
	return func(o *ResourcePoolConfig) {
		o.minIdle = minIdle
	}
}

相比於建造者模式,這種方式更其輕便,但是建造者模式也有有點,對於複雜引數的檢驗支援的更好

適用範圍

1、類的必填屬性放到建構函式中,強制建立物件的時候就設定。然後引數比較多,並且有必填校驗

2、類的屬性之間有一定的依賴關係或者約束條件

3、希望建立不可變物件

總結下就是

1、需要生成的物件具有複雜的內部結構。

2、需要生成的物件內部屬性本身相互依賴。

與工廠模式的區別

工廠模式:工廠模式是用來建立不同但是相關型別的物件(繼承同一父類或者介面的一組子類),由給定的引數來決定建立哪種型別的物件

建造者模式:建造者模式是用來建立一種型別的複雜物件,通過設定不同的可選引數,“定製化”地建立不同的物件。

來個栗子:

顧客走進一家餐館點餐,我們利用工廠模式,根據使用者不同的選擇,來製作不同的食物,比如披薩、漢堡、沙拉。對於披薩來說,使用者又有各種配料可以定製,比如乳酪、蕃茄、起司,我們通過建造者模式根據使用者選擇的不同配料來製作披薩。

優點

1、建造者獨立,易擴充套件。

2、便於控制細節風險。

缺點

1、產品必須有共同點,範圍有限制。

2、如內部變化複雜,會有很多的建造類。

參考

【文中涉及到的程式碼】https://github.com/boilingfrog/design-pattern-learning/tree/master/建造者模式
【大話設計模式】https://book.douban.com/subject/2334288/
【極客時間】https://time.geekbang.org/column/intro/100039001
【建造者模式】https://www.runoob.com/design-pattern/builder-pattern.html
【Go設計模式03-建造者模式】https://lailin.xyz/post/builder.html
【建造者模式】https://boilingfrog.github.io/2021/11/06/使用go實現建造者模式/

相關文章