組合模式

概述

對於樹形結構,當容器對象(如文件夾)的某一個方法被調用時,將遍歷整個樹形結構,尋找也包含這個方法的成員對象(可以是容器對象,也可以是葉子對象)並調用執行,牽一而動百,其中使用了遞歸調用的機制來對整個結構進行處理。由於容器對象和葉子對象在功能 上的區別,在使用這些對象的代碼中必須有區別地對待容器對象和葉子對象,而實際上大多 數情況下我們希望一致地處理它們,因爲對於這些對象的區別對待將會使得程序非常複雜。 組合模式爲解決此類問題而誕生,它可以讓葉子對象和容器對象的使用具有一致性。

定義

組合模式(Composite Pattern):組合多個對象形成樹形結構以表示具有 “整體—部分” 關係的層次結構。組合模式對單個對象(即葉子對象)和組合對象(即容器對象)的使用具有一致 性,組合模式又可以稱爲“整體—部分”(Part-Whole)模式,它是一種對象結構型模式。

在組合模式中引入了抽象構件類 Component,它是所有容器類和葉子類的公共父類,客戶端針對Component進行編程。組合模式結構如圖所示:

在這裏插入圖片描述
在組合模式結構圖中包含如下幾個角色:

  • 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();	
		}						
	}					
}

實例:

現在有如下場景:需要對某一文件夾下所有文件進行殺毒操作,如果當前文件爲文件夾,那麼需要對其下所有文件都進行殺毒操作。
根據組合模式設計出如下的基本結構圖:
在這裏插入圖片描述
抽象結構:

public abstract class AbstractFile {
    public abstract void add(AbstractFile file);
    public abstract void remove(AbstractFile file);
    public abstract AbstractFile getChild(int index);
    public abstract void killVirus();
}

文件夾類:容器構件.

public class Folder extends AbstractFile {
    private String name;
    //定義集合,用於存儲 AbstractFile 類型的成員.
    private List<AbstractFile> fileList;

    public Folder(String name) {
        this.name = name;
        this.fileList = new ArrayList<>();
    }

    @Override
    public void add(AbstractFile file) {
        fileList.add(file);
    }

    @Override
    public void remove(AbstractFile file) {
        fileList.remove(file);
    }

    @Override
    public AbstractFile getChild(int index) {
        return fileList.get(index);
    }

    @Override
    public void killVirus() {
        //模擬殺毒
        System.out.println("*** 對文件夾《" + name + "》進行殺毒");
        //遞歸調用成員構件的 killVirus() 方法.
        for (AbstractFile file : fileList) {
            file.killVirus();
        }
    }
}

圖像文件類:葉子結點.

public class ImageFile extends AbstractFile {
    private String name;

    public ImageFile(String name) {
        this.name = name;
    }

    @Override
    public void add(AbstractFile file) {
        System.out.println("對不起,葉子結點不支持該操作.");

    }

    @Override
    public void remove(AbstractFile file) {
        System.out.println("對不起,葉子結點不支持該操作.");
    }

    @Override
    public AbstractFile getChild(int index) {
        System.out.println("對不起,葉子結點不支持該操作.");
        return null;
    }

    @Override
    public void killVirus() {
        //模擬殺毒
        System.out.println("--- 對圖像文件[" + name + "]進行殺毒");
    }
}

文本文件:葉子構件.

public class TextFile extends AbstractFile {
    private String name;

    public TextFile(String name) {
        this.name = name;
    }

    @Override
    public void add(AbstractFile file) {
        System.out.println("對不起,葉子結點不支持該操作.");

    }

    @Override
    public void remove(AbstractFile file) {
        System.out.println("對不起,葉子結點不支持該操作.");
    }

    @Override
    public AbstractFile getChild(int index) {
        System.out.println("對不起,葉子結點不支持該操作.");
        return null;
    }

    @Override
    public void killVirus() {
        //模擬殺毒
        System.out.println("--- 對文本文件[" + name + "]進行殺毒");
    }
}

視頻文件類:葉子構件.

public class VideoFile extends AbstractFile {
    private String name;

    public VideoFile(String name) {
        this.name = name;
    }

    @Override
    public void add(AbstractFile file) {
        System.out.println("對不起,葉子結點不支持該操作.");

    }

    @Override
    public void remove(AbstractFile file) {
        System.out.println("對不起,葉子結點不支持該操作.");
    }

    @Override
    public AbstractFile getChild(int index) {
        System.out.println("對不起,葉子結點不支持該操作.");
        return null;
    }

