組合模式介紹
組合模式(Composite Pattern)也稱為部分整體模式(Part-Whole Pattern),是結(jié)構(gòu)型設(shè)計模式之一酱固,它將一組相似的對象看做一個對象處理,并根據(jù)一個樹狀結(jié)構(gòu)來組合對象隅俘,然后提供統(tǒng)一的方法去訪問相應(yīng)的對象曼玩,以此忽略掉對象與對象之間的差別烘贴。
組合模式的定義
將對象組合成樹形結(jié)構(gòu)以表示 “部分-整體” 的層次結(jié)構(gòu)羡儿,使得用戶對單個對象和組合對象的使用具有一致性事示。
組合模式的使用場景
表示部分早像、整體層次結(jié)構(gòu)時,如樹形菜單肖爵,文件卢鹦、文件夾的管理。
組合模式的 UML 類圖
角色介紹:
- Component:抽象根節(jié)點劝堪,定義系統(tǒng)各層次對象的共有方法和屬性冀自,可以預(yù)先定義一些默認(rèn)行為和屬性揉稚。
- Composite:樹枝節(jié)點,定義樹枝節(jié)點的行為熬粗,存儲子節(jié)點搀玖,組合樹枝節(jié)點和葉子節(jié)點形成一個樹形結(jié)構(gòu);
- Leaf:葉子節(jié)點驻呐,葉子節(jié)點對象灌诅,其下再無節(jié)點,是系統(tǒng)層次遍歷的最小單位含末。
組合模式 在代碼具體實現(xiàn)上猜拾,有兩種不同的方式:
-
透明組合模式:把組合(樹節(jié)點)使用的方法放到統(tǒng)一行為(Component)中,讓不同層次(樹節(jié)點答渔,葉子節(jié)點)的結(jié)構(gòu)都具備一致行為关带;其 UML 類圖如下所示:
把所有公共方法都定義在 Component 中,這樣做的好處是客戶端無需分辨是葉子節(jié)點(Leaf)和樹枝節(jié)點(Composite)沼撕,它們具備完全一致的接口宋雏;缺點是葉子節(jié)點(Leaf)會繼承得到一些它所不需要(管理子類操作的方法)的方法,這與設(shè)計模式接口隔離原則相違背务豺。 -
安全組合模式:統(tǒng)一行為(Component)只規(guī)定系統(tǒng)各個層次的最基礎(chǔ)的一致行為磨总,而把組合(樹節(jié)點)本身的方法(管理子類對象的添加,刪除等)放到自身當(dāng)中笼沥;其 UML 類圖如下所示:
把系統(tǒng)各層次公有的行為定義在 Component 中蚪燕,把組合(樹節(jié)點)特有的行為(管理子類增加,刪除等)放到自身(Composite)中奔浅。這樣做的好處是接口定義職責(zé)清晰馆纳,符合設(shè)計模式 單一職責(zé)原則 和 接口隔離原則;缺點是客戶需要區(qū)分樹枝節(jié)點(Composite)和葉子節(jié)點(Leaf)汹桦,這樣才能正確處理各個層次的操作鲁驶,客戶端無法依賴抽象(Component),違背了設(shè)計模式 依賴倒置原則舞骆。
組合模式的實現(xiàn)
這里以文件管理器中的文件和文件夾的層級結(jié)構(gòu)為例
透明組合模式
抽象根節(jié)點 Component
public abstract class Dir {
private String name;
public Dir(String name) {
this.name = name;
}
public abstract void addDir(Dir dir);
public abstract void removeDir(Dir dir);
// 打印目錄結(jié)構(gòu)
public abstract void print(int depth);
public String getName() {
return name;
}
}
樹枝節(jié)點 Composite
對應(yīng)的就是文件夾
public class Folder extends Dir {
private List<Dir> dirs = new ArrayList<>();
public Folder(String name) {
super(name);
}
@Override
public void addDir(Dir dir) {
dirs.add(dir);
}
@Override
public void removeDir(Dir dir) {
dirs.remove(dir);
}
@Override
public void print(int depth) {
for (int i = 0; i < depth; i++) {
System.out.print("--");
}
System.out.println(getName());
for (Dir dir : dirs) {
dir.print(depth + 1);
}
}
}
葉子節(jié)點 Leaf
也就是文件類
public class File extends Dir {
public File(String name) {
super(name);
}
@Override
public void addDir(Dir dir) {
throw new UnsupportedOperationException("文件對象不支持該操作");
}
@Override
public void removeDir(Dir dir) {
throw new UnsupportedOperationException("文件對象不支持該操作");
}
@Override
public void print(int depth) {
for (int i = 0; i < depth; i++) {
System.out.print("--");
}
System.out.println(getName());
}
}
文件類不包含添加和刪除的操作钥弯,故拋出異常,這里就違背了接口隔離原則督禽。
客戶端
public class Client {
public static void main(String[] args) {
Dir root = new Folder("/");
Dir file = new File("root.txt");
root.addDir(file);
root.removeDir(file);
Dir folder1 = new Folder("home");
Dir file1 = new File("home.txt");
folder1.addDir(file1);
Dir folder2 = new Folder("etc");
Dir file2 = new File("etc.conf");
folder2.addDir(file2);
root.addDir(folder1);
root.addDir(folder2);
root.print(0);
}
}
客戶端聲明類型均采用抽象類型脆霎,符合依賴倒置原則。
輸出結(jié)果:
/
--home
----home.txt
--etc
----etc.conf
安全組合模式
將添加狈惫,刪除操作只存在樹枝節(jié)點中睛蛛,就變?yōu)榘踩M合模式。葉子節(jié)點就無需重寫自己不需要的方法,符合接口隔離原則玖院,此時客戶端要創(chuàng)建樹枝節(jié)點菠红,只能聲明為 Folder 類型,違背了依賴導(dǎo)致原則难菌。
問:透明組合模式 和 安全組合模式 都有各自的優(yōu)點和缺點,那么我們應(yīng)該優(yōu)先選擇哪一種呢蔑滓?
答:既然組合模式會被分為兩種實現(xiàn)郊酒,那么肯定是不同的場合某一種會更加適合,也即具體情況具體分析键袱。透明組合模式 將公共接口封裝到抽象根節(jié)點(Component)中燎窘,那么系統(tǒng)所有節(jié)點就具備一致行為,所以如果當(dāng)系統(tǒng)絕大多數(shù)層次具備相同的公共行為時蹄咖,采用 透明組合模式 也許會更好(代價:為剩下少數(shù)層次節(jié)點引入不需要的方法)褐健;而如果當(dāng)系統(tǒng)各個層次差異性行為較多或者樹節(jié)點層次相對穩(wěn)定(健壯)時,采用 安全組合模式澜汤。
總結(jié)
優(yōu)點
1.可以清晰地定義分層次的復(fù)雜對象蚜迅。
2.增加節(jié)點方便。
缺點
1.透明組合模式 和 安全組合模式俊抵,各自優(yōu)缺點比較明顯谁不,需要根據(jù)實際情況進(jìn)行選擇。
Android 源碼中組合模式
Android 源碼中有一個非常經(jīng)典的組合模式實現(xiàn)徽诲,那就是 View 和 ViewGroup 的組合刹帕,如下圖。
ViewGroup 就是容器谎替,相當(dāng)于樹枝節(jié)點偷溺,其可以包含 TextView,Button等,也可以包含繼承自 ViewGroup 的節(jié)點钱贯,但對于葉子節(jié)點 TextView 等就無法包含 ViewGroup挫掏。將添加、移除子節(jié)點的操作都定義在了 ViewGroup 中喷舀,故該組合模式為安全的組合模式砍濒。
為什么 ViewGroup 有容器的功能
ViewGroup 是繼承自 View 類的。
public abstract class ViewGroup extends View implements ViewParent, ViewManager {
...
}
ViewGroup 與 View 差別就在于 ViewGroup 實現(xiàn)了 ViewParent 和 ViewManager接口硫麻。
ViewManager 定義了 addView爸邢、removeView 等對子視圖操作的方法
public interface ViewManager
{
public void addView(View view, ViewGroup.LayoutParams params);
public void updateViewLayout(View view, ViewGroup.LayoutParams params);
public void removeView(View view);
}
而 ViewParent 則定義了刷新容器的接口 requestLayout 和其他一些焦點事件處理的接口
public interface ViewParent {
// 請求重新布局
public void requestLayout();
// 獲取父 View(不是父類)
public ViewParent getParent();
// 請求子視圖的焦點
public void requestChildFocus(View child, View focused);
...
}
ViewGroup 實現(xiàn)的這兩個接口與 View 不一樣,還有重要一點就是 ViewGroup 是抽象類拿愧,其將 View 中的 onLayout 方法重置為抽象方法杠河,也就是容器子類必須實現(xiàn),View 中該方法是空實現(xiàn),因為對應(yīng)一個普通 View 來說該方法沒有什么實現(xiàn)價值券敌。View 測繪流程的 onMeasure 和 onDraw 在 ViewGroup 都沒有重寫唾戚,對于 onMeasure 方法,在ViewGroup 中增加了測量子View的方法待诅,如 measureChildren叹坦;而對于 onDraw 方法,ViewGroup 中定義了 dispatchDraw 方法來調(diào)用每一個子 View 的 onDraw 方法卑雁。由此可見募书,ViewGroup 就是一個容器,只負(fù)責(zé)對子元素的操作测蹲,而非具體的個體行為莹捡。