這系列相關博客,參考 設計模式之美
設計模式之美 - 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() 函數)?