    @Override
    public void killVirus() {
        //模擬殺毒
        System.out.println("--- 對視頻文件[" + name + "]進行殺毒");
    }
}

測試代碼:

  @Test
  public void testComposite() {
      AbstractFile root, folder1, folder2, folder3, file1, file2, file3, file4, file5;
      root = new Folder("Tom 的資料");
      folder1 = new Folder("圖像文件");
      folder2 = new Folder("文本文件");
      folder3 = new Folder("視頻文件");

      file1 = new ImageFile("令狐沖.jpg");
      file2 = new ImageFile("東方不敗.jpg");

      file3 = new TextFile("獨孤九劍.doc");
      file4 = new TextFile("葵花寶典.doc");

      file5 = new VideoFile("笑傲江湖.mp4");


      folder1.add(file1);
      folder1.add(file2);

      folder2.add(file3);
      folder2.add(file4);

      folder3.add(file5);

      root.add(folder1);
      root.add(folder2);
      root.add(folder3);

      root.killVirus();
  }

結果:

*** 對文件夾《Tom 的資料》進行殺毒
*** 對文件夾《圖像文件》進行殺毒
--- 對圖像文件[令狐沖.jpg]進行殺毒
--- 對圖像文件[東方不敗.jpg]進行殺毒
*** 對文件夾《文本文件》進行殺毒
--- 對文本文件[獨孤九劍.doc]進行殺毒
--- 對文本文件[葵花寶典.doc]進行殺毒
*** 對文件夾《視頻文件》進行殺毒
--- 對視頻文件[笑傲江湖.mp4]進行殺毒

透明組合模式與安全組合模式

1. 透明組合模式

抽象構件Component中聲明瞭所有用於管理成員對象的方法,包括add()、 remove()以及getChild()等方法,這樣做的好處是確保所有的構件類都有相同的接口。在客戶端 看來,葉子對象與容器對象所提供的方法是一致的,客戶端可以相同地對待所有的對象。透 明組合模式也是組合模式的標準形式,雖然上面的解決方案一在客戶端可以有不透明的實現方法,但是由於在抽象構件中包含add()、remove()等方法,因此它還是透明組合模式,透明組合模式的完整結構如圖所示:
在這裏插入圖片描述

2. 安全組合模式

安全組合模式中,在抽象構件Component中沒有聲明任何用於管理成員對象的方法,而是在 Composite類中聲明並實現這些方法。這種做法是安全的,因爲根本不向葉子對象提供這些管理成員對象的方法,對於葉子對象,客戶端不可能調用到這些方法。安全組合模式的結構如圖所示:
在這裏插入圖片描述
安全組合模式的缺點是不夠透明,因爲葉子構件和容器構件具有不同的方法,且容器構件中那些用於管理成員對象的方法沒有在抽象構件類中定義,因此客戶端不能完全針對抽象編 程,必須有區別地對待葉子構件和容器構件。在實際應用中,安全組合模式的使用頻率也非常高,在Java AWT中使用的組合模式就是安全組合模式。

總結

主要優點

  • 組合模式可以清楚地定義分層次的複雜對象,表示對象的全部或部分層次,它讓客戶端忽 略了層次的差異,方便對整個層次結構進行控制。
  • 客戶端可以一致地使用一個組合結構或其中單個對象,不必關心處理的是單個對象還是整 個組合結構,簡化了客戶端代碼。
  • 在組合模式中增加新的容器構件和葉子構件都很方便,無須對現有類庫進行任何修改,符 合“開閉原則”。
  • 組合模式爲樹形結構的面向對象實現提供了一種靈活的解決方案,通過葉子對象和容器對 象的遞歸組合,可以形成複雜的樹形結構,但對樹形結構的控制卻非常簡單

主要缺點

在增加新構件時很難對容器中的構件類型進行限制。有時候我們希望一個容器中只能有某些特定類型的對象,例如在某個文件夾中只能包含文本文件,使用組合模式時,不能依賴類型系統來施加這些約束,因爲它們都來自於相同的抽象層,在這種情況下,必須通過在運行時進行類型檢查來實現,這個實現過程較爲複雜。

適用場景

  1. 在具有整體和部分的層次結構中,希望通過一種方式忽略整體與部分的差異,客戶端可以 一致地對待它們。
  2. 在一個使用面嚮對象語言開發的系統中需要處理一個樹形結構。
  3. 在一個系統中能夠分離出葉子對象和容器對象,而且它們的類型不固定,需要增加一些新
    的類型。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章