設計模式之美 - 53 | 組合模式:如何設計實現支持遞歸遍歷的文件系統目錄樹結構?

這系列相關博客,參考 設計模式之美

設計模式之美 - 53 | 組合模式:如何設計實現支持遞歸遍歷的文件系統目錄樹結構?

結構型設計模式就快要講完了,還剩下兩個不那麼常用的:組合模式和享元模式。今天,我們來講一下組合模式(Composite Design Pattern)

組合模式跟我們之前講的面向對象設計中的“組合關係(通過組合來組裝兩個類)”,完全是兩碼事。這裏講的“組合模式”,主要是用來處理樹形結構數據。這裏的“數據”,你可以簡單理解爲一組對象集合,待會我們會詳細講解。

正因爲其應用場景的特殊性,數據必須能表示成樹形結構,這也導致了這種模式在實際的項目開發中並不那麼常用。但是,一旦數據滿足樹形結構,應用這種模式就能發揮很大的作用,能讓代碼變得非常簡潔。

話不多說,讓我們正式開始今天的學習吧!

組合模式的原理與實現

在 GoF 的《設計模式》一書中,組合模式是這樣定義的:

Compose objects into tree structure to represent part-whole
hierarchies.Composite lets client treat individual objects and compositions of
objects uniformly.

翻譯成中文就是:將一組對象組織(Compose)成樹形結構,以表示一種“部分 - 整體”的層次結構。組合讓客戶端(在很多設計模式書籍中,“客戶端”代指代碼的使用者。)可以統一單個對象和組合對象的處理邏輯。

接下來,對於組合模式,我舉個例子來給你解釋一下。

假設我們有這樣一個需求:設計一個類來表示文件系統中的目錄,能方便地實現下面這些功能:

  • 動態地添加、刪除某個目錄下的子目錄或文件;
  • 統計指定目錄下的文件個數;
  • 統計指定目錄下的文件總大小。

我這裏給出了這個類的骨架代碼,如下所示。其中的核心邏輯並未實現,你可以試着自己去補充完整,再來看我的講解。在下面的代碼實現中,我們把文件和目錄統一用 FileSystemNode 類來表示,並且通過 isFile 屬性來區分。

public class FileSystemNode {
	private String path;
	private boolean isFile;
	private List<FileSystemNode> subNodes = new ArrayList<>();
	
	public FileSystemNode(String path, boolean isFile) {
		this.path = path;
		this.isFile = isFile;
	}
	
	public int countNumOfFiles() {
		// TODO:...
	}
	
	public long countSizeOfFiles() {
		// TODO:...
	}
	
	public String getPath() {
		return path;
	}
	
	public void addSubNode(FileSystemNode fileOrDir) {
		subNodes.add(fileOrDir);
	}
	
	public void removeSubNode(FileSystemNode fileOrDir) {
		int size = subNodes.size();
		int i = 0;
		for (; i < size; ++i) {
			if (subNodes.get(i).getPath().equalsIgnoreCase(fileOrDir.getPath())) {
				break;
			}
		}
		if (i < size) {
			subNodes.remove(i);
		}
	}
}

實際上,如果你看過我的《數據結構與算法之美》專欄,想要補全其中的 countNumOfFiles() 和 countSizeOfFiles() 這兩個函數,並不是件難事,實際上這就是樹上的遞歸遍歷算法。對於文件,我們直接返回文件的個數(返回 1)或大小。對於目錄,我們遍歷目錄中每個子目錄或者文件,遞歸計算它們的個數或大小,然後求和,就是這個目錄下的文件個數和文件大小。

我把兩個函數的代碼實現貼在下面了,你可以對照着看一下。

public int countNumOfFiles() {
	if (isFile) {
		return 1;
	}
	int numOfFiles = 0;
	for (FileSystemNode fileOrDir : subNodes) {
		numOfFiles += fileOrDir.countNumOfFiles();
	}
	return numOfFiles;
}

public long countSizeOfFiles() {
	if (isFile) {
		File file = new File(path);
		if (!file.exists()) return 0;
		return file.length();
	}
	long sizeofFiles = 0;
	for (FileSystemNode fileOrDir : subNodes) {
		sizeofFiles += fileOrDir.countSizeOfFiles();
	}
	return sizeofFiles;
}

單純從功能實現角度來說,上面的代碼沒有問題,已經實現了我們想要的功能。但是,如果我們開發的是一個大型系統,從擴展性(文件或目錄可能會對應不同的操作)、業務建模(文件和目錄從業務上是兩個概念)、代碼的可讀性(文件和目錄區分對待更加符合人們對業務的認知)的角度來說,我們最好對文件和目錄進行區分設計,定義爲File 和 Directory 兩個類。

