今天要介紹的設計模式是組合模式,組合模式也是結構型設計模式的一種,它主要體現了總體與部分的關係,其典型的應用就是樹形結構。組合是一組對象,其中的對象可能包含一個其餘對象,也可能包含一組其餘對象。html
組合模式定義爲:將對象組合成樹形結構以表示「總體-部分」的層次結構。組合模式是單個對象和組合對象的使用具備一致性。node
在使用組合模式的使用要注意如下兩點:設計模式
組合中既要能包含個體,也要能包含其餘組合。安全
要抽象出對象和組合的公共特性。ide
介紹了一些基本內容,可能會仍是不清楚組合模式究竟是什麼樣的一個模式,仍是老樣子,舉🌰說明。post
在咱們的家用PC電腦上的文件結構就是一個很好的例子,例如在個人電腦上有以下圖所示的文件目錄結構。學習
從root文件夾到具體的文件,一層一層的這種結構就是典型的樹形結構,root是硬盤中的某個文件夾,能夠理解爲根節點,這個文件下下面有兩個文件夾和一個文件,image-folder文件夾這種有分支的能夠理解爲分支節點,文件則理解爲葉子節點。優化
爲了要實現這種結構,這三種節點,咱們通常的思路是,建立三個根節點、分支節點、葉子節點這三個類,可是咱們發現根節點的特性其實和分支節點同樣,能夠理解爲一類,因此咱們只須要建立兩個類就能夠。this
定義分支節點(包含根節點)的接口url
/** * 定義分支節點(根節點) */ 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設計模式概述。