設計模式-組合模式(Composite)-Java

設計模式-組合模式-Java


目錄




內容

  樹形結構在軟件中隨處可見,例如操作系統中的目錄結構、應用軟件中的菜單、辦公系統中的公司組織結構等等,如何運用面向對象的方式來處理這種樹形結構是組合模式需要解決的問題,組合模式通過一種巧妙的設計方案使得用戶可以一致性地處理整個樹形結構或者樹形、結構的一部分,也可以一致性地處理樹形結構中的葉子節點(不包含子節點的節點)和容器節點(包含子節點的節點)。下面將學習這種用於處理樹形結構的組合模式。

1、示例案例- 設計殺毒軟件的框架結構

1.1、需求

  Sunny軟件公司欲開發一個殺毒(AntiVirus)軟件,該軟件既可以對某個文件夾(Folder)殺毒,也可以對某個指定的文件(File)進行殺毒。該殺毒軟件還可以根據各類文件的特點,爲不同類型的文件提供不同的殺毒方式,例如圖像文件(ImageFile)和文本文件(TextFile)的殺毒方式就有所差異。現需要提供該殺毒軟件的整體框架設計方案。

  在介紹Sunny公司開發人員提出的初始解決方案之前,我們先來分析一下操作系統中的文件目錄結構,例如在Windows操作系統中,存在如圖1.1-1所示目錄結構:
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-gJgQmm8y-1591711483843)(./images/dirStructure.png)]
圖1.1-1 Windows目錄結構

圖1.1-1可以簡化爲如圖1.1-2所示樹形目錄結構:
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-vJhOMsuT-1591711483847)(./images/dirStructure_simple.png)]
圖1.1-2 樹形結構示意圖

  我們可以看出,在圖11-2中包含文件(灰色節點)和文件夾(白色節點)兩類不同的元素,其中在文件夾中可以包含文件,還可以繼續包含子文件夾,但是在文件中不能再包含子文件或者子文件夾。在此,我們可以稱文件夾爲容器(Container),而不同類型的各種文件是其成員,也稱爲葉子(Leaf),一個文件夾也可以作爲另一個更大的文件夾的成員。如果我們現在要對某一個文件夾進行操作,如查找文件,那麼需要對指定的文件夾進行遍歷,如果存在子文件夾則打開其子文件夾繼續遍歷,如果是文件則判斷之後返回查找結果。

1.2、初始化實現

  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();
		樹形結構的處理——組合模式(一)
		165
		}
		//如果是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();
		}
	}
	
	編譯並運行程序,輸出結果如下:
	****對文件夾'Sunny的資料'進行殺毒
	****對文件夾'圖像文件'進行殺毒
	----對圖像文件'小龍女.jpg'進行殺毒
	----對圖像文件'張無忌.gif'進行殺毒
	****對文件夾'文本文件'進行殺毒
	----對文本文件'九陰真經.txt'進行殺毒
	----對文本文件'葵花寶典.doc'進行殺毒

1.3、問題發現

Sunny公司開發人員“成功”實現了殺毒軟件的框架設計,但通過仔細分析,發現該設計方案存在如下問題:

  • (1) 文件夾類Folder的設計和實現都非常複雜,需要定義多個集合存儲不同類型的成員,而且需要針對不同的成員提供增加、刪除和獲取等管理和訪問成員的方法,存在大量的冗餘代碼,系統維護較爲困難;
  • (2) 由於系統沒有提供抽象層,客戶端代碼必須有區別地對待充當容器的文件夾Folder和充當葉子的ImageFile和TextFile,無法統一對它們進行處理;
  • (3) 系統的靈活性和可擴展性差,如果需要增加新的類型的葉子和容器都需要對原有代碼進行修改,例如如果需要在系統中增加一種新類型的視頻文件VideoFile,則必須修改Folder類的源代碼,否則無法在文件夾中添加視頻文件。

  面對以上問題,Sunny軟件公司的開發人員該如何來解決?這就需要用到本章將要介紹的組合模式,組合模式爲處理樹形結構提供了一種較爲完美的解決方案,它描述瞭如何將容器和葉子進行遞歸組合,使得用戶在使用時無須對它們進行區分,可以一致地對待容器和葉子。

2、組合模式概述

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

2.1、組合模式定義

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

