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