簡介
Compose objects into tree structures to represent part-whole hierarchies.Composite lets clients treat individual objects and compositions of objects uniformly.
將對象組合成樹形結(jié)構(gòu)以表示 “部分-整體” 的層次結(jié)構(gòu)迂烁,使得用戶對單個對象和組合對象的使用具有一致性伪节。
組合模式(Composite Pattern) 也稱為 整體-部分(Part-Whole)模式,它的宗旨是通過將單個對象(葉子節(jié)點)和組合對象(樹枝節(jié)點)用相同的接口進(jìn)行表示,使得客戶對單個對象和組合對象的使用具有一致性铸屉。
組合模式 一般用來描述 整體 與 部分 的關(guān)系,它將對象組織到樹形結(jié)構(gòu)中钳枕,最頂層的節(jié)點稱為 根節(jié)點横朋,根節(jié)點下面可以包含 樹枝節(jié)點 和 葉子節(jié)點,樹枝節(jié)點下面又可以包含 樹枝節(jié)點 和 葉子節(jié)點兑燥。如下圖所示:
由上圖可以看出亮瓷,其實 根節(jié)點 和 樹枝節(jié)點 本質(zhì)上是同一種數(shù)據(jù)類型(藍(lán)色圓圈),可以作為容器使用降瞳;而 葉子節(jié)點 與 樹枝節(jié)點 在語義上不屬于同一種類型嘱支,但是在 組合模式 中,會把 樹枝節(jié)點 和 葉子節(jié)點 認(rèn)為是同一種數(shù)據(jù)類型(用同一接口定義)挣饥,讓它們具備一致行為除师。這樣,在 組合模式 中扔枫,整個樹形結(jié)構(gòu)中的對象都是同一種類型汛聚,帶來的一個好處就是客戶無需辨別 樹枝節(jié)點 還是 葉子節(jié)點,而是可以直接進(jìn)行操作短荐,給客戶使用帶來極大的便利倚舀。
組合模式 核心:借助同一接口,使葉子節(jié)點和樹枝節(jié)點的操作具備一致性忍宋。
主要解決
當(dāng)子系統(tǒng)與其內(nèi)各個對象層次呈現(xiàn)樹形結(jié)構(gòu)時瞄桨,可以使用 組合模式 讓該子系統(tǒng)內(nèi)各個對象層次的行為操作具備一致性⊙茸伲客戶端使用該子系統(tǒng)內(nèi)任意一個層次對象時芯侥,無須進(jìn)行區(qū)分,直接使用通用操作即可,為客戶端的使用帶來了便捷柱查。
注:如果樹形結(jié)構(gòu)系統(tǒng)不使用 組合模式 進(jìn)行架構(gòu)廓俭,那么按照正常的思維邏輯,對該系統(tǒng)進(jìn)行職責(zé)分析唉工,按上文樹形結(jié)構(gòu)圖所示研乒,該系統(tǒng)具備兩種對象層次類型:樹枝節(jié)點和葉子節(jié)點。那么我們就需要構(gòu)造兩種對應(yīng)的類型淋硝,然后由于樹枝節(jié)點具備容器功能雹熬,因此樹枝節(jié)點類內(nèi)部需維護(hù)多個集合存儲其他對象層次(eg:List<Composite>
,List<Leaf>
),如果當(dāng)前系統(tǒng)對象層次更復(fù)雜時谣膳,那么樹枝節(jié)點內(nèi)就又要增加對應(yīng)的層次集合竿报,這對樹枝節(jié)點的構(gòu)建帶來了巨大的復(fù)雜性,臃腫性以及不可擴(kuò)展性继谚。同時客戶端訪問該系統(tǒng)層次時烈菌,還需進(jìn)行層次區(qū)分,這樣才能使用對應(yīng)的行為花履,給客戶端的使用也帶來了巨大的復(fù)雜性芽世。而如果使用 組合模式 構(gòu)建該系統(tǒng),由于 組合模式 抽取了系統(tǒng)各個層次的共性行為诡壁,具體層次只需按需實現(xiàn)所需行為即可济瓢,這樣子系統(tǒng)各個層次就都屬于同一種類型,所以樹枝節(jié)點只需維護(hù)一個集合(List<Component>
)即可存儲系統(tǒng)所有層次內(nèi)容妹卿,并且客戶端也無需區(qū)分該系統(tǒng)各個層次對象葬荷,對內(nèi)系統(tǒng)架構(gòu)簡潔優(yōu)雅,對外接口精簡易用纽帖。
優(yōu)缺點
優(yōu)點
- 組合模式 屏蔽了對象系統(tǒng)的層次差異性(樹節(jié)點和葉子節(jié)點為不同類型)宠漩,將客戶代碼與復(fù)雜的容器對象解耦,使得客戶端可以忽略層次間的差異懊直,使用一致的行為控制不同層次扒吁。
- 在 組合模式 可以很方便地增加 樹枝節(jié)點 和 葉子節(jié)點 對象,并對現(xiàn)有類庫無侵入室囊,符合 開閉原則雕崩;
缺點
- 如果類系統(tǒng)(樹形結(jié)構(gòu))過于龐大,雖然對不同層次都提供一致性操作融撞,但客戶端仍需花費時間理清類之間的層次關(guān)系盼铁;
- 組合模式 在具體實現(xiàn)上違背了設(shè)計模式 接口隔離原則 或 依賴倒置原則;
使用場景
- 系統(tǒng)對象層次具備整體和部分尝偎,呈樹形結(jié)構(gòu)饶火,且要求具備統(tǒng)一行為(如樹形菜單鹏控,操作系統(tǒng)目錄結(jié)構(gòu),公司組織架構(gòu)等)肤寝;
模式講解
組合模式 主要包含三種角色:
- 抽象根節(jié)點(Component):定義系統(tǒng)各層次對象的共有方法和屬性当辐,可以預(yù)先定義一些默認(rèn)行為和屬性;
- 樹枝節(jié)點(Composite):定義樹枝節(jié)點的行為鲤看,存儲子節(jié)點缘揪,組合樹枝節(jié)點和葉子節(jié)點形成一個樹形結(jié)構(gòu);
- 葉子節(jié)點(Leaf):葉子節(jié)點對象义桂,其下再無分支找筝,是系統(tǒng)層次遍歷的最小單位;
組合模式 在代碼具體實現(xiàn)上慷吊,有兩種不同的方式:
- 透明模式:把組合(樹節(jié)點)使用的方法放到統(tǒng)一行為(Component)中袖裕,讓不同層次(樹節(jié)點,葉子節(jié)點)的結(jié)構(gòu)都具備一致行為罢浇;其 UML 類圖如下所示:
透明組合模式 把所有公共方法都定義在 Component 中陆赋,這樣做的好處是客戶端無需分辨是葉子節(jié)點(Leaf)和樹枝節(jié)點(Composite)沐祷,它們具備完全一致的接口嚷闭;缺點是葉子節(jié)點(Leaf)會繼承得到一些它所不需要(管理子類操作的方法)的方法,這與設(shè)計模式 接口隔離原則 相違背赖临。
透明組合模式 通用代碼如下所示:
package com.yn.design_pattern.composite.universal.transparent;
import java.util.ArrayList;
import java.util.List;
class Client {
public static void main(String[] args) {
// 來一個根節(jié)點
Component root = new Composite("root");
// 來一個樹枝節(jié)點
Component branchA = new Composite("---branchA");
Component branchB = new Composite("------branchB");
// 來一個葉子節(jié)點
Component leafA = new Leaf("------leafA");
Component leafB = new Leaf("---------leafB");
Component leafC = new Leaf("---leafC");
root.addChild(branchA);
root.addChild(leafC);
branchA.addChild(leafA);
branchA.addChild(branchB);
branchB.addChild(leafB);
String result = root.operation();
System.out.println(result);
}
// 抽象根節(jié)點
static abstract class Component {
protected String name;
public Component(String name) {
this.name = name;
}
public abstract String operation();
public boolean addChild(Component component) {
throw new UnsupportedOperationException("addChild not supported!");
}
public boolean removeChild(Component component) {
throw new UnsupportedOperationException("removeChild not supported!");
}
public Component getChild(int index) {
throw new UnsupportedOperationException("getChild not supported!");
}
}
// 樹節(jié)點
static class Composite extends Component {
private List<Component> mComponents;
public Composite(String name) {
super(name);
this.mComponents = new ArrayList<>();
}
@Override
public String operation() {
StringBuilder builder = new StringBuilder(this.name);
for (Component component : this.mComponents) {
builder.append("\n");
builder.append(component.operation());
}
return builder.toString();
}
@Override
public boolean addChild(Component component) {
return this.mComponents.add(component);
}
@Override
public boolean removeChild(Component component) {
return this.mComponents.remove(component);
}
@Override
public Component getChild(int index) {
return this.mComponents.get(index);
}
}
//葉子節(jié)點
static class Leaf extends Component {
public Leaf(String name) {
super(name);
}
@Override
public String operation() {
return this.name;
}
}
}
透明組合模式 中胞锰,由于 Component 包含葉子節(jié)點所不需要的方法,因此兢榨,我們直接將這些方法默認(rèn)拋出UnsupportedOperationException
異常嗅榕。
- 安全模式:統(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è)計模式 依賴倒置原則驹尼。
安全組合模式 的通用代碼相對 透明組合模式 而言,需要進(jìn)行如下修改:
- 修改 Component 代碼:只保留各層次公有行為:
// 抽象根節(jié)點
static abstract class Component {
protected String name;
public Component(String name) {
this.name = name;
}
public abstract String operation();
}
- 修改客戶端代碼:將樹枝節(jié)點類型更改為 Composite 類型庞呕,以便獲取管理子類操作的方法:
class Client {
public static void main(String[] args) {
// 來一個根節(jié)點
Composite root = new Composite("root");
// 來一個樹枝節(jié)點
Composite branchA = new Composite("---branchA");
Composite branchB = new Composite("------branchB");
// 來一個葉子節(jié)點
Component leafA = new Leaf("------leafA");
Component leafB = new Leaf("---------leafB");
Component leafC = new Leaf("---leafC");
root.addChild(branchA);
root.addChild(leafC);
branchA.addChild(leafA);
branchA.addChild(branchB);
branchB.addChild(leafB);
String result = root.operation();
System.out.println(result);
}
}
上述例子的運行結(jié)果如下:
root
---branchA
------leafA
------branchB
---------leafB
---leafC
運行結(jié)果顯示:root 包含 branchA 和 leafC新翎;branchA 包含 leafA 和 branchB;branchB 包含 leafB。
這樣我們就使用 組合模式 模擬了一個樹形結(jié)構(gòu)系統(tǒng)料祠。
問:透明組合模式 和 安全組合模式 都有各自的優(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)定(健壯)時来涨,采用 安全組合模式
注:設(shè)計模式的出現(xiàn)并不是說我們要寫的代碼一定要遵循設(shè)計模式所要求的方方面面,這是不現(xiàn)實同時也是不可能的启盛。設(shè)計模式的出現(xiàn)蹦掐,其實只是強調(diào)好的代碼所具備的一些特征(六大設(shè)計原則),這些特征對于項目開發(fā)是具備積極效應(yīng)的僵闯,但不是說我們每實現(xiàn)一個類就一定要全部滿足設(shè)計模式的要求卧抗,如果真的存在完全滿足設(shè)計模式的要求,反而可能存在過度設(shè)計的嫌疑鳖粟。同時社裆,23種設(shè)計模式,其實都是嚴(yán)格依循設(shè)計模式六大原則進(jìn)行設(shè)計向图,只是不同的模式在不同的場景中會更加適用泳秀。設(shè)計模式的理解應(yīng)該重于意而不是形,真正編碼時榄攀,經(jīng)常使用的是某種設(shè)計模式的變形體嗜傅,真正切合項目的模式才是正確的模式。
舉個例子
例子:對于程序員來說檩赢,電腦是我們每天都要接觸的吕嘀。它的目錄系統(tǒng)其實就是一個典型的樹形結(jié)構(gòu),目錄包含文件夾和文件漠畜,文件夾里面又可以包含文件夾和文件···下面我們就用代碼來實現(xiàn)一個目錄系統(tǒng)币他。
分析:目錄系統(tǒng)有兩個大的層次:文件夾,文件憔狞。其中蝴悉,文件夾能容乃其他層次,為樹枝節(jié)點瘾敢;文件為最小單位拍冠,為葉子節(jié)點尿这。由于目錄系統(tǒng)層次較少,且樹枝節(jié)點(文件夾)結(jié)構(gòu)相對穩(wěn)定庆杜,而文件其實可以有很多類型射众,所以這里我們選擇使用 安全組合模式 來實現(xiàn)目錄系統(tǒng),可以避免為葉子類型(文件)引入冗余方法晃财。
代碼如下:
class Client {
public static void main(String[] args) {
Folder diskC = new Folder("C:\\");
Folder windows = new Folder("---Windows");
Folder system32 = new Folder("------system32");
File calcFile = new File("---------calc.exe");
File pingFile = new File("---------ping.exe");
diskC.addDir(windows);
windows.addDir(system32);
system32.addDir(calcFile);
system32.addDir(pingFile);
diskC.print();
}
//抽象根類:Component
static abstract class Directory {
protected String name;
public Directory(String name) {
this.name = name;
}
public abstract void print();
}
//葉子節(jié)點:Leaf
static class File extends Directory {
public File(String name) {
super(name);
}
@Override
public void print() {
System.out.println(this.name + ": file");
}
}
//樹枝節(jié)點:Composite
static class Folder extends Directory {
private List<Directory> mDirs;
public Folder(String name) {
super(name);
this.mDirs = new ArrayList<>();
}
@Override
public void print() {
System.out.println(this.name + ": Folder");
for (Directory dir : this.mDirs) {
dir.print();
}
}
public boolean addDir(Directory dir) {
return this.mDirs.add(dir);
}
public boolean removeDir(Directory dir) {
return this.mDirs.remove(dir);
}
public Directory getDir(int index) {
return this.mDirs.get(index);
}
}
}
客戶端創(chuàng)建了一個磁盤 C:\叨橱,并再 C 盤下創(chuàng)建了 windows 目錄,windows 目錄下又創(chuàng)建了 system32 目錄断盛,system32 目錄下包含兩個文件 calc.exe 和 ping.exe罗洗。上面代碼執(zhí)行的結(jié)果如下:
C:\: Folder
---Windows: Folder
------system32: Folder
---------calc.exe: File
---------ping.exe: File