2.2、組合模式結構圖中角色

  在組合模式中引入了抽象構件類Component,它是所有容器類和葉子類的公共父類,客戶端針對Component進行編程。組合模式結構如圖2.2-1所示
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-MsGH99o3-1591711483848)(./images/model_composite.png)]
圖2.2-1 組合模式結構圖

  在組合模式結構圖中包含如下幾個角色:

  • Componenet(抽象構件):它可以是接口或抽象類,爲葉子構件和容器構件對象聲明接口,在該角色中可以包含所有子類共有行爲的聲明和實現。在抽象構件中定義了訪問及管理它的子構件的方法,如增加子構件、刪除子構件、獲取子構件等。
  • 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()方法。

3、完整解決方案-殺毒軟件框架設計

  爲了讓系統具有更好的靈活性和可擴展性,客戶端可以一致地對待文件和文件夾,Sunny公司開發人員使用組合模式來進行殺毒軟件的框架設計,其基本結構如圖3-1所示:
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-uyEYE1w5-1591711483850)(./images/antiVirus.png)]
圖3-1 殺毒軟件框架設計結構圖

在圖3-1中, AbstractFile充當抽象構件類,Folder充當容器構件類,ImageFile、TextFile和VideoFile充當葉子構件類。完整代碼如下所示:

  • AbstractFile類代碼3-1:抽象文件類

      package composite;
    
      //抽象文件類:抽象構件
      public abstract class AbstractFile {
    
      	private String name;
    
      	public AbstractFile() {}
    
      	public AbstractFile(String name) {
      		super();
      		this.name = name;
      	}
    
    
      	public String getName() {
      		return name;
      	}
    
      	public void setName(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 abstract void killVirus();
      }
    
  • ImageFile類代碼3-2:葉子構件

      package composite;
    
      //圖像文件類:葉子構件
      public class ImageFile extends AbstractFile{
    
      	public ImageFile(String name) {
      		super(name);
      	}
      	@Override
      	public void killVirus() {
      		//模擬殺毒
      		System.out.println("----對圖像文件'" + getName() + "'進行殺毒");
      	}
      }
    
  • TextFile類代碼3-3:葉子構件

      package composite;
    
      //文本文件類:葉子構件
      public class TextFile extends AbstractFile{
    
    
      	public TextFile(String name) {
      		super(name);
      		// TODO Auto-generated constructor stub
      	}
    
      	@Override
      	public void killVirus() {
      		//模擬殺毒
      		System.out.println("----對文本文件'" + this.getName() + "'進行殺毒");
      	}
      }
    
  • VideoFile類代碼3-4:葉子構件

      package composite;
    
      public class VideoFile extends AbstractFile {
    
    
      	public VideoFile() {}
    
      	public VideoFile(String name) {
      		super(name);
      	}
    
      	@Override
      	public void killVirus() {
      		//模擬殺毒
      		System.out.println("----對視頻文件'" + this.getName() + "'進行殺毒");
      	}
      }
    
  • Folder類代碼3-5:容器構件

      package composite;
    
      import java.util.ArrayList;
    
      public class Folder extends AbstractFile{
    
      	private ArrayList<AbstractFile> fileList = new ArrayList<>();
    
      	public Folder(String name) {
      		super(name);
      	}
    
      	@Override
      	public void add(AbstractFile file) {
      		this.fileList.add(file);
      	}
    
      	@Override
      	public void remove(AbstractFile file) {
      		this.fileList.remove(file);
      	}
    
      	@Override
      	public AbstractFile getChild(int i) {
      		return this.fileList.get(i);
      	}
    
      	@Override
      	public void killVirus() {
      		System.out.println("****對文件夾'" + this.getName() + "'進行殺毒"); //模擬殺毒
      		//遞歸調用成員構件的killVirus()方法
      		for(AbstractFile obj : fileList) {
      			obj.killVirus();
      		}
      	}
      }
    
  • Client類代碼3-6:測試類

      package composite;
    
      public 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'進行殺毒
    

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

  通過引入組合模式,Sunny公司設計的殺毒軟件具有良好的可擴展性,在增加新的文件類型時,無須修改現有類庫代碼,只需增加一個新的文件類作爲AbstractFile類的子類即可,但是由於在AbstractFile中聲明瞭大量用於管理和訪問成員構件的方法,例如add()、remove()等方法,我們不得不在新增的文件類中實現這些方法,提供對應的錯誤提示和異常處理。爲了簡化代碼,我們有以下兩個解決方案:

  • 解決方案一:將葉子構件add()、remove()等方法的實現代碼移至AbstractFile類zhong,由AbstractFile提供統一的默認實現,代碼4-1同上

  • 解決方案二:除此之外,還有一種解決方法是在抽象構件AbstractFile中不聲明任何用於訪問和管理成員構件的方法,代碼4-2如下:

      abstract class AbstractFile {
      	public abstract void killVirus();
      }
    

  此時,由於在AbstractFile中沒有聲明add()、remove()等訪問和管理成員的方法,其葉子構件子類無須提供實現;而且無論客戶端如何定義葉子構件對象都無法調用到這些方法,不需要做任何錯誤和異常處理,容器構件再根據需要增加訪問和管理成員的方法,但這時候也存在一個問題:客戶端不得不使用容器類本身來聲明容器構件對象,否則無法訪問其中新增的add()、remove()等方法,如果客戶端一致性地對待葉子和容器,將會導致容器構件的新增對客戶端不可見,客戶端代碼對於容器構件無法再使用抽象構件來定義,客戶端代碼片段4-3如下所示:

class Client {
	public static void main(String args[]) {
	AbstractFile file1,file2,file3,file4,file5;
	Folder folder1,folder2,folder3,folder4; //不能透明處理容器構件
	//其他代碼省略
	}
}

  在使用組合模式時,根據抽象構件類的定義形式,我們可將組合模式分爲透明組合模式和安全組合模式兩種形式:

4.1、透明組合模式

透明組合模式中,抽象構件Component中聲明瞭所有用於管理成員對象的方法,包括add()、remove()以及getChild()等方法,這樣做的好處是確保所有的構件類都有相同的接口。在客戶端看來,葉子對象與容器對象所提供的方法是一致的,客戶端可以相同地對待所有的對象。透明組合模式也是組合模式的標準形式,雖然上面的解決方案一在客戶端可以有不透明的實現方法,但是由於在抽象構件中包含add()、remove()等方法,因此它還是透明組合模式,透明組合模式的完整結構如圖4-1所示:
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-nY3yTZtZ-1591711483851)(./images/composite_touming.png)]
圖4-1 透明組合模式

  透明組合模式的缺點是不夠安全,因爲葉子對象和容器對象在本質上是有區別的。葉子對象不可能有下一個層次的對象,即不可能包含成員對象,因此爲其提供add()、remove()以及getChild()等方法是沒有意義的,這在編譯階段不會出錯,但在運行階段如果調用這些方法可能會出錯(如果沒有提供相應的錯誤處理代碼)。

