組合設計模式
組合模式是一種結構型設計模式,它允許你將對象組合成樹狀結構來表現“整體-部分”關系。組合模式使得客戶端可以統一對待單個對象和組合對象,從而使得代碼更加靈活和易于擴展。
概述
? 對于這個圖片肯定會非常熟悉,上圖我們可以看做是一個文件系統,對于這樣的結構我們稱之為樹形結構。在樹形結構中可以通過調用某個方法來遍歷整個樹,當我們找到某個葉子節點后,就可以對葉子節點進行相關的操作。可以將這顆樹理解成一個大的容器,容器里面包含很多的成員對象,這些成員對象即可是容器對象也可以是葉子對象。但是由于容器對象和葉子對象在功能上面的區別,使得我們在使用的過程中必須要區分容器對象和葉子對象,但是這樣就會給客戶帶來不必要的麻煩,作為客戶而已,它始終希望能夠一致的對待容器對象和葉子對象。
定義:
? 組合模式又名部分整體模式,是用于把一組相似的對象當作一個單一的對象。組合模式依據樹形結構來組合對象,用來表示部分以及整體層次。這種類型的設計模式屬于結構型模式,它創建了對象組的樹形結構。
在組合模式中,有兩種主要的參與者:
-
組件(Component):組件是組合中的對象的抽象類或接口,它定義了組合中所有對象的通用行為和屬性。組件可以是單個對象,也可以是組合對象,即包含其他組件的對象。
-
葉節點(Leaf):葉節點是組合中的單個對象,它實現了組件接口,但沒有子組件。
-
容器(Composite):容器是組合中的包含其他組件的對象,它實現了組件接口,并且包含一個子組件列表。容器可以包含葉節點或其他容器作為其子組件。
示例一:
// 組件接口
interface Component {void operation();
}// 葉節點
class Leaf implements Component {@Overridepublic void operation() {System.out.println("Leaf operation");}
}// 容器
class Composite implements Component {private List<Component> children = new ArrayList<>();public void add(Component component) {children.add(component);}public void remove(Component component) {children.remove(component);}@Overridepublic void operation() {System.out.println("Composite operation");for (Component component : children) {component.operation();}}
}// 客戶端
public class Client {public static void main(String[] args) {Component leaf1 = new Leaf();Component leaf2 = new Leaf();Component composite = new Composite();composite.add(leaf1);composite.add(leaf2);composite.operation();}
}
在這個示例中,Leaf類表示葉節點,Composite類表示容器,Client類表示客戶端。組件接口Component定義了組合中所有對象的通用行為,Leaf和Composite實現了該接口。Composite類中包含一個子組件列表,它的operation方法會遞歸調用子組件的operation方法。Client類創建了葉節點和容器對象,并調用容器對象的operation方法,從而實現了對組合結構的操作。
示例二:
軟件菜單
如下圖,我們在訪問別的一些管理系統時,經常可以看到類似的菜單。一個菜單可以包含菜單項(菜單項是指不再包含其他內容的菜單條目),也可以包含帶有其他菜單項的菜單,因此使用組合模式描述菜單就很恰當,我們的需求是針對一個菜單,打印出其包含的所有菜單以及菜單項的名稱。
要實現該案例,我們先畫出類圖:
代碼實現:
不管是菜單還是菜單項,都應該繼承自統一的接口,這里姑且將這個統一的接口稱為菜單組件。
/*** @author OldGj 2024/02/28* @version v1.0* @apiNote 抽象根節點 - 菜單組件*/
public abstract class MenuComponent {protected String name;protected int level;// 添加子菜單public void add(MenuComponent menuComponent){throw new UnsupportedOperationException();}// 刪除子菜單public void remove(MenuComponent menuComponent){throw new UnsupportedOperationException();}// 獲取指定的子菜單public MenuComponent getChild(int index){throw new UnsupportedOperationException();}// 獲取菜單名public String getName(){return name;}// 打印菜單即所有子菜單public abstract void print();}
這里的MenuComponent定義為抽象類,因為有一些共有的屬性和行為要在該類中實現,Menu和MenuItem類就可以只覆蓋自己感興趣的方法,而不用搭理不需要或者不感興趣的方法,舉例來說,Menu類可以包含子菜單,因此需要覆蓋add()、remove()、getChild()方法,但是MenuItem就不應該有這些方法。這里給出的默認實現是拋出異常,你也可以根據自己的需要改寫默認實現。
/*** @author OldGj 2024/02/28* @version v1.0* @apiNote 樹枝節點 - 菜單類*/
public class Menu extends MenuComponent {private final List<MenuComponent> menuComponentList = new ArrayList<>();public Menu(String name, int level) {this.name = name;this.level = level;}@Overridepublic void add(MenuComponent menuComponent) {menuComponentList.add(menuComponent);}@Overridepublic void remove(MenuComponent menuComponent) {menuComponentList.remove(menuComponent);}@Overridepublic MenuComponent getChild(int index) {return menuComponentList.get(index);}@Overridepublic String getName() {return super.getName();}@Overridepublic void print() {for (int i = 0; i < level; i++) {System.out.print("-");}System.out.println(name);for (MenuComponent menuComponent : menuComponentList) {menuComponent.print();}}
}
Menu類已經實現了除了getName方法的其他所有方法,因為Menu類具有添加菜單,移除菜單和獲取子菜單的功能。
/*** @author OldGj 2024/02/28* @version v1.0* @apiNote 葉子節點 - 菜單項*/
public class MenuItem extends MenuComponent {public MenuItem(String name, int level) {this.name = name;this.level = level;}@Overridepublic void print() {for (int i = 0; i < level; i++) {System.out.print("-");}System.out.println(name);}
}
MenuItem是菜單項,不能再有子菜單,所以添加菜單,移除菜單和獲取子菜單的功能并不能實現。
package com.tyut.pattern._02_structure_model.e06combination;/*** @author OldGj 2024/02/28* @version v1.0* @apiNote 客戶端 - 測試類*/
public class Client {public static void main(String[] args) {MenuComponent menu1 = new Menu("菜單管理",2);menu1.add(new MenuItem("頁面訪問",3));menu1.add(new MenuItem("展開菜單",3));MenuComponent menu2 = new Menu("權限配置",2);menu2.add(new MenuItem("頁面訪問",3));menu2.add(new MenuItem("提交保存",3));MenuComponent menu3 = new Menu("角色管理",2);menu3.add(new MenuItem("頁面訪問",3));menu3.add(new MenuItem("新增角色",3));MenuComponent component = new Menu("系統管理",1);component.add(menu1);component.add(menu2);component.add(menu3);component.print();}
}
組合模式分類:
組合模式可以根據其結構和功能特點進行不同的分類。根據結構的不同,組合模式可以分為兩種主要的類型:
- 透明式組合模式(Transparent Composite Pattern):
透明組合模式中,抽象根節點角色中聲明了所有用于管理成員對象的方法,比如在示例中 MenuComponent
聲明了 add
、remove
、getChild
方法,這樣做的好處是確保所有的構件類都有相同的接口。透明組合模式也是組合模式的標準形式。
透明組合模式的缺點是不夠安全,因為葉子對象和容器對象在本質上是有區別的,葉子對象不可能有下一個層次的對象,即不可能包含成員對象,因此為其提供 add()、remove() 等方法是沒有意義的,這在編譯階段不會出錯,但在運行階段如果調用這些方法可能會出錯(如果沒有提供相應的錯誤處理代碼)
- 安全式組合模式(Safe Composite Pattern):
在安全組合模式中,在抽象構件角色中沒有聲明任何用于管理成員對象的方法,而是在樹枝節點 Menu
類中聲明并實現這些方法。安全組合模式的缺點是不夠透明,因為葉子構件和容器構件具有不同的方法,且容器構件中那些用于管理成員對象的方法沒有在抽象構件類中定義,因此客戶端不能完全針對抽象編程,必須有區別地對待葉子構件和容器構件。
此外,組合模式還可以根據功能特點進行分類,主要有以下兩種類型:
-
單一職責的組合模式:
在單一職責的組合模式中,組件類負責自身的行為和職責,不負責管理子組件。這意味著葉節點和容器對象的職責是單一的,葉節點負責執行具體的操作,而容器對象負責組合子組件。單一職責的組合模式的主要特點是各個組件類的職責清晰,但可能需要更多的組合對象來實現復雜的操作。 -
透明的組合模式:
在透明的組合模式中,組件類負責自身的行為和管理子組件。這意味著葉節點和容器對象具有相同的接口和行為,客戶端無需區分葉節點和容器對象。透明的組合模式的主要特點是接口的統一性和一致性,但可能會導致接口中包含一些不適用于所有對象的方法。
優點
- 組合模式可以清楚地定義分層次的復雜對象,表示對象的全部或部分層次,它讓客戶端忽略了層次的差異,方便對整個層次結構進行控制。
- 客戶端可以一致地使用一個組合結構或其中單個對象,不必關心處理的是單個對象還是整個組合結構,簡化了客戶端代碼。
- 在組合模式中增加新的樹枝節點和葉子節點都很方便,無須對現有類庫進行任何修改,符合“開閉原則”。
- 組合模式為樹形結構的面向對象實現提供了一種靈活的解決方案,通過葉子節點和樹枝節點的遞歸組合,可以形成復雜的樹形結構,但對樹形結構的控制卻非常簡單。
使用場景
組合模式正是應樹形結構而生,所以組合模式的使用場景就是出現樹形結構的地方。比如:文件目錄顯示,多級目錄呈現等樹形結構數據的操作。