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"
}
我們看下這樣的實現有什麼好處?
-
由於對象的拷貝成員函數中處理了,對於使用者無需關注對象的類型是什麼,需要複製哪些成員。
-
我們示例代碼中對象的屬性都是 public 類型(大寫開頭),在 struct 外部可訪問,如果是 private,在成員函數外部還無法對屬性直接賦值,而成員內部是可以賦值的。
-
如果成員的屬性特別多,複製過程交給成員函數處理,使用者無需關注那些不變的元素,示例中關注顏色屬性就行。
-
代碼複用,每複製一個對象,無需把複製的代碼又寫一遍。
我們看代碼中的 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