組合模式(Composite)是針對由多個節(jié)點對象(部分)組成的樹形結(jié)構(gòu)的對象(整體)而發(fā)展出的一種結(jié)構(gòu)型設(shè)計模式,它能夠使客戶端在操作整體對象或者其下的每個節(jié)點對象時做出統(tǒng)一的響應(yīng),保證樹形結(jié)構(gòu)對象使用方法的一致性,使客戶端不必關(guān)注對象的整體或部分,最終達到對象復(fù)雜的層次結(jié)構(gòu)與客戶端解耦的目的。
組合模式的核心思想是將對象看作是一個樹形結(jié)構(gòu),其中每個節(jié)點可以是一個單獨的對象(葉子節(jié)點)或者一個包含其他節(jié)點的容器(組合節(jié)點)。葉子節(jié)點和組合節(jié)點都實現(xiàn)了相同的接口,這樣客戶端就可以對它們進行一致的操作,而不需要關(guān)心它們的具體類型。
(資料圖片僅供參考)
組合模式有以下幾個角色:
Component(組件接口):所有復(fù)合節(jié)點與葉節(jié)點的高層抽象,定義出需要對組件操作的接口標準。對應(yīng)本章例程中的抽象節(jié)點類,具體使用接口還是抽象類需根據(jù)具體場景而定。Composite(復(fù)合組件):包含多個子組件對象(可以是復(fù)合組件或葉端組件)的復(fù)合型組件,并實現(xiàn)組件接口中定義的操作方法。對應(yīng)本章例程中作為“根節(jié)點/枝節(jié)點”的文件夾類。Leaf(葉端組件):不包含子組件的終端組件,同樣實現(xiàn)組件接口中定義的操作方法。對應(yīng)本章例程中作為“葉節(jié)點”的文件類。Client(客戶端):按所需的層級關(guān)系部署相關(guān)對象并操作組件接口所定義的接口,即可遍歷樹結(jié)構(gòu)上的所有組件。好處和壞處組合模式的好處有:
可以將對象組合成樹形結(jié)構(gòu),表示整體-部分的層次關(guān)系,符合人們的直覺。可以統(tǒng)一處理單個對象和對象組合,簡化了客戶端的代碼邏輯,提高了系統(tǒng)的可復(fù)用性。可以遵循開閉原則,擴展性高,增加新的節(jié)點類型時不需要修改原有代碼。組合模式的壞處有:
可以使設(shè)計變得過于抽象,不利于理解和維護??梢赃`反單一職責(zé)原則,讓葉子節(jié)點和組合節(jié)點具有相同的接口,導(dǎo)致葉子節(jié)點出現(xiàn)不必要的方法??梢詫?dǎo)致遞歸調(diào)用過深,影響系統(tǒng)的性能。應(yīng)用場景組合模式是一種將對象組合成樹形結(jié)構(gòu)的設(shè)計模式,它可以表示整體-部分的層次關(guān)系,并且提供了一致的接口來操作單個對象和對象組合。應(yīng)用場景有:
當需要表示一個對象整體與部分的層次結(jié)構(gòu)時,可以使用組合模式來實現(xiàn)樹形結(jié)構(gòu)。例如,文件系統(tǒng)中的文件與文件夾、組織機構(gòu)中的部門與員工、商品分類中的類別與商品等。當需要統(tǒng)一處理單個對象和對象組合時,可以使用組合模式來實現(xiàn)多態(tài)性。例如,圖形界面中的簡單控件與容器控件、菜單系統(tǒng)中的菜單項與子菜單、報表系統(tǒng)中的單元格與表格等。當需要將對象的創(chuàng)建和使用分離時,可以使用組合模式來實現(xiàn)依賴注入。例如,Spring框架中的Bean對象與BeanFactory對象、測試框架中的測試用例與測試套件等。Java 代碼示例假設(shè)我們有一個文件系統(tǒng),其中有兩種類型的文件:文本文件和文件夾。文本文件是葉子節(jié)點,文件夾是組合節(jié)點,可以包含其他文件。我們想要使用組合模式來實現(xiàn)文件系統(tǒng)的層次結(jié)構(gòu),并且提供一個打印文件路徑的方法。代碼如下:
定義抽象組件
public interface File { // 獲取文件名稱 String getName(); // 添加子文件 void add(File file); // 刪除子文件 void remove(File file); // 獲取子文件 List getChildren(); // 打印文件路徑 void printPath(int space);}
定義葉子節(jié)點
public class TextFile implements File { private String name; public TextFile(String name) { this.name = name; } @Override public String getName() { return name; } @Override public void add(File file) { throw new UnsupportedOperationException("Text file cannot add child file"); } @Override public void remove(File file) { throw new UnsupportedOperationException("Text file cannot remove child file"); } @Override public List getChildren() { throw new UnsupportedOperationException("Text file has no child file"); } @Override public void printPath(int space) { StringBuilder sp = new StringBuilder(); for (int i = 0; i < space; i++) { sp.append(" "); } System.out.println(sp + name); }}
定義組合節(jié)點
public class Folder implements File { private String name; private List children; public Folder(String name) { this.name = name; children = new ArrayList<>(); } @Override public String getName() { return name; } @Override public void add(File file) { children.add(file); } @Override public void remove(File file) { children.remove(file); } @Override public List getChildren() { return children; } @Override public void printPath(int space) { StringBuilder sp = new StringBuilder(); for (int i = 0; i < space; i++) { sp.append(" "); } System.out.println(sp + name); space += 2; for (File child : children) { child.printPath(space); } }}
客戶端代碼
public class Client { public static void main(String[] args) { // 創(chuàng)建一個根文件夾,并添加兩個文本文件和一個子文件夾 File root = new Folder("root"); root.add(new TextFile("a.txt")); root.add(new TextFile("b.txt")); File subFolder = new Folder("subFolder"); root.add(subFolder); // 在子文件夾中添加兩個文本文件 subFolder.add(new TextFile("c.txt")); subFolder.add(new TextFile("d.txt")); // 打印根文件夾的路徑 root.printPath(0); }}
輸出結(jié)果:
root a.txt b.txt subFolder c.txt d.txt
Go 代碼示例package main// importing fmt packageimport ("fmt")// IComposite interfacetype IComposite interface {perform()}// Leaflet structtype Leaflet struct {name string}// Leaflet class method performfunc (leaf *Leaflet) perform() {fmt.Println("Leaflet " + leaf.name)}// Branch structtype Branch struct {leafs []Leafletname stringbranches []Branch}// Branch class method performfunc (branch *Branch) perform() {fmt.Println("Branch: " + branch.name)for _, leaf := range branch.leafs {leaf.perform()}for _, branch := range branch.branches {branch.perform()}}// Branch class method add leafletfunc (branch *Branch) add(leaf Leaflet) {branch.leafs = append(branch.leafs, leaf)}//Branch class method addBranch branchfunc (branch *Branch) addBranch(newBranch Branch) {branch.branches = append(branch.branches, newBranch)}//Branch class method getLeafletsfunc (branch *Branch) getLeaflets() []Leaflet {return branch.leafs}// main methodfunc main() {var branch = &Branch{name: "branch 1"}var leaf1 = Leaflet{name: "leaf 1"}var leaf2 = Leaflet{name: "leaf 2"}var branch2 = Branch{name: "branch 2"}branch.add(leaf1)branch.add(leaf2)branch.addBranch(branch2)branch.perform()}
輸出結(jié)果:
G:\GoLang\examples>go run composite.goBranch: branch 1Leaflet leaf 1Leaflet leaf 2Branch: branch 2
Spring 代碼示例Spring 框架也可以使用組合模式來實現(xiàn)對象的層次結(jié)構(gòu),它提供了一個注解叫做 @Component
,它可以用來標注一個類是一個組件,即一個可被Spring管理的Bean對象。@Component
注解有一個屬性叫做value,它可以用來指定組件的名稱。我們可以使用 @Component
注解來標注我們的文件類,然后在配置文件或注解中聲明這些組件,Spring 就會自動創(chuàng)建和管理這些組件對象。
假設(shè)我們有一個文件系統(tǒng),其中有兩種類型的文件:文本文件和文件夾。文本文件是葉子節(jié)點,文件夾是組合節(jié)點,可以包含其他文件。我們想要使用組合模式來實現(xiàn)文件系統(tǒng)的層次結(jié)構(gòu),并且提供一個打印文件路徑的方法。我們可以使用 @Component
注解來實現(xiàn),代碼如下:
抽象組件不用改造
public interface File { // 獲取文件名稱 String getName(); // 添加子文件 void add(File file); // 刪除子文件 void remove(File file); // 獲取子文件 List getChildren(); // 打印文件路徑 void printPath();}
葉子節(jié)點添加 @Component("a.txt")
注解
@Component("a.txt")public class TextFile implements File { private String name; public TextFile() { this.name = "a.txt"; } @Override public String getName() { return name; } @Override public void add(File file) { throw new UnsupportedOperationException("Text file cannot add child file"); } @Override public void remove(File file) { throw new UnsupportedOperationException("Text file cannot remove child file"); } @Override public List getChildren() { throw new UnsupportedOperationException("Text file has no child file"); } @Override public void printPath() { System.out.println(name); }}
組合節(jié)點添加 @Component("root")
注解
@Component("root")public class Folder implements File { private String name; private List children; // 通過@Autowired注解自動注入所有類型為File的Bean對象到children集合中 @Autowired public Folder(List children) { this.name = "root"; this.children = children; } @Override public String getName() { return name; } @Override public void add(File file) { children.add(file); } @Override public void remove(File file) { children.remove(file); } @Override public List getChildren() { return children; } @Override public void printPath() { System.out.println(name); for (File child : children) { child.printPath(); } }}
SpringBoot 測試代碼
@Slf4j@SpringBootTestclass SpringBootTest { @Autowired private Folder folder; @Test void test() { folder.printPath(); }}
輸出結(jié)果:
roota.txt
總結(jié)組合模式是一種常用的結(jié)構(gòu)型設(shè)計模式,它可以將對象組合成樹形結(jié)構(gòu),并且提供了一致的接口來操作單個對象和對象組合,是一種值得學(xué)習(xí)和掌握的設(shè)計模式。
關(guān)注公眾號【waynblog】每周分享技術(shù)干貨、開源項目、實戰(zhàn)經(jīng)驗、高效開發(fā)工具等,您的關(guān)注將是我的更新動力!
標簽: