當前位置: 妍妍網 > 碼農

【結構型】樹形結構的套用王者,組合模式

2024-09-19碼農



在日常開發中,我們往往忽視了設計模式的重要性。這可能是因為計畫時間緊迫,或者對設計模式理解不深。其實,很多時候我們可能在不經意間已經使用了某些模式。

重要的是要有意識地學習和套用,讓程式碼更加優雅和高效。也許是時候重新審視我們的編程實踐,將設計模式融入其中了。

今天由淺入深,重學【組合模式】,讓我們一起「重學設計模式」。

一、組合模式

1、組合模式是什麽?

組合模式(Composite Pattern)是一種結構型設計模式,它允許你將物件組合成樹形結構來表示「部份-整體」的階層。組合模式使得使用者可以像使用單個物件一樣使用物件組合,簡化了復雜結構的處理。

2、組合模式的主要參與者:

  1. Component(抽象元件):定義了物件的介面,所有組合物件和葉子節點都應實作它。

  2. Leaf(葉子節點):表示沒有子節點的物件,即樹的末端。

  3. Composite(組合物件):表示擁有子節點的物件。它不僅實作了 Component 介面,還能夠儲存並管理其子節點。

二、最佳化案例:檔案系統

檔案系統是組合模式的經典案例。檔案系統中的資料夾可以包含檔或其他資料夾。無論是檔還是資料夾,它們都應該有一些共同的行為,例如顯示名稱或計算大小。

1、不使用組合模式

如果不采用組合模式,程式碼將需要分別處理葉子節點(如檔)和組合物件(如資料夾),這會導致程式碼復雜性增加。沒有統一的介面意味著檔和資料夾需要不同的處理邏輯,導致程式碼的重復和不易擴充套件。

每次添加新的檔型別或子資料夾時,都需要修改已有程式碼,增加檔和資料夾的處理邏輯,程式碼會變得難以維護和擴充套件。

  1. 冗余程式碼 :Folder 類中有兩個集合,一個儲存檔,另一個儲存子資料夾。由於沒有通用介面,必須為檔和資料夾分別編寫邏輯。

  2. 缺乏一致性 :要操作檔和資料夾時,必須分別對待。例如,showDetails() 方法中,檔和資料夾需要分開處理,無法將它們統一成一個物件。

  3. 擴充套件性差 :如果將來想要添加新的型別,比如符號連結、壓縮檔等,必須分別為它們定義類,並在每個相關的邏輯中添加處理它們的程式碼。

public classFile{
private String name;
privateint size;
publicFile(String name, int size){
this.name = name;
this.size = size;
}
publicvoidshowDetails(){
System.out.println("File: " + name + " (Size: " + size + " KB)");
}
}
public classFolder{
private String name;
private List<File> files = new ArrayList<>();
private List<Folder> subFolders = new ArrayList<>();
publicFolder(String name){
this.name = name;
}
publicvoidaddFile(File file){
files.add(file);
}
publicvoidaddFolder(Folder folder){
subFolders.add(folder);
}
publicvoidshowDetails(){
System.out.println("Folder: " + name);
// 顯示資料夾中的檔
for (File file : files) {
file.showDetails();
}
// 遞迴顯示子資料夾中的內容
for (Folder folder : subFolders) {
folder.showDetails();
}
}
}
public classTest{
publicstaticvoidmain(String[] args){
File file1 = new File("Document.txt"50);
File file2 = new File("Photo.jpg"200);
Folder folder = new Folder("MyFolder");
folder.addFile(file1);
folder.addFile(file2);

File file3 = new File("test0.txt"50);
File file4 = new File("test1.jpg"200);
Folder folderRoot = new Folder("MyFolderRoot");
folderRoot.addFile(file3);
folderRoot.addFile(file4);
folderRoot.addFolder(folder);
folderRoot.showDetails();
}
}















2、透過組合模式最佳化上面程式碼

在電子商務網站的產品目錄中,可以透過組合模式管理產品和產品類別。每個產品(葉子節點)具有價格和描述,產品類別(組合物件)可以包含其他類別或產品。透過使用組合模式,可以簡化查詢價格、庫存和類別層次的操作。

