組合模式用於表示部分-整體的層次結構。適用於希望使用者忽略組合物件與單個物件的不同,使用者將統一地使用組合結構中的所有物件的情況。
顧名思義,什麼叫部分-整體,比如常見的前端UI,一個DIV標籤中可以存在多個A標籤、P標籤、DIV標籤等等。
相較於DIV這個容器整體而言,其中所含的A標籤、P標籤甚至是DIV標籤都是單個的部分。
而顯示的時候卻是一視同仁,不分部分還是整體。
這就是典型的組合模式。
再比如WinForms應用程式中,Label、TextBox等這樣簡單的控制元件,可以理解為節點物件,它們中無法再插入其他控制元件,它們就是最小的。
而比如GroupBox、DataGrid這樣由多個簡單控制元件組成的複合控制元件或者容器,就可以理解為容器物件,它們中可以再插入其他的節點物件,甚至是再插入其他容器物件。
但不管是Label這種節點物件還是DataGrid這種容器物件,想要顯示的話都需要執行OnPaint方法。
為了表示這種物件之間整體與部分的層次結構,System.Windows.Forms.Control類就是應用了這種組合模式。
這樣就可以簡單的把組合模式分為三個部分:
當發現需求中是體現部分與整體層次的結構時,以及你希望使用者可以忽略組合物件與單個物件的不同,統一地使用組合結構中的所有物件時,就應該考慮使用組合模式了。
UI的一系列控制元件就是使用了組合模式,整體和部分可以被一致對待。
組合模式有時候又叫做部分-整體模式,它使我們樹型結構的問題中,模糊了簡單元素和複雜元素的概念,客戶程式可以向處理簡單元素一樣來處理複雜元素,從而使得客戶程式與複雜元素的內部結構解耦。
以下情況下適用Composite模式:
1.物件的部分-整體層次結構。
2.忽略組合物件與單個物件的不同,統一地使用組合結構中的所有物件。
組合模式實現的最關鍵的地方是——簡單物件和複合物件必須實現相同的介面。這就是組合模式能夠將組合物件和簡單物件進行一致處理的原因。
組合模式有兩種實現方式,一種是:透明式的組合模式,另外一種是:安全式的組合模式。
透明方式————————————————
Leaf葉類中也有Add 與 Remove方法,這種方式叫透明方式。
也就是說在Component中宣告所有用來管理子物件的方法,其中包括Add、Remove等。
這樣實現Component介面的所有子類都具備了Add與Remove。
這樣做的好處是葉節點和枝節點對於外界沒有區別,它們具有一致的行為介面。
但問題也很明顯,因為Leaf類本身不具備Add、Remove方法的功能,其實現是沒有意義的。
安全方式————————————————
在Component介面中不去宣告Add與Remove方法,那麼子類Leaf也就不用必須實現它們,而在Composite類中宣告所有用來管理子類物件的方法。
以檔案管理器為例,資料夾為Composite,各類檔案為Leaf。
透明方式
1.抽象類
1 /// <summary> 2 /// 抽象元件類(Component) 3 /// </summary> 4 public abstract class DocumentComponent 5 { 6 public string Name { get; set; } 7 protected List<DocumentComponent> mChildren; 8 public List<DocumentComponent> Children 9 { 10 get { return mChildren; } 11 } 12 public DocumentComponent(string name) 13 { 14 this.Name = name; 15 mChildren = new List<DocumentComponent>(); 16 } 17 18 19 public abstract void AddChild(DocumentComponent document); 20 21 public abstract void RemoveChild(DocumentComponent document); 22 23 public abstract void Show(); 24 }
介面或抽象類,為節點元件和容器元件物件宣告介面,在該類中包含共有行為的宣告。
在抽象元件類中,定義了存取及管理它的子元件的方法。
本範例中Show為節點和容器元件共有方法,AddChild和RemoveChild為容器元件方法。
本類主要是為了讓節點類和容器類進行繼承方便統一管理。
2.節點元件類
1 /// <summary> 2 /// 節點元件類(Leaf),各類檔案,每型別可以新增一個對應類。 3 /// </summary> 4 public sealed class Word : DocumentComponent 5 { 6 public Word(string name) 7 : base(name) 8 { } 9 public override void AddChild(DocumentComponent document) 10 { 11 throw new Exception("節點類不支援"); 12 } 13 14 public override void RemoveChild(DocumentComponent document) 15 { 16 throw new Exception("節點類不支援"); 17 } 18 19 public override void Show() 20 { 21 Console.WriteLine("這是一篇word檔案:" + Name); 22 } 23 }
節點物件為最小元件(可以理解為樹葉),並繼承自抽象元件類,實現show方法。
AddChild和RemoveChild為容器元件方法,在節點類中丟擲異常即可。
該類是最小單位,沒有子節點。
本類一個word檔案物件,如果有多個型別的檔案,可以宣告多個類。
3.容器元件類
1 /// <summary> 2 /// 容器元件類(Composite),資料夾 3 /// </summary> 4 public class Folder : DocumentComponent 5 { 6 public Folder(string name) 7 : base(name) 8 { } 9 public override void AddChild(DocumentComponent document) 10 { 11 mChildren.Add(document); 12 Console.WriteLine("檔案或資料夾增加成功"); 13 } 14 public override void RemoveChild(DocumentComponent document) 15 { 16 mChildren.Remove(document); 17 Console.WriteLine("檔案或資料夾刪除成功"); 18 } 19 public override void Show() 20 { 21 Console.WriteLine("這是一個資料夾:" + Name); 22 } 23 }
容器物件可以包含無數節點物件和無數容器元件(可以理解為樹枝,可以有無數樹葉或者分支),容器物件需要實現管理子物件的方法,如AddChild、RemoveChild等。
本類是一個資料夾物件。
4.使用者端
1 /// <summary> 2 /// 使用者端 3 /// </summary> 4 class Client 5 { 6 /// <summary> 7 /// 廣度優先檢索 8 /// </summary> 9 /// <param name="component"></param> 10 private static void BreadthFirstSearch(DocumentComponent component) 11 { 12 Queue<DocumentComponent> q = new Queue<DocumentComponent>(); 13 q.Enqueue(component); 14 Console.WriteLine(component.Name); 15 while (q.Count > 0) 16 { 17 DocumentComponent temp = q.Dequeue(); 18 List<DocumentComponent> children = temp.Children; 19 foreach (DocumentComponent child in children) 20 { 21 Console.WriteLine(child.Name); 22 q.Enqueue(child); 23 } 24 } 25 } 26 27 /// <summary> 28 /// 深度優先檢索 29 /// </summary> 30 /// <param name="component"></param> 31 private static void DepthFirstSearch(DocumentComponent component) 32 { 33 Console.WriteLine(component.Name); 34 List<DocumentComponent> children = component.Children; 35 if (children == null || children.Count == 0) return; 36 foreach (DocumentComponent child in children) 37 { 38 DepthFirstSearch(child); 39 } 40 } 41 42 static void Main(string[] args) 43 { 44 Console.WriteLine("建立三個目錄:"); 45 Folder folder = new Folder("根目錄"); 46 Folder folder1 = new Folder("子目錄1"); 47 Folder folder2 = new Folder("子目錄2"); 48 49 Console.WriteLine("\r\n建立兩個檔案:"); 50 Word word1 = new Word("word檔案1"); 51 Word word2 = new Word("word檔案2"); 52 53 Console.WriteLine("\r\n將子目錄1新增到根目錄下:"); 54 folder.AddChild(folder1); 55 Console.WriteLine("\r\n將子目錄2新增到子目錄1下:"); 56 folder1.AddChild(folder2); 57 58 Console.WriteLine("\r\n將word檔案1新增到子目錄2下:"); 59 folder2.AddChild(word1); 60 Console.WriteLine("\r\n將word檔案2新增到根目錄下:"); 61 folder.AddChild(word2); 62 63 Console.WriteLine("\r\n廣度優先列表:"); 64 DepthFirstSearch(folder); 65 Console.WriteLine("\r\n深度優先列表:"); 66 BreadthFirstSearch(folder); 67 68 Console.ReadKey(); 69 } 70 71 72 }
注:BreadthFirstSearch為廣度優先檢索,依次列出所有元素。DepthFirstSearch為深度優先檢索,列舉完一個資料夾後,返回根目錄繼續列舉其他資料夾。
通過上述範例可以看出,資料夾可以建立N個子資料夾,但檔案只能放在資料夾中,無法放在另一個檔案中。
安全方式
1 /// <summary> 2 /// 抽象元件類(Component) 3 /// </summary> 4 public abstract class DocumentComponent 5 { 6 public string Name { get; set; } 7 protected List<DocumentComponent> mChildren; 8 public List<DocumentComponent> Children 9 { 10 get { return mChildren; } 11 } 12 public DocumentComponent(string name) 13 { 14 this.Name = name; 15 mChildren = new List<DocumentComponent>(); 16 } 17 18 public abstract void Show(); 19 } 20 21 /// <summary> 22 /// 節點元件類(Leaf),各類檔案,每型別可以新增一個對應類。 23 /// </summary> 24 public sealed class Word : DocumentComponent 25 { 26 public Word(string name) 27 : base(name) 28 { } 29 30 public override void Show() 31 { 32 Console.WriteLine("這是一篇word檔案:" + Name); 33 } 34 } 35 36 /// <summary> 37 /// 容器元件類(Composite),資料夾 38 /// </summary> 39 public class Folder : DocumentComponent 40 { 41 public Folder(string name) 42 : base(name) 43 { } 44 public void AddChild(DocumentComponent document) 45 { 46 mChildren.Add(document); 47 Console.WriteLine("檔案或資料夾增加成功"); 48 } 49 public void RemoveChild(DocumentComponent document) 50 { 51 mChildren.Remove(document); 52 Console.WriteLine("檔案或資料夾刪除成功"); 53 } 54 public override void Show() 55 { 56 Console.WriteLine("這是一個資料夾:" + Name); 57 } 58 } 59 60 61 /// <summary> 62 /// 使用者端 63 /// </summary> 64 class Client 65 { 66 /// <summary> 67 /// 廣度優先檢索 68 /// </summary> 69 /// <param name="component"></param> 70 private static void BreadthFirstSearch(DocumentComponent component) 71 { 72 Queue<DocumentComponent> q = new Queue<DocumentComponent>(); 73 q.Enqueue(component); 74 Console.WriteLine(component.Name); 75 while (q.Count > 0) 76 { 77 DocumentComponent temp = q.Dequeue(); 78 List<DocumentComponent> children = temp.Children; 79 foreach (DocumentComponent child in children) 80 { 81 Console.WriteLine(child.Name); 82 q.Enqueue(child); 83 } 84 } 85 } 86 87 /// <summary> 88 /// 深度優先檢索 89 /// </summary> 90 /// <param name="component"></param> 91 private static void DepthFirstSearch(DocumentComponent component) 92 { 93 Console.WriteLine(component.Name); 94 List<DocumentComponent> children = component.Children; 95 if (children == null || children.Count == 0) return; 96 foreach (DocumentComponent child in children) 97 { 98 DepthFirstSearch(child); 99 } 100 } 101 102 static void Main(string[] args) 103 { 104 Console.WriteLine("建立三個目錄:"); 105 Folder folder = new Folder("根目錄"); 106 Folder folder1 = new Folder("子目錄1"); 107 Folder folder2 = new Folder("子目錄2"); 108 109 Console.WriteLine("\r\n建立兩個檔案:"); 110 Word word1 = new Word("word檔案1"); 111 Word word2 = new Word("word檔案2"); 112 113 Console.WriteLine("\r\n將子目錄1新增到根目錄下:"); 114 folder.AddChild(folder1); 115 Console.WriteLine("\r\n將子目錄2新增到子目錄1下:"); 116 folder1.AddChild(folder2); 117 118 Console.WriteLine("\r\n將word檔案1新增到子目錄2下:"); 119 folder2.AddChild(word1); 120 Console.WriteLine("\r\n將word檔案2新增到根目錄下:"); 121 folder.AddChild(word2); 122 123 Console.WriteLine("\r\n廣度優先列表:"); 124 DepthFirstSearch(folder); 125 Console.WriteLine("\r\n深度優先列表:"); 126 BreadthFirstSearch(folder); 127 128 Console.ReadKey(); 129 } 130 131 }
從上述範例中可以看出,安全模式其實就是把共有的方法放在抽象類的。
資料夾獨有的方法放在容器類中,這樣做保證了節點類就沒有Add和Remove等無用方法。
組合模式解耦了客戶程式與複雜元素內部結構,從而使客戶程式可以向處理簡單元素一樣來處理複雜元素。