組合模式詳解

2023-06-26 21:01:25

簡介

組合模式(Composite)是針對由多個節點物件(部分)組成的樹形結構的物件(整體)而發展出的一種結構型設計模式,它能夠使使用者端在操作整體物件或者其下的每個節點物件時做出統一的響應,保證樹形結構物件使用方法的一致性,使使用者端不必關注物件的整體或部分,最終達到物件複雜的層次結構與使用者端解耦的目的。

組合模式的核心思想是將物件看作是一個樹形結構,其中每個節點可以是一個單獨的物件(葉子節點)或者一個包含其他節點的容器(組合節點)。葉子節點和組合節點都實現了相同的介面,這樣使用者端就可以對它們進行一致的操作,而不需要關心它們的具體型別。

組合模式有以下幾個角色:

  • Component(元件介面):所有複合節點與葉節點的高層抽象,定義出需要對元件操作的介面標準。對應本章例程中的抽象節點類,具體使用介面還是抽象類需根據具體場景而定。
  • Composite(複合元件):包含多個子元件物件(可以是複合元件或葉端元件)的複合型元件,並實現元件介面中定義的操作方法。對應本章例程中作為「根節點/枝節點」的資料夾類。
  • Leaf(葉端元件):不包含子元件的終端元件,同樣實現元件介面中定義的操作方法。對應本章例程中作為「葉節點」的檔案類
  • Client(使用者端):按所需的層級關係部署相關物件並操作元件介面所定義的介面,即可遍歷樹結構上的所有元件。

好處和壞處

組合模式的好處有:

  • 可以將物件組合成樹形結構,表示整體-部分的層次關係,符合人們的直覺。
  • 可以統一處理單個物件和物件組合,簡化了使用者端的程式碼邏輯,提高了系統的可複用性。
  • 可以遵循開閉原則,擴充套件性高,增加新的節點型別時不需要修改原有程式碼。

組合模式的壞處有:

  • 可以使設計變得過於抽象,不利於理解和維護。
  • 可以違反單一職責原則,讓葉子節點和組合節點具有相同的介面,導致葉子節點出現不必要的方法。
  • 可以導致遞迴呼叫過深,影響系統的效能。

應用場景

組合模式是一種將物件組合成樹形結構的設計模式,它可以表示整體-部分的層次關係,並且提供了一致的介面來操作單個物件和物件組合。應用場景有:

  • 當需要表示一個物件整體與部分的層次結構時,可以使用組合模式來實現樹形結構。例如,檔案系統中的檔案與資料夾、組織機構中的部門與員工、商品分類中的類別與商品等。
  • 當需要統一處理單個物件和物件組合時,可以使用組合模式來實現多型性。例如,圖形介面中的簡單控制元件與容器控制元件、選單系統中的選單項與子選單、報表系統中的單元格與表格等。
  • 當需要將物件的建立和使用分離時,可以使用組合模式來實現依賴注入。例如,Spring框架中的Bean物件與BeanFactory物件、測試框架中的測試用例與測試套件等。

Java 程式碼範例

假設我們有一個檔案系統,其中有兩種型別的檔案:文字檔案和資料夾。文字檔案是葉子節點,資料夾是組合節點,可以包含其他檔案。我們想要使用組合模式來實現檔案系統的層次結構,並且提供一個列印檔案路徑的方法。程式碼如下:

定義抽象元件

public interface File {
    // 獲取檔名稱
    String getName();
    // 新增子檔案
    void add(File file);
    // 刪除子檔案
    void remove(File file);
    // 獲取子檔案
    List<File> getChildren();
    // 列印檔案路徑
    void printPath(int space);
}

定義葉子節點

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<File> 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);
    }
}

定義組合節點

public class Folder implements File {
    private String name;
    private List<File> 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<File> 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) {
        // 建立一個根資料夾,並新增兩個文字檔案和一個子資料夾
        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);
    }
}

輸出結果:

root
  a.txt
  b.txt
  subFolder
    c.txt
    d.txt

Go 程式碼範例

package main

// importing fmt package
import (
	"fmt"
)

// IComposite interface
type IComposite interface {
	perform()
}

// Leaflet struct
type Leaflet struct {
	name string
}

// Leaflet class method perform
func (leaf *Leaflet) perform() {

	fmt.Println("Leaflet " + leaf.name)
}

// Branch struct
type Branch struct {
	leafs    []Leaflet
	name     string
	branches []Branch
}

// Branch class method perform
func (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 leaflet
func (branch *Branch) add(leaf Leaflet) {
	branch.leafs = append(branch.leafs, leaf)

}

//Branch class method addBranch branch
func (branch *Branch) addBranch(newBranch Branch) {

	branch.branches = append(branch.branches, newBranch)
}

//Branch class  method getLeaflets
func (branch *Branch) getLeaflets() []Leaflet {
	return branch.leafs
}

// main method
func 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()

}

輸出結果:

G:\GoLang\examples>go run composite.go
Branch: branch 1
Leaflet leaf 1
Leaflet leaf 2
Branch: branch 2

Spring 程式碼範例

Spring 框架也可以使用組合模式來實現物件的層次結構,它提供了一個註解叫做 @Component,它可以用來標註一個類是一個元件,即一個可被Spring管理的Bean物件。@Component 註解有一個屬性叫做value,它可以用來指定元件的名稱。我們可以使用 @Component 註解來標註我們的檔案類,然後在組態檔或註解中宣告這些元件,Spring 就會自動建立和管理這些元件物件。

假設我們有一個檔案系統,其中有兩種型別的檔案:文字檔案和資料夾。文字檔案是葉子節點,資料夾是組合節點,可以包含其他檔案。我們想要使用組合模式來實現檔案系統的層次結構,並且提供一個列印檔案路徑的方法。我們可以使用 @Component 註解來實現,程式碼如下:

抽象元件不用改造

public interface File {
    // 獲取檔名稱
    String getName();
    // 新增子檔案
    void add(File file);
    // 刪除子檔案
    void remove(File file);
    // 獲取子檔案
    List<File> getChildren();
    // 列印檔案路徑
    void printPath();
}

葉子節點新增 @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<File> getChildren() {
        throw new UnsupportedOperationException("Text file has no child file");
    }

    @Override
    public void printPath() {
        System.out.println(name);
    }
}

組合節點新增 @Component("root") 註解

@Component("root")
public class Folder implements File {
    private String name;
    private List<File> children;

    // 通過@Autowired註解自動注入所有型別為File的Bean物件到children集合中
    @Autowired
    public Folder(List<File> 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<File> getChildren() {
        return children;
    }

    @Override
    public void printPath() {
        System.out.println(name);
        for (File child : children) {
            child.printPath();
        }
    }
}

SpringBoot 測試程式碼

@Slf4j
@SpringBootTest
class SpringBootTest {

    @Autowired
    private Folder folder;

    @Test
    void test() {
        folder.printPath();
    }
}

輸出結果:

root
a.txt

總結

組合模式是一種常用的結構型設計模式,它可以將物件組合成樹形結構,並且提供了一致的介面來操作單個物件和物件組合,是一種值得學習和掌握的設計模式。

關注公眾號【waynblog】每週分享技術乾貨、開源專案、實戰經驗、高效開發工具等,您的關注將是我的更新動力!