Java設(shè)計模式之java組合模式詳解
引言
樹形結(jié)構(gòu)不論在生活中或者是開發(fā)中都是一種非常常見的結(jié)構(gòu),一個容器對象(如文件夾)下可以存放多種不同的葉子對象或者容器對象,容器對象與葉子對象之間屬性差別可能非常大。
由于容器對象和葉子對象在功能上的區(qū)別,在使用這些對象的代碼中必須有區(qū)別地對待容器對象和葉子對象,而實際上大多數(shù)情況下我們希望一致地處理它們,因為對于這些對象的區(qū)別對待將會使得程序非常復雜。
組合模式為解決此類問題而誕生,它可以讓葉子對象和容器對象的使用具有一致性。
組合模式介紹
組合多個對象形成樹形結(jié)構(gòu)以表示具有 “整體—部分” 關(guān)系的層次結(jié)構(gòu)。組合模式對單個對象(即葉子對象)和組合對象(即容器對象)的使用具有一致性,組合模式又可以稱為 “整體—部分”(Part-Whole) 模式,它是一種對象結(jié)構(gòu)型模式。
由于在軟件開發(fā)中存在大量的樹形結(jié)構(gòu),因此組合模式是一種使用頻率較高的結(jié)構(gòu)型設(shè)計模式,
在XML解析、組織結(jié)構(gòu)樹處理、文件系統(tǒng)設(shè)計等領(lǐng)域,組合模式都得到了廣泛應用。
角色
- Component(抽象構(gòu)件):它可以是接口或抽象類,為葉子構(gòu)件和容器構(gòu)件對象聲明接口,在該角色中可以包含所有子類共有行為的聲明和實現(xiàn)。在抽象構(gòu)件中定義了訪問及管理它的子構(gòu)件的方法,如增加子構(gòu)件、刪除子構(gòu)件、獲取子構(gòu)件等。
- Leaf(葉子構(gòu)件):它在組合結(jié)構(gòu)中表示葉子節(jié)點對象,葉子節(jié)點沒有子節(jié)點,它實現(xiàn)了在抽象構(gòu)件中定義的行為。對于那些訪問及管理子構(gòu)件的方法,可以通過異常等方式進行處理。
- Composite(容器構(gòu)件):它在組合結(jié)構(gòu)中表示容器節(jié)點對象,容器節(jié)點包含子節(jié)點,其子節(jié)點可以是葉子節(jié)點,也可以是容器節(jié)點,它提供一個集合用于存儲子節(jié)點,實現(xiàn)了在抽象構(gòu)件中定義的行為,包括那些訪問及管理子構(gòu)件的方法,在其業(yè)務(wù)方法中可以遞歸調(diào)用其子節(jié)點的業(yè)務(wù)方法。
組合模式的關(guān)鍵是定義了一個抽象構(gòu)件類,它既可以代表葉子,又可以代表容器,而客戶端針對該抽象構(gòu)件類進行編程,無須知道它到底表示的是葉子還是容器,可以對其進行統(tǒng)一處理。同時容器對象與抽象構(gòu)件類之間還建立一個聚合關(guān)聯(lián)關(guān)系,在容器對象中既可以包含葉子,也可以包含容器,以此實現(xiàn)遞歸組合,形成一個樹形結(jié)構(gòu)。
模式結(jié)構(gòu)

示例代碼