按照這個設計思路,我們對代碼進行重構。重構之後的代碼如下所示:

public abstract class FileSystemNode {
	protected String path;
	
	public FileSystemNode(String path) {
		this.path = path;
	}
	
	public abstract int countNumOfFiles();
	public abstract long countSizeOfFiles();
	
	public String getPath() {
		return path;
	}
}

public class File extends FileSystemNode {
	public File(String path) {
		super(path);
	}
	
	@Override
	public int countNumOfFiles() {
		return 1;
	}
	
	@Override
	public long countSizeOfFiles() {
		java.io.File file = new java.io.File(path);
		if (!file.exists()) return 0;
		return file.length();
	}
}

public class Directory extends FileSystemNode {
	private List<FileSystemNode> subNodes = new ArrayList<>();
	
	public Directory(String path) {
		super(path);
	}
	
	@Override
	public int countNumOfFiles() {
		int numOfFiles = 0;
		for (FileSystemNode fileOrDir : subNodes) {
			numOfFiles += fileOrDir.countNumOfFiles();
		}
		return numOfFiles;
	}
	
	@Override
	public long countSizeOfFiles() {
		long sizeofFiles = 0;
		for (FileSystemNode fileOrDir : subNodes) {
			sizeofFiles += fileOrDir.countSizeOfFiles();
		}
		return sizeofFiles;
	}
	
	public void addSubNode(FileSystemNode fileOrDir) {
		subNodes.add(fileOrDir);
	}
	
	public void removeSubNode(FileSystemNode fileOrDir) {
		int size = subNodes.size();
		int i = 0;
		for (; i < size; ++i) {
			if (subNodes.get(i).getPath().equalsIgnoreCase(fileOrDir.getPath())) {
				break;
			}
		}
		if (i < size) {
			subNodes.remove(i);
		}
	}
}

文件和目錄類都設計好了,我們來看,如何用它們來表示一個文件系統中的目錄樹結構。具體的代碼示例如下所示:

public class Demo {
	public static void main(String[] args) {
		/**
		* /
		* /wz/
		* /wz/a.txt
		* /wz/b.txt
		* /wz/movies/
		* /wz/movies/c.avi
		* /xzg/
		* /xzg/docs/
		* /xzg/docs/d.txt
		*/
		Directory fileSystemTree = new Directory("/");
		Directory node_wz = new Directory("/wz/");
		Directory node_xzg = new Directory("/xzg/");
		fileSystemTree.addSubNode(node_wz);
		fileSystemTree.addSubNode(node_xzg);
		
		File node_wz_a = new File("/wz/a.txt");
		File node_wz_b = new File("/wz/b.txt");
		Directory node_wz_movies = new Directory("/wz/movies/");
		node_wz.addSubNode(node_wz_a);
		node_wz.addSubNode(node_wz_b);
		node_wz.addSubNode(node_wz_movies);
		
		File node_wz_movies_c = new File("/wz/movies/c.avi");
		node_wz_movies.addSubNode(node_wz_movies_c);
		
		Directory node_xzg_docs = new Directory("/xzg/docs/");
		node_xzg.addSubNode(node_xzg_docs);
		
		File node_xzg_docs_d = new File("/xzg/docs/d.txt");
		node_xzg_docs.addSubNode(node_xzg_docs_d);
		
		System.out.println("/ files num:" + fileSystemTree.countNumOfFiles());
		System.out.println("/wz/ files num:" + node_wz.countNumOfFiles());
	}
}

我們對照着這個例子,再重新看一下組合模式的定義:“將一組對象(文件和目錄)組織成樹形結構,以表示一種‘部分 - 整體’的層次結構(目錄與子目錄的嵌套結構)。組合模式讓客戶端可以統一單個對象(文件)和組合對象(目錄)的處理邏輯(遞歸遍歷)。”

實際上,剛纔講的這種組合模式的設計思路,與其說是一種設計模式,倒不如說是對業務場景的一種數據結構和算法的抽象。其中,數據可以表示成樹這種數據結構,業務需求可以通過在樹上的遞歸遍歷算法來實現。

組合模式的應用場景舉例

剛剛我們講了文件系統的例子,對於組合模式,我這裏再舉一個例子。搞懂了這兩個例子,你基本上就算掌握了組合模式。在實際的項目中,遇到類似的可以表示成樹形結構的業務場景,你只要“照葫蘆畫瓢”去設計就可以了。

