在 DAY 1 ~ DAY 12 已經介紹了我認知常見的 concurrency patterns,接下來就要介紹經典的 GoF design pattern,但在這之前我認為可以先介紹一些 design pattern 的基本概念供大家有相同的認知,以便更清楚的講解,所以接下來幾篇會說明這些概念。
本篇要介紹的是 UML Class diagrams!
什麼是 UML Class diagrams?
透過圖形來說明物件建立、耦合、互動的關係
GoF design pattern 主要在講解物件與物件的關係,如果直接把 code 貼出來,「一行一行」的程式碼很難讓剛接觸此系統的人有「整體」的想像,俗話說「一張圖,勝過千言萬語」,Class diagrams 就是希望就由圖來讓人更快速的對此系統有初步的理解,所以應用在講解 GoF design pattern 也是合適的。
但 Class diagrams 的定義沒有一個統一標準,有許多規則的演變,但並不是每種都很常使用,我選擇以下幾種我認為最重要的規則,並且用mermaid-js繪畫(使用教學可以看我這篇讓你心裡的邏輯具現化的念能力工具),實用與讓人看得懂最為重要,如果規則太複雜讓人無法理解就適得其反了。
先說明幾個小細節:
- +號代表是公開屬性或方法
- 號代表是私有屬性或方法
- 線條上如果有數字,代表此物件在這整個關係上的數量
(相關的 code 在Github - go-design-patterns)
Generalization/Inheritance 繼承
A 會先定義好相關的屬性與方法並且實作,而 B 繼承 A 後,B 會擁有 A 相關的屬性與方法,並且 B 也可有有自己的屬性與方法
舉例來說:PS5 是繼承自 PS4 而誕生的,PS5 能擁有玩 PS4 遊戲的功能,但也可以玩 PS5 的遊戲
code 如下,需特別注意的是其實 golang 沒有繼承,只有組合(下方會介紹),但組合只組一項事物,也可以達到繼承的效果:
package main import "fmt" type PS4 struct { PS4Game string } func (p PS4) PlayPS4Game() { fmt.Printf("play %s", p.PS4Game) } type PS5 struct { PS4 PS5Game string } func (p PS5) PlayPS5Game() { fmt.Printf("play %s", p.PS5Game) } func main() { ps5 := PS5{ PS4: PS4{PS4Game: "KOF14"}, } ps5.PlayPS4Game() }
Realization/Implementation 實作
A 會先定義好相關的方法但不實作,而 B 必須以 A 的定義來實作相關方法
舉例來說:表演比賽說明來參賽的人只要會唱歌即可,所以不同的人種都可以參加
code 如下:
package main import "fmt" type Contestant interface { Sing() } type Mike struct{} func (m Mike) Sing() { fmt.Println("mike singing") } type Kevin struct{} func (m Kevin) Sing() { fmt.Println("kevin singing") } type York struct{} func (m York) Sing() { fmt.Println("york singing") } func Show(contestant Contestant) { contestant.Sing() } func main() { Show(Mike{}) Show(Kevin{}) Show(York{}) }
Composition 組合
A 擁有多個部件,並且這些部件與 A 的生命週期是相同的,他們彼此強制聯繫
舉例來說:PS5 擁有搖桿、CPU、顯示晶片這些部件,這些部件是為了 PS5 而生
code 如下:
package main import "fmt" type PS5 struct { CPU Controller GPU } type CPU struct{} type Controller struct{} type GPU struct{} func main() { ps5 := PS5{ CPU{}, Controller{}, GPU{}, } fmt.Println(ps5) }
Aggregation 聚合
A 擁有多個部件,並且這些部件與 A 的生命週期是不相同的,他們彼此不強制聯繫
跟組合類似,但關鍵差異是:
- 組合:是在定義 struct 的時候就要求把部件聯繫在一起
- 聚合:是透過 function 去設置部件,所以不去設置也是可以的,因此彼此聯繫不強制
舉例來說:switch 可以買鼓組也可以買專用手把來玩太鼓達人,但不買也是可以玩的
code 如下:
package main import "fmt" type NintendoSwitch struct { Controller string } func (n *NintendoSwitch) SetController(controller string) { n.Controller = controller } func (n NintendoSwitch) PlayGame() { if n.Controller != "" { fmt.Printf("use %s to play game", n.Controller) } else { fmt.Println("use default controller to play game") } } func main() { nintendoSwitch := NintendoSwitch{} nintendoSwitch.SetController("drum") nintendoSwitch.PlayGame() }
Association 關聯
A 的屬性中擁有 B,並在 A 創建的時候帶入 B
跟聚合相似,但關鍵差異是:
- 聚合是使用
.SetXXX()
這類的 function 將屬性設置 - 關聯是在創建物件的時候就將屬性設置
但 golang 並不能再創建物件時就將屬性設置,因為 golang 沒有 construct,所以我們可透過一個
CreateXXX()
來模擬舉例來說:我擁有 mac,我可以利用 mac 來去處理鐵人賽文章
code 如下:
package main import "fmt" type Me struct { Mac } type Mac struct{} func (m Mac) WriteArticle() { fmt.Println("write article") } func CreateMe(mac Mac) *Me { return &Me{mac} } func (m Me) WriteIronmanArticle() { m.Mac.WriteArticle() } func main() { me := CreateMe(Mac{}) me.WriteIronmanArticle() }
- 聚合是使用
Dependency 相依
A 不擁有屬性 B,但 A 的某方法有透過參數 B 來實現
舉例來說:我雖然不擁有公司白板,但我可以透過它來跟其他人溝通
code 如下:
package main import "fmt" type Me struct{} type Whiteboard struct{} func (w Whiteboard) DrawOnWhiteboard() { fmt.Println("write") } func (m Me) Discuss(whiteboard Whiteboard) { whiteboard.DrawOnWhiteboard() } func main() { me := Me{} me.Discuss(Whiteboard{}) }