典型的抽象構(gòu)件角色代碼:
public abstract class Component
{
public abstract void add(Component c);
public abstract void remove(Component c);
public abstract Component getChild(int i);
public abstract void operation();
}
典型的容器構(gòu)件角色代碼:
public class Leaf extends Component
{
public void add(Component c)
{ //異常處理或錯誤提示 }
public void remove(Component c)
{ //異常處理或錯誤提示 }
public Component getChild(int i)
{ //異常處理或錯誤提示 }
public void operation()
{
//實現(xiàn)代碼
}
}
水果盤
在水果盤(Plate)中有一些水果,如蘋果(Apple)、香蕉(Banana)、梨子(Pear),當然大水果盤中還可以有小水果盤,現(xiàn)需要對盤中的水果進行遍歷(吃),當然如果對一個水果盤執(zhí)行“吃”方法,實際上就是吃其中的水果。使用組合模式模擬該場景 。


//抽象構(gòu)建
public abstract class MyElement
{
public abstract void eat();
}
//容器構(gòu)建
import java.util.*;
public class Plate extends MyElement
{
private ArrayList list=new ArrayList();
public void add(MyElement element)
{
list.add(element);
}
public void delete(MyElement element)
{
list.remove(element);
}
public void eat()
{
for(Object object:list)
{
((MyElement)object).eat(); //遞歸
}
}
}
//葉子構(gòu)建
public class Apple extends MyElement
{
public void eat()
{
System.out.println("吃蘋果!");
}
}
//葉子構(gòu)建
public class Banana extends MyElement
{
public void eat()
{
System.out.println("吃香蕉!");
}
}
//葉子構(gòu)建
public class Pear extends MyElement
{
public void eat()
{
System.out.println("吃梨子!");
}
}
//客戶端
public class Client
{
public static void main(String a[])
{
MyElement obj1,obj2,obj3,obj4,obj5;
Plate plate1,plate2,plate3;
obj1=new Apple();
obj2=new Pear();
plate1=new Plate();
plate1.add(obj1);
plate1.add(obj2);
obj3=new Banana();
obj4=new Banana();
plate2=new Plate();
plate2.add(obj3);
plate2.add(obj4);
obj5=new Apple();
plate3=new Plate();
plate3.add(plate1);
plate3.add(plate2);
plate3.add(obj5);
plate3.eat();
}
}

文件瀏覽
我們來實現(xiàn)一個簡單的目錄樹,有文件夾和文件兩種類型,首先需要一個抽象構(gòu)件類,聲明了文件夾類和文件類需要的方法
public abstract class Component {
public String getName() {
throw new UnsupportedOperationException("不支持獲取名稱操作");
}
public void add(Component component) {
throw new UnsupportedOperationException("不支持添加操作");
}
public void remove(Component component) {
throw new UnsupportedOperationException("不支持刪除操作");
}
public void print() {
throw new UnsupportedOperationException("不支持打印操作");
}
public String getContent() {
throw new UnsupportedOperationException("不支持獲取內(nèi)容操作");
}
}
實現(xiàn)一個文件夾類 Folder,繼承 Component,定義一個 List<Component> 類型的componentList屬性,用來存儲該文件夾下的文件和子文件夾,并實現(xiàn) getName、add、remove、print等方法
public class Folder extends Component {
private String name;
private List<Component> componentList = new ArrayList<Component>();
public Integer level;
public Folder(String name) {
this.name = name;
}
@Override
public String getName() {
return this.name;
}
@Override
public void add(Component component) {
this.componentList.add(component);
}
@Override
public void remove(Component component) {
this.componentList.remove(component);
}
@Override
public void print() {
System.out.println(this.getName());
if (this.level == null) {
this.level = 1;
}
String prefix = "";
for (int i = 0; i < this.level; i++) {
prefix += "\t- ";
}
for (Component component : this.componentList) {
if (component instanceof Folder){
((Folder)component).level = this.level + 1;
}
System.out.print(prefix);
component.print();
}
this.level = null;
}
}
文件類 File,繼承Component父類,實現(xiàn) getName、print、getContent等方法
public class File extends Component {
private String name;
private String content;
public File(String name, String content) {
this.name = name;
this.content = content;
}
@Override
public String getName() {
return this.name;
}
@Override
public void print() {
System.out.println(this.getName());
}
@Override
public String getContent() {
return this.content;
}
}
我們來測試一下
public class Test {
public static void main(String[] args) {
Folder DSFolder = new Folder("設(shè)計模式資料");
File note1 = new File("組合模式筆記.md", "組合模式組合多個對象形成樹形結(jié)構(gòu)以表示具有 \"整體—部分\" 關(guān)系的層次結(jié)構(gòu)");
File note2 = new File("工廠方法模式.md", "工廠方法模式定義一個用于創(chuàng)建對象的接口,讓子類決定將哪一個類實例化。");
DSFolder.add(note1);
DSFolder.add(note2);
Folder codeFolder = new Folder("樣例代碼");
File readme = new File("README.md", "# 設(shè)計模式示例代碼項目");
Folder srcFolder = new Folder("src");
File code1 = new File("組合模式示例.java", "這是組合模式的示例代碼");
srcFolder.add(code1);
codeFolder.add(readme);
codeFolder.add(srcFolder);
DSFolder.add(codeFolder);
DSFolder.print();
}
}
輸出結(jié)果
設(shè)計模式資料
- 組合模式筆記.md
- 工廠方法模式.md
- 樣例代碼
- - README.md
- - src
- - - 組合模式示例.java
在這里父類 Component 是一個抽象構(gòu)件類,Folder 類是一個容器構(gòu)件類,F(xiàn)ile 是一個葉子構(gòu)件類,F(xiàn)older 和 File 繼承了 Component,Folder 與 Component 又是聚合關(guān)系
更復雜的組合模式

透明與安全
在使用組合模式時,根據(jù)抽象構(gòu)件類的定義形式,我們可將組合模式分為透明組合模式和安全組合模式兩種形式。
透明組合模式
透明組合模式中,抽象構(gòu)件角色中聲明了所有用于管理成員對象的方法,譬如在示例中 Component 聲明了 add、remove 方法,這樣做的好處是確保所有的構(gòu)件類都有相同的接口。透明組合模式也是組合模式的標準形式。
透明組合模式的缺點是不夠安全,因為葉子對象和容器對象在本質(zhì)上是有區(qū)別的,葉子對象不可能有下一個層次的對象,即不可能包含成員對象,因此為其提供 add()、remove() 等方法是沒有意義的,這在編譯階段不會出錯,但在運行階段如果調(diào)用這些方法可能會出錯(如果沒有提供相應的錯誤處理代碼)

安全組合模式
在安全組合模式中,在抽象構(gòu)件角色中沒有聲明任何用于管理成員對象的方法,而是在容器構(gòu)件 Composite 類中聲明并實現(xiàn)這些方法
安全組合模式的缺點是不夠透明,因為葉子構(gòu)件和容器構(gòu)件具有不同的方法,且容器構(gòu)件中那些用于管理成員對象的方法沒有在抽象構(gòu)件類中定義,因此客戶端不能完全針對抽象編程,必須有區(qū)別地對待葉子構(gòu)件和容器構(gòu)件。
在實際應用中 java.awt 和 swing 中的組合模式即為安全組合模式。

組合模式總結(jié)
優(yōu)點
- 組合模式可以清楚地定義分層次的復雜對象,表示對象的全部或部分層次,它讓客戶端忽略了層次的差異,方便對整個層次結(jié)構(gòu)進行控制。
- 客戶端可以一致地使用一個組合結(jié)構(gòu)或其中單個對象,不必關(guān)心處理的是單個對象還是整個組合結(jié)構(gòu),簡化了客戶端代碼。
- 在組合模式中增加新的容器構(gòu)件和葉子構(gòu)件都很方便,無須對現(xiàn)有類庫進行任何修改,符合“開閉原則”。
- 組合模式為樹形結(jié)構(gòu)的面向?qū)ο髮崿F(xiàn)提供了一種靈活的解決方案,通過葉子對象和容器對象的遞歸組合,可以形成復雜的樹形結(jié)構(gòu),但對樹形結(jié)構(gòu)的控制卻非常簡單。
缺點
- 使得設(shè)計更加復雜,客戶端需要花更多時間理清類之間的層次關(guān)系。
- 在增加新構(gòu)件時很難對容器中的構(gòu)件類型進行限制。
適用場景
- 在具有整體和部分的層次結(jié)構(gòu)中,希望通過一種方式忽略整體與部分的差異,客戶端可以一致地對待它們。
- 在一個使用面向?qū)ο笳Z言開發(fā)的系統(tǒng)中需要處理一個樹形結(jié)構(gòu)。
- 在一個系統(tǒng)中能夠分離出葉子對象和容器對象,而且它們的類型不固定,需要增加一些新的類型。
應用
XML文檔解析

1 <?xml version="1.0"?> 2 <books> 3 <book> 4 <author>Carson</author> 5 <price format="dollar">31.95</price> 6 <pubdate>05/01/2001</pubdate> 7 </book> 8 <pubinfo> 9 <publisher>MSPress</publisher> 10 <state>WA</state> 11 </pubinfo> 12 </books>
文件
操作系統(tǒng)中的目錄結(jié)構(gòu)是一個樹形結(jié)構(gòu),因此在對文件和文件夾進行操作時可以應用組合模式,例如殺毒軟件在查毒或殺毒時,既可以針對一個具體文件,也可以針對一個目錄。如果是對目錄查毒或殺毒,將遞歸處理目錄中的每一個子目錄和文件。
HashMap
HashMap 提供 putAll 的方法,可以將另一個 Map 對象放入自己的存儲空間中,如果有相同的 key 值則會覆蓋之前的 key 值所對應的 value 值
public class Test {
public static void main(String[] args) {
Map<String, Integer> map1 = new HashMap<String, Integer>();
map1.put("aa", 1);
map1.put("bb", 2);
map1.put("cc", 3);
System.out.println("map1: " + map1);
Map<String, Integer> map2 = new LinkedMap();
map2.put("cc", 4);
map2.put("dd", 5);
System.out.println("map2: " + map2);
map1.putAll(map2);
System.out.println("map1.putAll(map2): " + map1);
}
}
輸出結(jié)果
map1: {aa=1, bb=2, cc=3}
map2: {cc=4, dd=5}
map1.putAll(map2): {aa=1, bb=2, cc=4, dd=5}
查看 putAll 源碼
public void putAll(Map<? extends K, ? extends V> m) {
putMapEntries(m, true);
}
putAll 接收的參數(shù)為父類 Map 類型,所以 HashMap 是一個容器類,Map 的子類為葉子類,當然如果 Map 的其他子類也實現(xiàn)了 putAll 方法,那么它們都既是容器類,又都是葉子類
Mybatis SqlNode中的組合模式
MyBatis 的強大特性之一便是它的動態(tài)SQL,其通過 if, choose, when, otherwise, trim, where, set, foreach 標簽,可組合成非常靈活的SQL語句,從而提高開發(fā)人員的效率。
<select id="findActiveBlogLike" resultType="Blog">
SELECT * FROM BLOG WHERE state = ‘ACTIVE'
<if test="title != null">
AND title like #{title}
</if>
<if test="author != null and author.name != null">
AND author_name like #{author.name}
</if>
</select>
Mybatis在處理動態(tài)SQL節(jié)點時,應用到了組合設(shè)計模式,Mybatis會將映射配置文件中定義的動態(tài)SQL節(jié)點、文本節(jié)點等解析成對應的 SqlNode 實現(xiàn),并形成樹形結(jié)構(gòu)。
SQLNode 的類圖如下所示:

需要先了解 DynamicContext 類的作用:主要用于記錄解析動態(tài)SQL語句之后產(chǎn)生的SQL語句片段,可以認為它是一個用于記錄動態(tài)SQL語句解析結(jié)果的容器
抽象構(gòu)件為 SqlNode 接口,源碼如下:
public interface SqlNode {
boolean apply(DynamicContext context);
}
apply 是 SQLNode 接口中定義的唯一方法,該方法會根據(jù)用戶傳入的實參,參數(shù)解析該SQLNode所記錄的動態(tài)SQL節(jié)點,并調(diào)用 DynamicContext.appendSql() 方法將解析后的SQL片段追加到 DynamicContext.sqlBuilder 中保存,當SQL節(jié)點下所有的 SqlNode 完成解析后,我們就可以從 DynamicContext 中獲取一條動態(tài)生產(chǎn)的、完整的SQL語句
然后來看 MixedSqlNode 類的源碼:
public class MixedSqlNode implements SqlNode {
private List<SqlNode> contents;
public MixedSqlNode(List<SqlNode> contents) {
this.contents = contents;
}
@Override
public boolean apply(DynamicContext context) {
for (SqlNode sqlNode : contents) {
sqlNode.apply(context);
}
return true;
}
}
MixedSqlNode 維護了一個 List< SqlNode > 類型的列表,用于存儲 SqlNode 對象,apply 方法通過 for循環(huán) 遍歷 contents 并調(diào)用其中對象的 apply 方法,這里跟我們的示例中的 Folder 類中的 print 方法非常類似,很明顯 MixedSqlNode 扮演了容器構(gòu)件角色
對于其他SqlNode子類的功能,稍微概括如下:
- TextSqlNode:表示包含 ${} 占位符的動態(tài)SQL節(jié)點,其 apply 方法會使用 GenericTokenParser解析 ${} 占位符,并直接替換成用戶給定的實際參數(shù)值
- IfSqlNode:對應的是動態(tài)SQL節(jié)點 < If > 節(jié)點,其 apply 方法首先通過ExpressionEvaluator.evaluateBoolean() 方法檢測其 test 表達式是否為 true,然后根據(jù)test 表達式的結(jié)果,決定是否執(zhí)行其子節(jié)點的 apply() 方法
- TrimSqlNode :會根據(jù)子節(jié)點的解析結(jié)果,添加或刪除相應的前綴或后綴。
- WhereSqlNode 和 SetSqlNode 都繼承了 TrimSqlNode
- ForeachSqlNode:對應 < for each > 標簽,對集合進行迭代
- 動態(tài)SQL中的 < choose >、< when >、< otherwise > 分別解析成ChooseSqlNode、IfSqlNode、MixedSqlNode
綜上,SqlNode 接口有多個實現(xiàn)類,每個實現(xiàn)類對應一個動態(tài)SQL節(jié)點,其中 SqlNode 扮演抽象構(gòu)件角色,MixedSqlNode 扮演容器構(gòu)件角色,其它一般是葉子構(gòu)件角色
參考文章
總結(jié)
本篇文章就到這里了,希望能夠給你帶來幫助,也希望您能夠多多關(guān)注腳本之家的更多內(nèi)容!
相關(guān)文章
SSM框架中entity mapper dao service controll
這篇文章主要介紹了SSM框架中entity mapper dao service controller層的使用方式,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2023-11-11
Spring boot整合shiro+jwt實現(xiàn)前后端分離
這篇文章主要為大家詳細介紹了Spring boot整合shiro+jwt實現(xiàn)前后端分離,文中示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下2019-12-12
Java使用正則表達式去除小數(shù)點后面多余的0功能示例
這篇文章主要介紹了Java使用正則表達式去除小數(shù)點后面多余的0功能,結(jié)合具體實例形式分析了java字符串正則替換相關(guān)操作技巧,需要的朋友可以參考下2017-06-06
springboot配置過濾器和多個攔截器、執(zhí)行順序(案例詳解)
這篇文章主要介紹了springboot配置過濾器和多個攔截器、執(zhí)行順序,在文章開頭給大家介紹了過濾器配置的兩種方法,創(chuàng)建兩個攔截器,重寫方法結(jié)合實例代碼給大家介紹的非常詳細,需要的朋友可以參考下2023-10-10
Java中easypoi導入excel文件列名相同的處理方案
這篇文章主要介紹了Java中easypoi導入excel文件列名相同的處理方案,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2020-06-06
基于JavaScript動態(tài)規(guī)劃編寫一個益智小游戲
最近在學習動態(tài)規(guī)劃相關(guān)的知識,所以本文將利用動態(tài)規(guī)劃編寫一個簡單的益智小游戲,文中的示例代碼講解詳細,感興趣的小伙伴可以了解一下2023-06-06

