樹形結構在軟件中隨處可見,例如操做系統中的目錄結構、應用軟件中的菜單、辦公系統中的公司組織結構等等,如何運用面向對象的方式來處理這種樹形結構是組合模式須要解決的問題,組合模式經過一種巧妙的設計方案使得用戶能夠一致性地處理整個樹形結構或者樹形結構的一部分,也能夠一致性地處理樹形結構中的葉子節點(不包含子節點的節點)和容器節點(包含子節點的節點)。下面將學習這種用於處理樹形結構的組合模式。html
Sunny軟件公司欲開發一個殺毒(AntiVirus)軟件,該軟件既能夠對某個文件夾(Folder)殺毒,也能夠對某個指定的文件(File)進行殺毒。該殺毒軟件還能夠根據各種文件的特色,爲不一樣類型的文件提供不一樣的殺毒方式,例如圖像文件(ImageFile)和文本文件(TextFile)的殺毒方式就有所差別。現須要提供該殺毒軟件的總體框架設計方案。java |
在介紹Sunny公司開發人員提出的初始解決方案以前,咱們先來分析一下操做系統中的文件目錄結構,例如在Windows操做系統中,存在如圖11-1所示目錄結構:算法
圖11-1 Windows目錄結構docker
圖11-1能夠簡化爲如圖11-2所示樹形目錄結構:編程
圖11-2 樹形目錄結構示意圖設計模式
咱們能夠看出,在圖11-2中包含文件(灰色節點)和文件夾(白色節點)兩類不一樣的元素,其中在文件夾中能夠包含文件,還能夠繼續包含子文件夾,可是在文件中不能再包含子文件或者子文件夾。在此,咱們能夠稱文件夾爲容器(Container),而不一樣類型的各類文件是其成員,也稱爲葉子(Leaf),一個文件夾也能夠做爲另外一個更大的文件夾的成員。若是咱們如今要對某一個文件夾進行操做,如查找文件,那麼須要對指定的文件夾進行遍歷,若是存在子文件夾則打開其子文件夾繼續遍歷,若是是文件則判斷以後返回查找結果。安全
Sunny軟件公司的開發人員經過分析,決定使用面向對象的方式來實現對文件和文件夾的操做,定義了以下圖像文件類ImageFile、文本文件類TextFile和文件夾類Folder:框架
//爲了突出核心框架代碼,咱們對殺毒過程的實現進行了大量簡化 import java.util.*; //圖像文件類 class ImageFile { private String name; public ImageFile(String name) { this.name = name; } public void killVirus() { //簡化代碼,模擬殺毒 System.out.println("----對圖像文件'" + name + "'進行殺毒"); } } //文本文件類 class TextFile { private String name; public TextFile(String name) { this.name = name; } public void killVirus() { //簡化代碼,模擬殺毒 System.out.println("----對文本文件'" + name + "'進行殺毒"); } } //文件夾類 class Folder { private String name; //定義集合folderList,用於存儲Folder類型的成員 private ArrayList<Folder> folderList = new ArrayList<Folder>(); //定義集合imageList,用於存儲ImageFile類型的成員 private ArrayList<ImageFile> imageList = new ArrayList<ImageFile>(); //定義集合textList,用於存儲TextFile類型的成員 private ArrayList<TextFile> textList = new ArrayList<TextFile>(); public Folder(String name) { this.name = name; } //增長新的Folder類型的成員 public void addFolder(Folder f) { folderList.add(f); } //增長新的ImageFile類型的成員 public void addImageFile(ImageFile image) { imageList.add(image); } //增長新的TextFile類型的成員 public void addTextFile(TextFile text) { textList.add(text); } //需提供三個不一樣的方法removeFolder()、removeImageFile()和removeTextFile()來刪除成員,代碼省略 //需提供三個不一樣的方法getChildFolder(int i)、getChildImageFile(int i)和getChildTextFile(int i)來獲取成員,代碼省略 public void killVirus() { System.out.println("****對文件夾'" + name + "'進行殺毒"); //模擬殺毒 //若是是Folder類型的成員,遞歸調用Folder的killVirus()方法 for(Object obj : folderList) { ((Folder)obj).killVirus(); } //若是是ImageFile類型的成員,調用ImageFile的killVirus()方法 for(Object obj : imageList) { ((ImageFile)obj).killVirus(); } //若是是TextFile類型的成員,調用TextFile的killVirus()方法 for(Object obj : textList) { ((TextFile)obj).killVirus(); } } }
編寫以下客戶端測試代碼進行測試:ide
class Client { public static void main(String args[]) { Folder folder1,folder2,folder3; folder1 = new Folder("Sunny的資料"); folder2 = new Folder("圖像文件"); folder3 = new Folder("文本文件"); ImageFile image1,image2; image1 = new ImageFile("小龍女.jpg"); image2 = new ImageFile("張無忌.gif"); TextFile text1,text2; text1 = new TextFile("九陰真經.txt"); text2 = new TextFile("葵花寶典.doc"); folder2.addImageFile(image1); folder2.addImageFile(image2); folder3.addTextFile(text1); folder3.addTextFile(text2); folder1.addFolder(folder2); folder1.addFolder(folder3); folder1.killVirus(); } }
編譯並運行程序,輸出結果以下:學習
****對文件夾'Sunny的資料'進行殺毒 ****對文件夾'圖像文件'進行殺毒 ----對圖像文件'小龍女.jpg'進行殺毒 ----對圖像文件'張無忌.gif'進行殺毒 ****對文件夾'文本文件'進行殺毒 ----對文本文件'九陰真經.txt'進行殺毒 ----對文本文件'葵花寶典.doc'進行殺毒 |
Sunny公司開發人員「成功」實現了殺毒軟件的框架設計,但經過仔細分析,發現該設計方案存在以下問題:
(1) 文件夾類Folder的設計和實現都很是複雜,須要定義多個集合存儲不一樣類型的成員,並且須要針對不一樣的成員提供增長、刪除和獲取等管理和訪問成員的方法,存在大量的冗餘代碼,系統維護較爲困難;
(2) 因爲系統沒有提供抽象層,客戶端代碼必須有區別地對待充當容器的文件夾Folder和充當葉子的ImageFile和TextFile,沒法統一對它們進行處理;
(3) 系統的靈活性和可擴展性差,若是須要增長新的類型的葉子和容器都須要對原有代碼進行修改,例如若是須要在系統中增長一種新類型的視頻文件VideoFile,則必須修改Folder類的源代碼,不然沒法在文件夾中添加視頻文件。
面對以上問題,Sunny軟件公司的開發人員該如何來解決?這就須要用到本章將要介紹的組合模式,組合模式爲處理樹形結構提供了一種較爲完美的解決方案,它描述瞭如何將容器和葉子進行遞歸組合,使得用戶在使用時無須對它們進行區分,能夠一致地對待容器和葉子。
對於樹形結構,當容器對象(如文件夾)的某一個方法被調用時,將遍歷整個樹形結構,尋找也包含這個方法的成員對象(能夠是容器對象,也能夠是葉子對象)並調用執行,牽一而動百,其中使用了遞歸調用的機制來對整個結構進行處理。因爲容器對象和葉子對象在功能上的區別,在使用這些對象的代碼中必須有區別地對待容器對象和葉子對象,而實際上大多數狀況下咱們但願一致地處理它們,由於對於這些對象的區別對待將會使得程序很是複雜。組合模式爲解決此類問題而誕生,它可讓葉子對象和容器對象的使用具備一致性。
組合模式定義以下:
組合模式(Composite Pattern):組合多個對象造成樹形結構以表示具備「總體—部分」關係的層次結構。組合模式對單個對象(即葉子對象)和組合對象(即容器對象)的使用具備一致性,組合模式又能夠稱爲「總體—部分」(Part-Whole)模式,它是一種對象結構型模式。 |
在組合模式中引入了抽象構件類Component,它是全部容器類和葉子類的公共父類,客戶端針對Component進行編程。組合模式結構如圖11-3所示:
圖11-3 組合模式結構圖
在組合模式結構圖中包含以下幾個角色:
● Component(抽象構件):它能夠是接口或抽象類,爲葉子構件和容器構件對象聲明接口,在該角色中能夠包含全部子類共有行爲的聲明和實現。在抽象構件中定義了訪問及管理它的子構件的方法,如增長子構件、刪除子構件、獲取子構件等。
● Leaf(葉子構件):它在組合結構中表示葉子節點對象,葉子節點沒有子節點,它實現了在抽象構件中定義的行爲。對於那些訪問及管理子構件的方法,能夠經過異常等方式進行處理。
● Composite(容器構件):它在組合結構中表示容器節點對象,容器節點包含子節點,其子節點能夠是葉子節點,也能夠是容器節點,它提供一個集合用於存儲子節點,實現了在抽象構件中定義的行爲,包括那些訪問及管理子構件的方法,在其業務方法中能夠遞歸調用其子節點的業務方法。
組合模式的關鍵是定義了一個抽象構件類,它既能夠表明葉子,又能夠表明容器,而客戶端針對該抽象構件類進行編程,無須知道它到底表示的是葉子仍是容器,能夠對其進行統一處理。同時容器對象與抽象構件類之間還創建一個聚合關聯關係,在容器對象中既能夠包含葉子,也能夠包含容器,以此實現遞歸組合,造成一個樹形結構。
若是不使用組合模式,客戶端代碼將過多地依賴於容器對象複雜的內部實現結構,容器對象內部實現結構的變化將引發客戶代碼的頻繁變化,帶來了代碼維護複雜、可擴展性差等弊端。組合模式的引入將在必定程度上解決這些問題。
下面經過簡單的示例代碼來分析組合模式的各個角色的用途和實現。對於組合模式中的抽象構件角色,其典型代碼以下所示:
abstract class Component { public abstract void add(Component c); //增長成員 public abstract void remove(Component c); //刪除成員 public abstract Component getChild(int i); //獲取成員 public abstract void operation(); //業務方法 }
通常將抽象構件類設計爲接口或抽象類,將全部子類共有方法的聲明和實現放在抽象構件類中。對於客戶端而言,將針對抽象構件編程,而無須關心其具體子類是容器構件仍是葉子構件。
若是繼承抽象構件的是葉子構件,則其典型代碼以下所示:
class Leaf extends Component { public void add(Component c) { //異常處理或錯誤提示 } public void remove(Component c) { //異常處理或錯誤提示 } public Component getChild(int i) { //異常處理或錯誤提示 return null; } public void operation() { //葉子構件具體業務方法的實現 } }
做爲抽象構件類的子類,在葉子構件中須要實如今抽象構件類中聲明的全部方法,包括業務方法以及管理和訪問子構件的方法,可是葉子構件不能再包含子構件,所以在葉子構件中實現子構件管理和訪問方法時須要提供異常處理或錯誤提示。固然,這無疑會給葉子構件的實現帶來麻煩。
若是繼承抽象構件的是容器構件,則其典型代碼以下所示:
class Composite extends Component { private ArrayList<Component> list = new ArrayList<Component>(); public void add(Component c) { list.add(c); } public void remove(Component c) { list.remove(c); } public Component getChild(int i) { return (Component)list.get(i); } public void operation() { //容器構件具體業務方法的實現 //遞歸調用成員構件的業務方法 for(Object obj:list) { ((Component)obj).operation(); } } }
在容器構件中實現了在抽象構件中聲明的全部方法,既包括業務方法,也包括用於訪問和管理成員子構件的方法,如add()、remove()和getChild()等方法。須要注意的是在實現具體業務方法時,因爲容器構件充當的是容器角色,包含成員構件,所以它將調用其成員構件的業務方法。在組合模式結構中,因爲容器構件中仍然能夠包含容器構件,所以在對容器構件進行處理時須要使用遞歸算法,即在容器構件的operation()方法中遞歸調用其成員構件的operation()方法。
|
import java.util.*; //抽象文件類:抽象構件 abstract class AbstractFile { public abstract void add(AbstractFile file); public abstract void remove(AbstractFile file); public abstract AbstractFile getChild(int i); public abstract void killVirus(); } //圖像文件類:葉子構件 class ImageFile extends AbstractFile { private String name; public ImageFile(String name) { this.name = name; } public void add(AbstractFile file) { System.out.println("對不起,不支持該方法!"); } public void remove(AbstractFile file) { System.out.println("對不起,不支持該方法!"); } public AbstractFile getChild(int i) { System.out.println("對不起,不支持該方法!"); return null; } public void killVirus() { //模擬殺毒 System.out.println("----對圖像文件'" + name + "'進行殺毒"); } } //文本文件類:葉子構件 class TextFile extends AbstractFile { private String name; public TextFile(String name) { this.name = name; } public void add(AbstractFile file) { System.out.println("對不起,不支持該方法!"); } public void remove(AbstractFile file) { System.out.println("對不起,不支持該方法!"); } public AbstractFile getChild(int i) { System.out.println("對不起,不支持該方法!"); return null; } public void killVirus() { //模擬殺毒 System.out.println("----對文本文件'" + name + "'進行殺毒"); } } //視頻文件類:葉子構件 class VideoFile extends AbstractFile { private String name; public VideoFile(String name) { this.name = name; } public void add(AbstractFile file) { System.out.println("對不起,不支持該方法!"); } public void remove(AbstractFile file) { System.out.println("對不起,不支持該方法!"); } public AbstractFile getChild(int i) { System.out.println("對不起,不支持該方法!"); return null; } public void killVirus() { //模擬殺毒 System.out.println("----對視頻文件'" + name + "'進行殺毒"); } } //文件夾類:容器構件 class Folder extends AbstractFile { //定義集合fileList,用於存儲AbstractFile類型的成員 private ArrayList<AbstractFile> fileList=new ArrayList<AbstractFile>(); private String name; public Folder(String name) { this.name = name; } public void add(AbstractFile file) { fileList.add(file); } public void remove(AbstractFile file) { fileList.remove(file); } public AbstractFile getChild(int i) { return (AbstractFile)fileList.get(i); } public void killVirus() { System.out.println("****對文件夾'" + name + "'進行殺毒"); //模擬殺毒 //遞歸調用成員構件的killVirus()方法 for(Object obj : fileList) { ((AbstractFile)obj).killVirus(); } } }
編寫以下客戶端測試代碼:
class Client { public static void main(String args[]) { //針對抽象構件編程 AbstractFile file1,file2,file3,file4,file5,folder1,folder2,folder3,folder4; folder1 = new Folder("Sunny的資料"); folder2 = new Folder("圖像文件"); folder3 = new Folder("文本文件"); folder4 = new Folder("視頻文件"); file1 = new ImageFile("小龍女.jpg"); file2 = new ImageFile("張無忌.gif"); file3 = new TextFile("九陰真經.txt"); file4 = new TextFile("葵花寶典.doc"); file5 = new VideoFile("笑傲江湖.rmvb"); folder2.add(file1); folder2.add(file2); folder3.add(file3); folder3.add(file4); folder4.add(file5); folder1.add(folder2); folder1.add(folder3); folder1.add(folder4); //從「Sunny的資料」節點開始進行殺毒操做 folder1.killVirus(); } }
編譯並運行程序,輸出結果以下:
****對文件夾'Sunny的資料'進行殺毒 ****對文件夾'圖像文件'進行殺毒 ----對圖像文件'小龍女.jpg'進行殺毒 ----對圖像文件'張無忌.gif'進行殺毒 ****對文件夾'文本文件'進行殺毒 ----對文本文件'九陰真經.txt'進行殺毒 ----對文本文件'葵花寶典.doc'進行殺毒 ****對文件夾'視頻文件'進行殺毒 ----對視頻文件'笑傲江湖.rmvb'進行殺毒 |
因爲在本實例中使用了組合模式,在抽象構件類中聲明瞭全部方法,包括用於管理和訪問子構件的方法,如add()方法和remove()方法等,所以在ImageFile等葉子構件類中實現這些方法時必須進行相應的異常處理或錯誤提示。在容器構件類Folder的killVirus()方法中將遞歸調用其成員對象的killVirus()方法,從而實現對整個樹形結構的遍歷。
若是須要更換操做節點,例如只需對文件夾「文本文件」進行殺毒,客戶端代碼只需修改一行便可,將代碼:
folder1.killVirus(); |
改成:
folder3.killVirus(); |
輸出結果以下:
****對文件夾'文本文件'進行殺毒 ----對文本文件'九陰真經.txt'進行殺毒 ----對文本文件'葵花寶典.doc'進行殺毒 |
在具體實現時,咱們能夠建立圖形化界面讓用戶選擇所需操做的根節點,無須修改源代碼,符合「開閉原則」,客戶端無須關心節點的層次結構,能夠對所選節點進行統一處理,提升系統的靈活性。
經過引入組合模式,Sunny公司設計的殺毒軟件具備良好的可擴展性,在增長新的文件類型時,無須修改現有類庫代碼,只需增長一個新的文件類做爲AbstractFile類的子類便可,可是因爲在AbstractFile中聲明瞭大量用於管理和訪問成員構件的方法,例如add()、remove()等方法,咱們不得不在新增的文件類中實現這些方法,提供對應的錯誤提示和異常處理。爲了簡化代碼,咱們有如下兩個解決方案:
解決方案一:將葉子構件的add()、remove()等方法的實現代碼移至AbstractFile類中,由AbstractFile提供統一的默認實現,代碼以下所示:
//提供默認實現的抽象構件類 abstract class AbstractFile { public void add(AbstractFile file) { System.out.println("對不起,不支持該方法!"); } public void remove(AbstractFile file) { System.out.println("對不起,不支持該方法!"); } public AbstractFile getChild(int i) { System.out.println("對不起,不支持該方法!"); return null; } public abstract void killVirus(); }
若是客戶端代碼針對抽象類AbstractFile編程,在調用文件對象的這些方法時將出現錯誤提示。若是不但願出現任何錯誤提示,咱們能夠在客戶端定義文件對象時不使用抽象層,而直接使用具體葉子構件自己,客戶端代碼片斷以下所示:
class Client { public static void main(String args[]) { //不能透明處理葉子構件 ImageFile file1,file2; TextFile file3,file4; VideoFile file5; AbstractFile folder1,folder2,folder3,folder4; //其餘代碼省略 } }
這樣就產生了一種不透明的使用方式,即在客戶端不能所有針對抽象構件類編程,須要使用具體葉子構件類型來定義葉子對象。
解決方案二:除此以外,還有一種解決方法是在抽象構件AbstractFile中不聲明任何用於訪問和管理成員構件的方法,代碼以下所示:
abstract class AbstractFile { public abstract void killVirus(); }
此時,因爲在AbstractFile中沒有聲明add()、remove()等訪問和管理成員的方法,其葉子構件子類無須提供實現;並且不管客戶端如何定義葉子構件對象都沒法調用到這些方法,不須要作任何錯誤和異常處理,容器構件再根據須要增長訪問和管理成員的方法,但這時候也存在一個問題:客戶端不得不使用容器類自己來聲明容器構件對象,不然沒法訪問其中新增的add()、remove()等方法,若是客戶端一致性地對待葉子和容器,將會致使容器構件的新增對客戶端不可見,客戶端代碼對於容器構件沒法再使用抽象構件來定義,客戶端代碼片斷以下所示:
class Client { public static void main(String args[]) { AbstractFile file1,file2,file3,file4,file5; Folder folder1,folder2,folder3,folder4; //不能透明處理容器構件 //其餘代碼省略 } }
在使用組合模式時,根據抽象構件類的定義形式,咱們可將組合模式分爲透明組合模式和安全組合模式兩種形式:
(1) 透明組合模式
透明組合模式中,抽象構件Component中聲明瞭全部用於管理成員對象的方法,包括add()、remove()以及getChild()等方法,這樣作的好處是確保全部的構件類都有相同的接口。在客戶端看來,葉子對象與容器對象所提供的方法是一致的,客戶端能夠相同地對待全部的對象。透明組合模式也是組合模式的標準形式,雖然上面的解決方案一在客戶端能夠有不透明的實現方法,可是因爲在抽象構件中包含add()、remove()等方法,所以它仍是透明組合模式,透明組合模式的完整結構如圖11-6所示:
圖11-6 透明組合模式結構圖
透明組合模式的缺點是不夠安全,由於葉子對象和容器對象在本質上是有區別的。葉子對象不可能有下一個層次的對象,即不可能包含成員對象,所以爲其提供add()、remove()以及getChild()等方法是沒有意義的,這在編譯階段不會出錯,但在運行階段若是調用這些方法可能會出錯(若是沒有提供相應的錯誤處理代碼)。
(2) 安全組合模式
安全組合模式中,在抽象構件Component中沒有聲明任何用於管理成員對象的方法,而是在Composite類中聲明並實現這些方法。這種作法是安全的,由於根本不向葉子對象提供這些管理成員對象的方法,對於葉子對象,客戶端不可能調用到這些方法,這就是解決方案二所採用的實現方式。安全組合模式的結構如圖11-7所示:
圖11-7 安全組合模式結構圖
安全組合模式的缺點是不夠透明,由於葉子構件和容器構件具備不一樣的方法,且容器構件中那些用於管理成員對象的方法沒有在抽象構件類中定義,所以客戶端不能徹底針對抽象編程,必須有區別地對待葉子構件和容器構件。在實際應用中,安全組合模式的使用頻率也很是高,在Java AWT中使用的組合模式就是安全組合模式。
在學習和使用組合模式時,Sunny軟件公司開發人員發現樹形結構其實隨處可見,例如Sunny公司的組織結構就是「一棵標準的樹」,如圖11-8所示:
圖11-8 Sunny公司組織結構圖
在Sunny軟件公司的內部辦公系統Sunny OA系統中,有一個與公司組織結構對應的樹形菜單,行政人員能夠給各級單位下發通知,這些單位能夠是總公司的一個部門,也能夠是一個分公司,還能夠是分公司的一個部門。用戶只須要選擇一個根節點便可實現通知的下發操做,而無須關心具體的實現細節。這不正是組合模式的「特長」嗎?因而Sunny公司開發人員繪製瞭如圖11-9所示結構圖:
圖11-9 Sunny公司組織結構組合模式示意圖
在圖11-9中,「單位」充當了抽象構件角色,「公司」充當了容器構件角色,「研發部」、「財務部」和「人力資源部」充當了葉子構件角色。
|
組合模式使用面向對象的思想來實現樹形結構的構建與處理,描述瞭如何將容器對象和葉子對象進行遞歸組合,實現簡單,靈活性好。因爲在軟件開發中存在大量的樹形結構,所以組合模式是一種使用頻率較高的結構型設計模式,Java SE中的AWT和Swing包的設計就基於組合模式,在這些界面包中爲用戶提供了大量的容器構件(如Container)和成員構件(如Checkbox、Button和TextComponent等),其結構如圖11-10所示:
圖11-10 AWT組合模式結構示意圖
在圖11-10中,Component類是抽象構件,Checkbox、Button和TextComponent是葉子構件,而Container是容器構件,在AWT中包含的葉子構件還有不少,由於篇幅限制沒有在圖中一一列出。在一個容器構件中能夠包含葉子構件,也能夠繼續包含容器構件,這些葉子構件和容器構件一塊兒組成了複雜的GUI界面。
除此之外,在XML解析、組織結構樹處理、文件系統設計等領域,組合模式都獲得了普遍應用。
1. 主要優勢
組合模式的主要優勢以下:
(1) 組合模式能夠清楚地定義分層次的複雜對象,表示對象的所有或部分層次,它讓客戶端忽略了層次的差別,方便對整個層次結構進行控制。
(2) 客戶端能夠一致地使用一個組合結構或其中單個對象,沒必要關心處理的是單個對象仍是整個組合結構,簡化了客戶端代碼。
(3) 在組合模式中增長新的容器構件和葉子構件都很方便,無須對現有類庫進行任何修改,符合「開閉原則」。
(4) 組合模式爲樹形結構的面向對象實現提供了一種靈活的解決方案,經過葉子對象和容器對象的遞歸組合,能夠造成複雜的樹形結構,但對樹形結構的控制卻很是簡單。
2. 主要缺點
組合模式的主要缺點以下:
在增長新構件時很難對容器中的構件類型進行限制。有時候咱們但願一個容器中只能有某些特定類型的對象,例如在某個文件夾中只能包含文本文件,使用組合模式時,不能依賴類型系統來施加這些約束,由於它們都來自於相同的抽象層,在這種狀況下,必須經過在運行時進行類型檢查來實現,這個實現過程較爲複雜。
3. 適用場景
在如下狀況下能夠考慮使用組合模式:
(1) 在具備總體和部分的層次結構中,但願經過一種方式忽略總體與部分的差別,客戶端能夠一致地對待它們。
(2) 在一個使用面嚮對象語言開發的系統中須要處理一個樹形結構。
(3) 在一個系統中可以分離出葉子對象和容器對象,並且它們的類型不固定,須要增長一些新的類型。
|
原文地址:https://www.cnblogs.com/lfxiao/p/6816026.html