Java設計模式學習記錄-組合模式
前言
今天要介紹的設計模式是組合模式,組合模式也是結構型設計模式的一種,它主要體現了整體與部分的關系,其典型的應用就是樹形結構。組合是一組對象,其中的對象可能包含一個其他對象,也可能包含一組其他對象。
組合模式
組合模式定義為:將對象組合成樹形結構以表示“整體-部分”的層次結構。組合模式是單個對象和組合對象的使用具有一致性。
在使用組合模式的使用要注意以下兩點:
組合中既要能包含個體,也要能包含其他組合。
要抽象出對象和組合的公共特性。
舉例說明
介紹了一些基本內容,可能會還是不清楚組合模式到底是什么樣的一個模式,還是老樣子,舉??說明。
在我們的家用PC電腦上的文件結構就是一個很好的例子,例如在我的電腦上有如下圖所示的文件目錄結構。

從root文件夾到具體的文件,一層一層的這種結構就是典型的樹形結構,root是硬盤中的某個文件夾,可以理解為根節點,這個文件下下面有兩個文件夾和一個文件,image-folder文件夾這種有分支的可以理解為分支節點,文件則理解為葉子節點。
為了要實現這種結構,這三種節點,我們一般的思路是,創建三個根節點、分支節點、葉子節點這三個類,但是我們發現根節點的特性其實和分支節點一樣,可以理解為一類,所以我們只需要創建兩個類就可以。
定義分支節點(包含根節點)的接口
/** * 定義分支節點(根節點) */ public interface IBranch { /** * 獲得分支節點信息 * @return */ public String getInfo(); /** * 增加分支節點(文件夾下還可能會有文件夾) * @param branch */ public void addBranch(IBranch branch); /** * 增加葉子節點 * @param leaf */ public void addLeaf(ILeaf leaf); /** * 獲得子集 * @return */ public ArrayList getChildren(); }
具體的實現如下
/** * 分支節點(文件夾) */ public class Folder implements IBranch{ /** * 節點名稱 */ private String name; /** * 子集 */ private ArrayList children = Lists.newArrayList(); /** * 帶參數的構造方法 * @param name */ public Folder(String name){ this.name = name; } /** * 獲得分支節點信息 * * @return */ @Override public String getInfo() { return "名稱:" + name; } /** * 增加分支節點(文件夾下還可能會有文件夾) * * @param branch */ @Override public void addBranch(IBranch branch) { children.add(branch); } /** * 增加葉子節點 * * @param leaf */ @Override public void addLeaf(ILeaf leaf) { children.add(leaf); } /** * 獲得子集 * * @return */ @Override public ArrayList getChildren() { return children; } }
定義葉子節點的接口
/** * 定義葉子節點 */ public interface ILeaf { /** * 獲得葉子節點的信息 * @return */ public String getInfo(); }
因為葉子節點,不會有子集所以只需要一個獲得描述信息的方法即可,具體的實現如下。
/** * 葉子節點(文件) */ public class File implements ILeaf { private String name; /** * * @param name */ public File(String name){ this.name = name; } /** * 獲得葉子節點的信息 * * @return */ @Override public String getInfo() { return "名稱:"+name; } }
節點類已經定義完成了,所以現在可以開始組裝數據了,然后將最終的數據打印出來看看是不是這個結構。
public class ClientTest { public static void main(String[] args) { //定義根節點 IBranch root = new Folder("root"); //定義二級節點的文件夾 IBranch imageFolder = new Folder("image-folder"); IBranch documentFolder = new Folder("document-folder"); //定義二級節點的文件 ILeaf systemFile = new File("system-file.bat"); //定義三級節點的文件夾 IBranch pngFolder = new Folder("png-folder"); IBranch gifFolder = new Folder("gif-folder"); //定義三級節點的文件 ILeaf testHtml = new File("test.html"); ILeaf testJS = new File("test.js"); //定義四級節點的文件,兩個png文件 ILeaf test1png = new File("test1.png"); ILeaf test2png = new File("test2.png"); //定義四級節點的文件3個gif文件 ILeaf my1gif = new File("my1.gif"); ILeaf my2gif = new File("my2.gif"); ILeaf my3gif = new File("my3.gif"); //填充一級文件夾 root.addBranch(imageFolder); root.addBranch(documentFolder); root.addLeaf(systemFile); //填充二級圖片文件夾 imageFolder.addBranch(pngFolder); imageFolder.addBranch(gifFolder); //填充二級文檔文件夾 documentFolder.addLeaf(testHtml); documentFolder.addLeaf(testJS); //填充三級png圖片文件夾 pngFolder.addLeaf(test1png); pngFolder.addLeaf(test2png); //填充三級gif圖片文件夾 gifFolder.addLeaf(my1gif); gifFolder.addLeaf(my2gif); gifFolder.addLeaf(my3gif); System.out.println(root.getInfo()); //打印出來 getChildrenInfo(root.getChildren()); } /** * 遞歸遍歷文件 * @param arrayList */ private static void getChildrenInfo(ArrayList arrayList){ int length = arrayList.size(); for(int m = 0;m<length;m++){ Object item = arrayList.get(m); //如果是葉子節點就直接打印出來名稱 if(item instanceof ILeaf){ System.out.println(((ILeaf) item).getInfo()); }else { //如果是分支節點就先打印分支節點的名稱,再遞歸遍歷子節點 System.out.println(((IBranch)item).getInfo()); getChildrenInfo(((IBranch)item).getChildren()); } } } }
最終的打印結果:
名稱:root 名稱:image-folder 名稱:png-folder 名稱:test1.png 名稱:test2.png 名稱:gif-folder 名稱:my1.gif 名稱:my2.gif 名稱:my3.gif 名稱:document-folder 名稱:test.html 名稱:test.js 名稱:system-file.bat
這個結果確實是我們想要的,但是仔細看看其實還是有可以優化的地方,Folder和File都有包含名字的構造方法,以及getInfo()方法,那么是不是可以抽取出來?那就改變一下吧。
新增節點公共抽象類
/** * 節點公共抽象類 */ public abstract class Node { private String name; /** * 帶參數的構造方法 * @param name */ public Node(String name){ this.name = name; } /** * 獲得節點信息 * @return */ public String getInfo(){ return "名稱:"+name; } }
改造后的File類
/** * 葉子節點(文件) */ public class File extends Node { /** * 調用父類的構造方法 * @param name */ public File(String name) { super(name); } }
改造后的Folder類
/** * 分支節點(文件夾) */ public class Folder extends Node{ /** * 子集 */ private ArrayList children = Lists.newArrayList(); /** * 帶參數的構造方法 * @param name */ public Folder(String name){ super(name); } /** * 新增節點,有可能是文件也有可能是文件夾 * @param node */ public void add(Node node){ this.children.add(node); } /** * 獲得子集 * * @return */ public ArrayList getChildren() { return children; } }
改造后的使用方式
public class ClientTest { public static void main(String[] args) { //定義根節點 Folder root = new Folder("root"); //定義二級節點的文件夾 Folder imageFolder = new Folder("image-folder"); Folder documentFolder = new Folder("document-folder"); //定義二級節點的文件 File systemFile = new File("system-file.bat"); //定義三級節點的文件夾 Folder pngFolder = new Folder("png-folder"); Folder gifFolder = new Folder("gif-folder"); //定義三級節點的文件 File testHtml = new File("test.html"); File testJS = new File("test.js"); //定義四級節點的文件,兩個png文件 File test1png = new File("test1.png"); File test2png = new File("test2.png"); //定義四級節點的文件3個gif文件 File my1gif = new File("my1.gif"); File my2gif = new File("my2.gif"); File my3gif = new File("my3.gif"); //填充一級文件夾 root.add(imageFolder); root.add(documentFolder); root.add(systemFile); //填充二級圖片文件夾 imageFolder.add(pngFolder); imageFolder.add(gifFolder); //填充二級文檔文件夾 documentFolder.add(testHtml); documentFolder.add(testJS); //填充三級png圖片文件夾 pngFolder.add(test1png); pngFolder.add(test2png); //填充三級gif圖片文件夾 gifFolder.add(my1gif); gifFolder.add(my2gif); gifFolder.add(my3gif); System.out.println(root.getInfo()); //打印出來 getChildrenInfo(root.getChildren()); } /** * 遞歸遍歷文件 * @param arrayList */ private static void getChildrenInfo(ArrayList arrayList){ int length = arrayList.size(); for(int m = 0;m<length;m++){ Object item = arrayList.get(m); //如果是葉子節點就直接打印出來名稱 if(item instanceof File){ System.out.println(((File) item).getInfo()); }else { //如果是分支節點就先打印分支節點的名稱,再遞歸遍歷子節點 System.out.println(((Folder)item).getInfo()); getChildrenInfo(((Folder)item).getChildren()); } } } }
這樣實現起來的各個節點的代碼變的更簡潔了,但是組裝數據的的代碼是沒變的。因為放數據要么自己造要么從某個地方查詢出來,這么個步驟是不能簡化的。
分析
現在我們的這個實現過程就是使用的了組合模式,下面我們來分析一下組合模式都有哪些部分組成。先來看一下根據上面這個例子畫出來的類圖。

組合模式主要有這么幾個角色:
抽象構件角色:
(Node類)這是一個抽象角色,它給參加組合的對象規定一個接口或抽象類,給出組合中對象的默認行為。
葉子構件角色:
(File類)代表參加組合的葉子節點對象,沒有子集,并且要定義出參加組合的原始對象行為。
樹枝構件角色:
(Folder類)代表參加組合的含義子對象的對象,并且也要給出參加組合的原始對象行為以及遍歷子集的行為。
組合模式的兩種形式
透明方式
透明方式來實現組合模式是指,按照上面舉得例子來說,File和Folder的方法和和屬性都一樣,就是說File也包含children屬性和getChildren方法兩者在類上沒有什么區別,只不過File的children為null,getChildren()獲得的也永遠是空。這樣葉子節點對象和樹枝節點對象的區別在抽象層次上就消失了,客戶端可以同等對待所有對象。
這種方式的缺點是不夠安全,因為葉子節點和樹枝節點在本質上是有區別的,葉子節點的getChildren()方法和children的存在沒有意義,雖然在編譯時不會出錯,但是如果在運行時之前沒有做過處理是很容易拋出異常的。
安全方式(非透明)
安全方式實現的組合模式,就是上面的例子介紹的那樣,這種實現方式把葉子和樹枝徹底的區分開來處理,并做到互不干擾,樹枝有單獨自己處理子類的方法,保證運行期不會出錯。
一般在如下情況下應當考慮使用組合模式:
- 需要描述對象的部分和整體的等級結構。
- 需要客戶端忽略掉個體構件和組合構件的區別,客戶端平等對待所以構件。
其實在我們日常的業務當中有很多場景其實都是可以使用組合模式的,例如,某公司的人員組織結構,從CEO到小職員,一級一級的人員關系就可以使用組合模式,還有就是在網上商城購物時,選擇地址,從省道區再到縣也是可以使用組合模式的。
想了解更多的設計模式請查看Java設計模式學習記錄-GoF設計模式概述。
作者:紀莫
歡迎任何形式的轉載,但請務必注明出處。
限于本人水平,如果文章和代碼有表述不當之處,還請不吝賜教。
歡迎掃描二維碼關注公眾號:Jimoer
文章會同步到公眾號上面,大家一起成長,共同提升技術能力。
聲援博主:如果您覺得文章對您有幫助,可以點擊文章右下角【推薦】一下。
您的鼓勵是博主的最大動力!


浙公網安備 33010602011771號