最佳化點:

  1. 簡化階層:在系統中,產品與類別都遵循相同的介面,可以統一處理產品和類別。

  2. 靈活性:可以動態地增加或移除產品和類別,提高系統的可延伸性和維護性。

這個模式特別適合套用於具有遞迴或樹形結構的場景。

檔和資料夾可以看作統一的元件,處理邏輯一致,新增型別時只需實作抽象介面,原有程式碼不需要修改。

/**
 * 抽象元件
 */

publicabstract classFileSystemComponent{
publicabstractvoidshowDetails();
}
/**
 * 葉子節點:檔
 */

public classFileextendsFileSystemComponent{
private String name;
privateint size;
publicFile(String name, int size){
this.name = name;
this.size = size;
}
@Override
publicvoidshowDetails(){
System.out.println("File: " + name + " (Size: " + size + " KB)");
}
}
/**
 * 組合物件:資料夾
 */

public classFolderextendsFileSystemComponent{
private String name;
private List<FileSystemComponent> components = new ArrayList<>();
publicFolder(String name){
this.name = name;
}
publicvoidaddComponent(FileSystemComponent component){
components.add(component);
}
@Override
publicvoidshowDetails(){
System.out.println("Folder: " + name);
for (FileSystemComponent component : components) {
component.showDetails();
}
}
}
public classCombinationClient{
publicstaticvoidmain(String[] args){
FileSystemComponent file1 = new File("Document.txt"50);
FileSystemComponent file2 = new File("Photo.jpg"200);
Folder folder = new Folder("MyFolder");
folder.addComponent(file1);
folder.addComponent(file2);
File file3 = new File("test0.txt"50);
File file4 = new File("test1.jpg"200);
Folder folderRoot = new Folder("MyFolderRoot");
folderRoot.addComponent(file3);
folderRoot.addComponent(file4);
folderRoot.addComponent(folder);
folderRoot.showDetails();
}
}














三、使用組合模式有哪些優勢

1、統一介面,簡化客戶端程式碼

組合模式為物件和物件組合提供了統一的介面,客戶端可以一致地操作單個物件和組合物件,而不必區分它們是葉子還是組合。這種一致性減少了處理不同型別物件的復雜度,簡化了程式碼。

2、遞迴結構處理方便

組合模式特別適合處理樹形或遞迴結構,如檔案系統、組織結構等。透過遞迴呼叫,可以輕松遍歷整個結構(無論是檔還是資料夾),不必寫不同的處理邏輯。

3、易擴充套件

新增葉子節點或組合物件時,只需實作相同的介面,不需要修改已有的程式碼。組合模式具有開放-封閉原則的優勢,使系統更加靈活、易於擴充套件。

4、簡化客戶端操作

客戶端不再需要關心物件的具體型別(葉子或組合),只需處理抽象元件。這使得程式碼更加簡潔,也減少了錯誤處理的復雜性。

5、動態組合靈活

透過組合模式,可以動態地組合物件,而無需預先定義復雜的類結構。這為復雜物件提供了靈活的處理方式,使得系統結構更具彈性。