假設我們在開發一個 OA 系統(辦公自動化系統)。公司的組織結構包含部門和員工兩種數據類型。其中,部門又可以包含子部門和員工。在數據庫中的表結構如下所示:
在這裏插入圖片描述
我們希望在內存中構建整個公司的人員架構圖(部門、子部門、員工的隸屬關係),並且提供接口計算出部門的薪資成本(隸屬於這個部門的所有員工的薪資和)。

部門包含子部門和員工,這是一種嵌套結構,可以表示成樹這種數據結構。計算每個部門的薪資開支這樣一個需求,也可以通過在樹上的遍歷算法來實現。所以,從這個角度來看,這個應用場景可以使用組合模式來設計和實現。

這個例子的代碼結構跟上一個例子的很相似,代碼實現我直接貼在了下面,你可以對比着看一下。其中,HumanResource 是部門類(Department)和員工類(Employee)抽象出來的父類,爲的是能統一薪資的處理邏輯。Demo 中的代碼負責從數據庫中讀取數據並在內存中構建組織架構圖。

public abstract class HumanResource {
	protected long id;
	protected double salary;
	
	public HumanResource(long id) {
		this.id = id;
	}
	
	public long getId() {
		return id;
	}
	
	public abstract double calculateSalary();
}

public class Employee extends HumanResource {
	public Employee(long id, double salary) {
		super(id);
		this.salary = salary;
	}
	
	@Override
	public double calculateSalary() {
		return salary;
	}
}

public class Department extends HumanResource {
	private List<HumanResource> subNodes = new ArrayList<>();
	
	public Department(long id) {
		super(id);
	}
	
	@Override
	public double calculateSalary() {
		double totalSalary = 0;
		for (HumanResource hr : subNodes) {
			totalSalary += hr.calculateSalary();
		}
		this.salary = totalSalary;
		return totalSalary;
	}
	
	public void addSubNode(HumanResource hr) {
		subNodes.add(hr);
	}
}

// 構建組織架構的代碼
public class Demo {
	private static final long ORGANIZATION_ROOT_ID = 1001;
	private DepartmentRepo departmentRepo; // 依賴注入
	private EmployeeRepo employeeRepo; // 依賴注入
	
	public void buildOrganization() {
		Department rootDepartment = new Department(ORGANIZATION_ROOT_ID);
		buildOrganization(rootDepartment);
	}
	
	private void buildOrganization(Department department) {
		List<Long> subDepartmentIds = departmentRepo.getSubDepartmentIds(departm
		for (Long subDepartmentId : subDepartmentIds) {
			Department subDepartment = new Department(subDepartmentId);
			department.addSubNode(subDepartment);
			buildOrganization(subDepartment);
		}
		List<Long> employeeIds = employeeRepo.getDepartmentEmployeeIds(departmen
		for (Long employeeId : employeeIds) {
			double salary = employeeRepo.getEmployeeSalary(employeeId);
			department.addSubNode(new Employee(employeeId, salary));
		}
	}
}

我們再拿組合模式的定義跟這個例子對照一下:“將一組對象(員工和部門)組織成樹形結構,以表示一種‘部分 - 整體’的層次結構(部門與子部門的嵌套結構)。組合模式讓客戶端可以統一單個對象(員工)和組合對象(部門)的處理邏輯(遞歸遍歷)。”

重點回顧

好了,今天的內容到此就講完了。我們一塊來總結回顧一下,你需要重點掌握的內容。

組合模式的設計思路,與其說是一種設計模式,倒不如說是對業務場景的一種數據結構和算法的抽象。其中,數據可以表示成樹這種數據結構,業務需求可以通過在樹上的遞歸遍歷算法來實現。

組合模式,將一組對象組織成樹形結構,將單個對象和組合對象都看做樹中的節點,以統一處理邏輯,並且它利用樹形結構的特點,遞歸地處理每個子樹,依次簡化代碼實現。使用組合模式的前提在於,你的業務場景必須能夠表示成樹形結構。所以,組合模式的應用場景也比較侷限,它並不是一種很常用的設計模式。

課堂討論

在文件系統那個例子中,countNumOfFiles() 和 countSizeOfFiles() 這兩個函數實現的效率並不高,因爲每次調用它們的時候,都要重新遍歷一遍子樹。有沒有什麼辦法可以提高這兩個函數的執行效率呢(注意:文件系統還會涉及頻繁的刪除、添加文件操作,也就是對應 Directory 類中的 addSubNode() 和 removeSubNode() 函數)?

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章