Go 代碼應用設計模式: 原型模式、單例模式

這一節我們繼續瞭解創建型模式的原型模式

我們仍然從實際案例入手來了解原型模式。例如假設有一幅畫,其中包含有天空、草地、很多棵樹,描述這幅畫的結構定義如下:

type Tree struct {
  Height string
  Color  string
}
type Pic struct {
  sky   string
  Grass string
  Trees []Tree
}

此時,如果已經存在一個對象爲一副春天的圖畫,而我再想生成一副秋天的圖畫。秋天和春天的實物是不變的,有天空、草地、相同數目的樹,僅僅只是顏色變化了。在程序的角度該如何處理呢?

我們還是從反例着手,我們先看第一個反例, 而且是錯了的反例,使用 = 對新對象的賦值,然後對對象的成員使用賦值的方式改變,這裏對 slice 的處理還會影響舊的 slice,詳細原因可見 go 語言系列 5 - 你不得不知道 slice 的那些事

func main() {
  trees := []Tree{
    {"1m", "Green"},
    {"3m", "Green"},
    {"5m", "Green"},
  }
  springPic := Pic{
    Sky:   "Blue",
    Grass: "Green",
    Trees: trees,
  }
  var autumnPic Pic = springPic
  autumnPic.Sky = "gray"
  autumnPic.Grass = "Yello"
  for i := range autumnPic.Trees {
    autumnPic.Trees[i].Color = "Yello"
  }
  fmt.Println(springPic)
  fmt.Println(autumnPic)
}

上面代碼的輸出中 springPic 的樹顏色也會更改爲 Yello 了。正確代碼如下:

func main() {
  trees := []Tree{
    {"1m", "Green"},
    {"3m", "Green"},
    {"5m", "Green"},
  }
  springPic := Pic{
    Sky:   "Blue",
    Grass: "Green",
    Trees: trees,
  }
  var autumnPic Pic = springPic
  autumnPic.Sky = "gray"
  autumnPic.Grass = "Yello"
  newTrees := make([]Tree, len(autumnPic.Trees))
  for i, t := range autumnPic.Trees {
    newTrees[i] = Tree{t.Height, "Yello"}
  }
  autumnPic.Trees = newTrees
  fmt.Println(springPic)
  fmt.Println(autumnPic)
}

這樣對 Trees 的賦值就不會影響 原有的對象了。但是我們考慮下,如果又要生成一幅冬天的畫,那還是得把上述代碼再寫一遍嗎?我們可以把這部分複製的過程交給 Pic 的成員變量去處理:

func (p Pic) Clone() Pic {
  newP := Pic{
    Sky:   p.Sky,
    Grass: p.Grass,
    Trees: make([]Tree, len(p.Trees)),
  }
  for i, t := range p.Trees {
    newP.Trees[i] = t
  }
  return newP
}

而生成一副秋天的畫的過程如下:

  autumnPic := springPic.Clone()
  autumnPic.Grass = "Yello"
  autumnPic.Sky = "Gray"
  for i := range autumnPic.Trees {
    autumnPic.Trees[i].Color = "Yello"
  }

我們看下這樣的實現有什麼好處?

我們看代碼中的 Trees 屬性 其實仍然是有優化空間的,我們現在的處理在 Pic 的成員內部還是有關注 Trees 是什麼類型進行處理。這裏我們可以對 Trees 再增加 Clone 方法,使代碼使用原型模式更徹底。

type Trees []Tree
type Pic struct {
  Sky   string
  Grass string
  Trees Trees
}
func (t Trees) Clone() Trees {
  newtrees := make([]Tree, len(t))
  for i := range newtrees {
    newtrees[i] = t[i]
  }
  return newtrees
}
func (p Pic) Clone() Pic {
  newP := Pic{
    Sky:   p.Sky,
    Grass: p.Grass,
    Trees: p.Trees.Clone(),
  }
  return newP
}

這樣 Pic 的 Clone 方法可以優化成如下:

func (p Pic) Clone() Pic {
  newP := Pic{
    Sky:   p.Sky,
    Grass: p.Grass,
    Trees: p.Trees.Clone(),
  }
  return newP
}

接下來我們看下在創建型設計模式中最常見的單例模式,這個模式可以不用多介紹。在使用數據庫連接等保證只有一個實例的場景經常使用到,我們直接看代碼:

type Conn struct {
}
var (
  conn *Conn
  once sync.Once
)
func GetConn() *Conn {
  once.Do(func() {
    conn = &Conn{}
  })
  return conn
}

單例模式中的單例是何時生成的呢?有兩種模式主動生成或者懶加載。上述代碼示例使用的懶加載形式,在使用時纔會去生成對象。這裏大家可能會通過 conn 是否爲 nil 來判斷,但是 Go 的 sync 包的 once 就支持使方法只執行一次的對象實現,可直接使用。

順便再說下主動的方式,大家可能會使用 init 來初始化 conn,這裏我不太建議交給 Go 去決定何時初始化,由於包引用關係在大型系統中非常複雜了,很難去閱讀出對象的初始化順序了,一旦某個對象依賴其他的對象,很可能由於順序問題出錯。我建議如下代碼示例中主動的去初始化 conn:

func initConn(){
  conn = &Conn{}
}
func GetConn() *Conn {
  return conn
}

  由程序的使用者(開發人員決定何時初始化 conn),而不是交給 Go 去決定何時初始化,儘可能的減少出錯的情況。

有關創建型設計模式在 Go 上的應用就講解到這裏了,接下來的章節我們將對剩餘的設計模式進行講解。

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