四、適用場景

  1. 樹形結構(階層) :組合模式非常適用於需要表示「部份-整體」關系的場景,尤其是樹形結構。例如,檔案系統、組織結構圖、產品目錄樹、選單系統等。

  • 檔案系統:檔和資料夾之間的層次關系可以透過組合模式來輕松管理。無論是單個檔,還是包含子資料夾的資料夾,都可以透過相同的方式處理。

  • 組織結構圖:公司中員工和部門之間存在階層,員工可以屬於某個部門,部門可以屬於其他部門,透過組合模式,可以方便地管理和展示這種結構。

  • GUI控制項(圖形化使用者介面) :GUI 元件經常包含其他元件,如按鈕、視窗、面板等。組合模式可以用來管理這些圖形控制項,讓它們統一處理。例如,一個視窗可能包含面板,面板中包含按鈕和文字域,這些控制項都可以透過相同的介面進行管理和渲染。

  • 選單系統 :選單項可以包含子選單,也可以是普通的選單項。組合模式可以用於選單系統的設計,使得選單項和子選單都實作相同的介面,從而簡化選單的顯示和操作。

  • 產品目錄和分類管理 :電子商務網站中,產品可以屬於某個類別,類別之間也有階層。組合模式可以用於管理產品和類別,透過統一介面可以輕松查詢、操作或統計不同類別下的產品資訊。

  • 圖形繪制系統 :在圖形繪制系統中,復雜的圖形可能是由多個簡單圖形組成的。組合模式允許將簡單圖形和復雜圖形統一起來處理,方便實作圖形的遞迴繪制和操作。

  • 許可權系統 :在許可權管理中,角色和許可權之間可能存在層次關系,一個角色可以擁有多個許可權,許可權可以包含子許可權。組合模式可以用於簡化許可權系統的設計,使得許可權的分配和管理更加靈活。

  • 編譯器中的抽象語法樹(AST) :編譯器在解析原始碼時,會生成抽象語法樹(AST),其中每個節點可以是語法結構的一個元素(如運算式、語句等)。透過組合模式,編譯器可以對這些節點進行統一處理,而不需要關心它們的具體型別。

  • 五、組合模式的劣勢

    雖然組合模式有很多優勢,但它也有一些潛在的劣勢:

    1. 過度抽象 :為了實作通用介面,可能導致系統設計過於抽象和復雜,尤其是在階層非常深的情況下,增加了理解和維護的難度。

    2. 效能問題 :由於組合模式需要遞迴處理物件結構,在大規模、深層次的樹形結構中,遞迴操作可能帶來效能問題。

    3. 型別安全問題 :組合模式統一了葉子節點和組合物件的介面,有時可能難以強制型別檢查。例如,某些操作只對葉子節點有效,呼叫這些操作時可能需要額外的型別判斷。

    六、在jdk源碼中,哪些地方套用了組合模式,程式碼舉例說明一下

    在JDK源碼中,組合模式被廣泛套用於處理樹形或階層的數據結構和設計,最典型的例子之一是 java.awt 包中的 Component 類,以及集合框架中的 java.util 包。

    以下是兩個常見的套用場景:

    1、AWT(Abstract Window Toolkit)中的元件樹

    在 java.awt 包中,Component 類和 Container 類使用了組合模式。Container 代表可以包含子元件的物件,而 Component 是一個抽象元件,代表所有的 GUI 元素。Container 既可以是單個元件(葉子),也可以是組合元件(容器),從而形成了一個樹形的 GUI 元件階層。

    (1)代分碼析:

    1. Component 類是所有 GUI 元素的基礎類別。

    2. Container 類是 Component 的子類別,它可以包含多個子元件。

    (2)JDK 源碼中的範例(簡化):

    // java.awt.Component
    publicabstract classComponent{
    // 省略大量方法
    publicvoidpaint(Graphics g){
    // 繪制元件的程式碼
    }
    }
    // java.awt.Container
    public classContainerextendsComponent{
    // 儲存子元件
    private List<Component> componentList = new ArrayList<>();
    publicvoidadd(Component comp){
    componentList.add(comp);
    }
    publicvoidremove(Component comp){
    componentList.remove(comp);
    }
    @Override
    publicvoidpaint(Graphics g){
    super.paint(g);
    // 遞迴呼叫子元件的 paint 方法
    for (Component comp : componentList) {
    comp.paint(g);
    }
    }
    }


    (3)組合模式分析:

    1. Component 是一個抽象類,定義了所有元件的通用介面。

    2. Container 是一個組合物件,包含其他元件,並可以遞迴地管理和繪制這些元件。

    3. 在使用時,GUI 系統可以統一處理 Component 物件,不論它是葉子元件(如按鈕、文字域),還是組合元件(如面板、視窗)。

    2、集合框架中的 java.util 包

    在 Java 集合框架中,List、Set、Map 等介面也使用了組合模式。集合類中既有可以直接儲存元素的類(如 ArrayList、HashSet),也有可以組合其他集合的類(如 Collections.unmodifiableList、Collections.synchronizedList 等)。

    (1)代分碼析:

    1. List 介面是所有列表的通用介面。

    2. ArrayList 實作了 List 介面,代表葉子節點,可以直接儲存元素。

    3. Collections.unmodifiableList 透過組合模式,將一個已有的列表封裝起來,實作不可修改的列表。

    (2)JDK 源碼中的範例:

    // java.util.List (介面)
    publicinterfaceList<EextendsCollection<E{
    // 定義列表的通用方法
    booleanadd(E e);
    get(int index);
    // 省略其他方法
    }
    // java.util.ArrayList (葉子節點)
    public classArrayList<EextendsAbstractList<EimplementsList<E{
    private Object[] elementData;
    privateint size;
    publicArrayList(){
    elementData = new Object[10];
    }
    @Override
    publicbooleanadd(E e){
    // 添加元素的邏輯
    elementData[size++] = e;
    returntrue;
    }
    @Override
    public E get(int index){
    return (E) elementData[index];
    }
    // 省略其他方法
    }
    // java.util.Collections.unmodifiableList (組合物件)
    publicstatic <T> List<T> unmodifiableList(List<? extends T> list){
    returnnew UnmodifiableList<>(list);
    }
    // 內部類,包裝一個已有的 List
    privatestatic classUnmodifiableList<EextendsAbstractList<E{
    privatefinal List<? extends E> list;
    UnmodifiableList(List<? extends E> list) {
    this.list = Objects.requireNonNull(list);
    }
    @Override
    public E get(int index){
    return list.get(index);
    }
    @Override
    publicbooleanadd(E e){
    thrownew UnsupportedOperationException();
    }
    }







    (3)組合模式分析:

    1. List 介面是抽象元件,定義了所有列表操作的通用方法。

    2. ArrayList 是具體的葉子節點,可以直接儲存和操作元素。

    3. UnmodifiableList 是一個組合物件,它透過包裝另一個 List 實作不可修改的列表,並且與其他 List 一樣實作了 List 介面。

    4. List 和 ArrayList 之間的關系符合組合模式:ArrayList 作為葉子節點實作了通用的 List 介面,而 UnmodifiableList 透過組合其他列表來擴充套件功能,形成了一個樹形的結構。

    3、小總結

    組合模式在 JDK 中的套用主要體現在處理階層、遞迴結構的場景。AWT 中的 Component 和 Container 類,集合框架中的 List 介面及其實作,都采用了組合模式。透過這種設計,Java 提供了靈活而統一的介面,簡化了對復雜物件結構的處理,同時保持了系統的可延伸性。

    七、總結

    組合模式(Composite Pattern)是一種結構型設計模式,適用於將物件組合成樹形結構來表示「部份-整體」的層次關系。它允許使用者像使用單個物件一樣操作物件組合,簡化了復雜結構的處理。組合模式的主要參與者包括抽象元件(Component),葉子節點(Leaf),和組合物件(Composite)。每個元件透過統一介面,支持遞迴處理子節點,使得系統更加靈活、易擴充套件。

    以檔案系統為例,資料夾可以包含檔或其他資料夾。在不使用組合模式時,檔和資料夾必須分別處理,導致程式碼復雜性增加且擴充套件性差。透過組合模式,可以使用統一的介面管理檔和資料夾,簡化階層並增強系統的擴充套件性和靈活性。

    組合模式在JDK源碼中有廣泛套用,如 java.awt 中的元件樹(Component 和 Container 類),以及集合框架中的 List、ArrayList 和 Collections.unmodifiableList。這些類透過組合模式處理遞迴結構和物件組合,使得操作統一、靈活。

    - EOF -

    推薦閱讀 點選標題可跳轉


    如何使用 ChatGPT o1-mini

    谷歌瀏覽器存取:https://www.nezhasoft.cn

    回復gpt,獲取ChatGPT4o、 o1-mini 直接使用地址

    點選閱讀原文,國內直接使用ChatGpt4o、o1-mini