4.2、安全組合模式

  安全組合模式中,在抽象構件Component中沒有聲明任何用於管理成員對象的方法,而是在Composite類中聲明並實現這些方法。這種做法是安全的,因爲根本不向葉子對象提供這些管理成員對象的方法,對於葉子對象,客戶端不可能調用到這些方法,這就是解決方案二所採用的實現方式。安全組合模式的結構如圖4.2-1所示:
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-qCF21hgR-1591711483853)(./images/composite_safe.png)]
圖4.2-1 安全組合模式結構圖

  安全組合模式的缺點是不夠透明,因爲葉子構件和容器構件具有不同的方法,且容器構件中那些用於管理成員對象的方法沒有在抽象構件類中定義,因此客戶端不能完全針對抽象編程,必須有區別地對待葉子構件和容器構件。在實際應用中,安全組合模式的使用頻率也非常高,在Java AWT中使用的組合模式就是安全組合模式。

5、總結

  組合模式使用面向對象的思想來實現樹形結構的構建與處理,描述瞭如何將容器對象和葉子對象進行遞歸組合,實現簡單,靈活性好。由於在軟件開發中存在大量的樹形結構,因此組合模式是一種使用頻率較高的結構型設計模式,Java SE中的AWT和Swing包的設計就基於組合模式,在這些界面包中爲用戶提供了大量的容器構件(如Container)和成員構件(如Checkbox、Button和TextComponent等)除此以外,在XML解析、組織結構樹處理、文件系統設計等領域,組合模式都得到了廣泛應用。

5.1、優缺點

  • 主要優點

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

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

5.2、適用場景

  在以下情況下可以考慮使用組合模式:

  • (1) 在具有整體和部分的層次結構中,希望通過一種方式忽略整體與部分的差異,客戶端可以一致地對待它們。
  • (2) 在一個使用面嚮對象語言開發的系統中需要處理一個樹形結構。
  • (3) 在一個系統中能夠分離出葉子對象和容器對象,而且它們的類型不固定,需要增加一些新的類型。

後記

  參考文獻:Java設計模式(劉偉).pdf。持續更新,歡迎交流,本人QQ:806797785

前端項目源代碼地址:https://gitee.com/gaogzhen/vue-leyou
後端JAVA源代碼地址:https://gitee.com/gaogzhen/JAVA
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章