17 【結構型模式】組合模式
定義
組合模式(Composite Pattern):組合多個物件形成樹形結構以表示具有“整體—部分”關係的層次結構。組合模式對單個物件(即葉子物件)和組合物件(即容器物件)的使用具有一致性,組合模式又可以稱為“整體—部分”(Part-Whole)模式,它是一種物件結構型模式。
結構圖
要素:
● 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();
}
}
}
示例
Sunny軟體公司欲開發一個防毒(AntiVirus)軟體,該軟體既可以對某個資料夾(Folder)防毒,也可以對某個指定的檔案(File)進行防毒。該防毒軟體還可以根據各類檔案的特點,為不同型別的檔案提供不同的防毒方式,例如影象檔案(ImageFile)和文字檔案(TextFile)的防毒方式就有所差異。現需要提供該防毒軟體的整體框架設計方案。
//為了突出核心框架程式碼,我們對防毒過程的實現進行了大量簡化
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();
}
}
}
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();
}
}
如下問題:
(1) 資料夾類Folder的設計和實現都非常複雜,需要定義多個集合儲存不同型別的成員,而且需要針對不同的成員提供增加、刪除和獲取等管理和訪問成員的方法,存在大量的冗餘程式碼,系統維護較為困難;
(2) 由於系統沒有提供抽象層,客戶端程式碼必須有區別地對待充當容器的資料夾Folder和充當葉子的ImageFile和TextFile,無法統一對它們進行處理;
(3) 系統的靈活性和可擴充套件性差,如果需要增加新的型別的葉子和容器都需要對原有程式碼進行修改,例如如果需要在系統中增加一種新型別的視訊檔案VideoFile,則必須修改Folder類的原始碼,否則無法在資料夾中新增視訊檔案。
解決方案:
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();
}
}
兩種模式
透明組合模式
透明組合模式中,抽象構件Component中聲明瞭所有用於管理成員物件的方法,包括add()、remove()以及getChild()等方法,這樣做的好處是確保所有的構件類都有相同的介面。在客戶端看來,葉子物件與容器物件所提供的方法是一致的,客戶端可以相同地對待所有的物件。
缺點:不夠安全,因為葉子物件和容器物件在本質上是有區別的。葉子物件不可能有下一個層次的物件,即不可能包含成員物件,因此為其提供add()、remove()以及getChild()等方法是沒有意義的,這在編譯階段不會出錯,但在執行階段如果呼叫這些方法可能會出錯(如果沒有提供相應的錯誤處理程式碼)安全組合模式【使用頻率較高】
安全組合模式中,在抽象構件Component中沒有宣告任何用於管理成員物件的方法,而是在Composite類中宣告並實現這些方法。
缺點:不夠透明,因為葉子構件和容器構件具有不同的方法,且容器構件中那些用於管理成員物件的方法沒有在抽象構件類中定義,因此客戶端不能完全針對抽象程式設計,必須有區別地對待葉子構件和容器構件。
總結
主要優點:
(1) 組合模式可以清楚地定義分層次的複雜物件,表示物件的全部或部分層次,它讓客戶端忽略了層次的差異,方便對整個層次結構進行控制。
(2) 客戶端可以一致地使用一個組合結構或其中單個物件,不必關心處理的是單個物件還是整個組合結構,簡化了客戶端程式碼。
(3) 在組合模式中增加新的容器構件和葉子構件都很方便,無須對現有類庫進行任何修改,符合“開閉原則”。
(4) 組合模式為樹形結構的面向物件實現提供了一種靈活的解決方案,通過葉子物件和容器物件的遞迴組合,可以形成複雜的樹形結構,但對樹形結構的控制卻非常簡單。
主要缺點:
在增加新構件時很難對容器中的構件型別進行限制。有時候我們希望一個容器中只能有某些特定型別的物件,例如在某個資料夾中只能包含文字檔案,使用組合模式時,不能依賴型別系統來施加這些約束,因為它們都來自於相同的抽象層,在這種情況下,必須通過在執行時進行型別檢查來實現,這個實現過程較為複雜。
適用場景
(1) 在具有整體和部分的層次結構中,希望通過一種方式忽略整體與部分的差異,客戶端可以一致地對待它們。
(2) 在一個使用面嚮物件語言開發的系統中需要處理一個樹形結構。
(3) 在一個系統中能夠分離出葉子物件和容器物件,而且它們的型別不固定,需要增加一些新的型別。