這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
我是設計模式的推崇者,相信一個良好的架構能夠給系統的穩定運行和後期維護帶來極大的方便,因為最近有時間重新學習GoF的設計模式,於是產生了用Go實現GoF經典設計模式的想法。
這篇文章遵循GoF書中的脈絡,本篇是這個系列的第一篇:組合模式(Composite),以後如果在正常工作允許的前提下,應該會每周更新一篇。歡迎大家訪問我的部落格,代碼可以在@Zuozuohao下載。
GoF在第二章通過設計一個Lexi的文檔編輯器來介紹設計模式的使用,GoF認為Lexi設計面臨七個問題:
1. 文檔結構
2. 格式化
3. 修飾使用者介面
4. 支援多種視感
5. 支援多種視窗系統
6. 使用者操作
7. 拼字檢查和連字號
GoF認為Lexi的文檔只針對字元、線、多邊形和其他圖形元素進行處理。但是Lexi的使用者通常面臨的是文檔的物理結構行、列、圖形、表和其他子結構,而這些子結構還有他自己的子結構。
Lexi使用者介面應該允許直接操作這些子結構,例如使用者可以直接操作圖表結構,可以引用、移動等等,而不是將表徵圖看作一堆文本和圖形。
所以Lexi內部表示應該支援
1. 保持文檔的物理結構,即將文本和表徵圖安排到行、列、表等。
2. 可視化產生和顯示文檔。
3. 根據顯示位置來映射文檔內部表示的元素。
GoF認為,首先,應該一致的對待文本和圖形,例如允許使用者在圖形嵌入文本,反之亦然。
其次,不應該強調單個元素和元素組的區別,Lexi應該一致的對待簡單元組和組合元素。
最後,如果考慮到後續增加文法分析功能,那麼簡單元素和組合元素的要求會跟第二條產生衝突,因為對簡單元素和組合元素的文法分析是不同的(所以設計模式需要權衡)。
遞迴組合
GoF使用遞迴組合(Recursive Composition)來表示Lexi圖元的層次化結構,首先將字元和圖形自左到右排列成文檔的一行,然後將多行組合成一列,最後將多列組成一頁等等(如所示)。
GoF將每個重要元素表示一個對象,從而描述這種階層。這些對象不僅包括字元、圖形等可見元素,還包括結構化元素,如行和列,對象結構如所示。
圖元
GoF將文檔對象的所有結構定義一個抽象圖元(Glyph)。他的子類即定義了基本的圖形元素(字元和映像等),還包括結構化元素(行和列),類的繼承結構如所示。
下表描述了Glyph的基本介面。
| Responsibity |
Operations |
| Appearance |
Virtual Void Draw(Window*) |
|
Virtual Void Bounds(Rect&) |
| hit detection |
Virtual bool Intersects(Const Point&) |
| Structure |
Virtual Void Insert(Glyph*, int) |
|
Virtual Void Remove(Glyph*) |
|
Virtual Void Remove(Glyph*) |
|
Virtual Glyph* Child(int) |
|
Virtual Glyph* Parent(int) |
圖元有三種責任,1)他們怎麼畫出自己,2)他們佔用多大空間,3)他們的父圖元和子圖元是什麼。
Glyph子類為了在視窗上呈現自己,必須重寫父類Glyph的Draw方法,從而在螢幕視窗上呈現自己。
Bounds方法返回圖元佔用的矩形地區,Glyph子類需要重寫該方法,因為每個對象所佔用的面積不同。
Intersects判斷一個指定點是否與圖元相交,用以確定使用者在Lexi介面點擊位置的圖元或者圖元結構。
Remove方法會移出一個對象的子圖元。
Child方法返回給定的圖元的子圖元。
Parent方法返回對象的父圖元。
以上是GoF關於Lexi文檔編輯器應該遵循的基本設計,總結起來應該是兩個要點:
1.層次化的對象結構,包括基本圖元和組合圖元
2.通用的介面設計
下面我們來嘗試用Golang來實現這個基本設計模式。
Golang圖元類型
Lexi文檔編輯器應該包括以元類型Character、Rectangle、Row和Column等等,為了方便閱讀(主要是真的不想敲那麼多字)我們只選擇Character、Rectangle、Row三種對象進行實現,其他的圖元類型可以自己嘗試一下。
限於篇幅原因(其實我真的不想碼字,嘿嘿)這裡只是選取了部分GoF定義的圖元和介面,請諒解。
Golang圖元類型介面實現*
正如類圖所設計的那樣,三者都包含Draw和Intersects方法,組合圖元Row多出一個插入子圖元的Insert介面。
因此我們設計一個通用的Appearancer介面用來描述通用介面類型,代碼如下:
type Appearancer interface { Draw(elemet Appearancer) Intersect(point int) SetParent(parentID int)}
圖元除了具有名稱屬性之外,還應該具有一個表徵身份的ID,用以區分不同圖元,所以Glyph、Character、Rectangle和Row類型設計如下:
type Glyph struct { Name string Position int ID int //ID must > 0 ParentID int //if ParentID equal 0, the Glyph has no parents}type Character struct { Glyph}type Rectangle struct { Glyph}type Row struct { Glyph Childs []Appearancer}
下面是Appearancer介面的實現部分,通用介面的工作基本可以在Glyph類型中完成:
func (g *Glyph) Draw(elemet Appearancer) { fmt.Println("I am a ", reflect.TypeOf(elemet), ":", g.Name)}func (g *Glyph) Intersect(point int) { if g.Position == point { fmt.Println(g.Name, " is far away from ", point) } else { fmt.Println(g.Name, " intersect with ", point) }}func (g *Glyph) SetParent(parentID int) { g.ParentID = parentID}func (r *Row) Insert(child Appearancer, position int) { index := r.insertInRightPlace(child, position) child.SetParent(r.ID) fmt.Println("Add ", child, "to Childs at position ", index) fmt.Println(r.Name, "'s length is ", len(r.Childs))}func (parent *Row) insertInRightPlace(child Appearancer, position int) int { insertedPosition := 0 childsLength := len(parent.Childs) if position > (childsLength - 1) { parent.Childs = append(parent.Childs, child) insertedPosition = childsLength } else { parent.Childs = append(parent.Childs[position:position], child) insertedPosition = position } return insertedPosition}
然後就可以直接向Row裡面插入圖元了,代碼如下:
func main() { c1 := &Row{Glyph{"c1", 12, 1, 0}, []Appearancer{}} c1.Draw(c1) c1.Intersect(2) c1.Insert(&Character{Glyph{"c1", 12, 2, 0}}, 3) fmt.Println("hello Composite")}
輸出:
I am a *main.Row : c1c1 intersect with 2Add &{{c1 12 2 1}} to Childs at position 0c1 's length is 1hello Composite
(大家可以在這裡試一下: https://play.golang.org/p/9Cc6HwIqcO )
其實這隻是一個很簡陋的Composite,裡面有很多的地方需要完善,例如我們需要一個全域變數取儲存圖元的ID數組,還有正確初始化的規則等等。但是關於Composite的基本骨架這裡應該都具有了,如果條件允許我會在以後去完善這些方面。
非常感謝您讀完這篇冗長的文章,如有錯誤之處請指出,我會儘快修改,謝謝!