Java 设计模式——组合模式
概述
有時我們可能會被要求處理一個層級結(jié)構(gòu)明顯的對象,比如上下級的公司員工、比如層級嵌套的文件夾,還有豐富多彩的美食菜單??墒?#xff0c;我們可能要屢試不爽地編寫深度搜索代碼、要小心翼翼地編寫遞歸邏輯?,F(xiàn)在你可以忘掉這些,學(xué)習(xí)一些新的技能,讓你秒刷副本。當(dāng)然,這句有些夸張,你可以忽略。只是它單純地表達(dá)我對本文要說的這個模式的喜歡(也有可能只是因?yàn)槲夜ぷ髦星『糜龅竭@個問題)。
組合模式
定義
將對象組合成樹形結(jié)構(gòu)以表示**“部分-整體”**的層次結(jié)構(gòu)。組合模式使得用戶對單個對象和組合對象的使用具有一致性。
情境分析
看到組合的模式的定義,或許你就知道了這里的組合與我們平常討論的*“繼承-組合”*并不是同一個概念。因?yàn)樵趯W(xué)習(xí)組合模式之前,我就是把這兩個弄得有點(diǎn)混淆,所以一直沒有認(rèn)真地學(xué)習(xí)過它,以為組合也就是那么回事了嘛。可是,當(dāng)我開始真的學(xué)習(xí)它的時候,才知道,這是兩回事。
定義上說的是部分與整體的層次結(jié)構(gòu),可能就這前半句還不能說明什么,不過我們可以從后半句中找到突破點(diǎn),那就是單個對象與組合對象。也就是在組合對象中組合了一個部分的集合,這個集合中的元素也就是單個元素所具有的對象類型。
當(dāng)然,你不要理解成組合模式里也可以只有這個集合了。如果是這樣,那么一個 List 完全可以可以搞定,又何必搞出個模式來突顯逼格呢?
現(xiàn)在我們就來“舉個栗子”,對,是栗子。哈哈~
我現(xiàn)在是一家中式餐廳的老板了。在我的店里你可以享用早餐、中餐、晚餐。早餐就簡單點(diǎn),只提供包子、油條跟豆?jié){吧(是的,這個不是主業(yè))。午餐會豐盛一些,在午餐里你們選擇北方菜或是南方菜。北方菜主要有:鍋包肉、豬肉燉粉條、辣子雞、干炸里脊、糖醋里脊、板栗燒雞、地三鮮、紅燒肉、回鍋肉、 口水雞、宮保雞丁、可樂雞翅;南方菜主要有:炒米粉、南瓜餅、南燜羊肉、蒸排骨、肉片炒青椒、水果皮薩、北炒魚香茄子、糯米糍團(tuán)、芥菜煲。晚餐上可以吃得精致一些,主要有地道小吃和甜點(diǎn)。地道小吃:肉夾饃、羊肉泡饃、烏冬面、章魚小丸子、蔥油餅、老婆餅;甜點(diǎn):冰淇淋、鮮奶蛋糕還有蜜汁藕。
把上面的描述轉(zhuǎn)換成圖片菜單就像下面這樣的:
當(dāng)然,這里并不夠齊全,只是為了照顧圖片的大小(當(dāng)然也不可否認(rèn)是博主偷懶了),只繪制了其中的一部分,不過足夠我們說明問題了。
一般組合模式
模式分析
在一般組合模式里,我們只做了一件事,那就是模糊了簡單元素與復(fù)雜元素。怎么說明這一點(diǎn)呢?針對上面舉的例子來說,我們的每一種菜都是一個簡單元素,而每一種菜系(南方/北方/小說/甜點(diǎn))或是餐飲的類型(早餐/午餐/晚餐)都是一個復(fù)雜元素,因?yàn)檫@里又包含若干的簡單元素。我們把菜系定義為 Menu,而每一道具體的菜則定義成 MenuItem。這樣我們就可以繪制出一般組合模式的類圖,如下:
這里面我們的菜單(Menu)和菜單項(xiàng)(MenuItem)都繼承自一個 MenuComponent。MenuComponent 是一個抽象的類。當(dāng) Menu 和 MenuItem 繼承自同一個類時,我們就可以實(shí)現(xiàn)模糊簡單元素與復(fù)雜元素了,因?yàn)槲覀兛梢园凑仗幚?MenuComponent 的方式處理 Menu 和 MenuItem。這里我們只是模糊了這兩個的分界,卻不能真的等同看待。很簡單,上面的的 MenuItem 最起碼有一個價(jià)格的屬性、而 Menu 就不存在這個屬性;Menu 可以有一個 add MenuComponent 的方法,而 MenuItem 則不可能會有。
說到這里,可能你會說組合模式并不完美。是的,我也這么覺得。它讓這件事情模糊了,讓 MenuComponent 的使用產(chǎn)生了歧義。比如我們在使用它的時候,根本不知道它的某一個方法是可以正常使用。比如,一個 MenuComponent(可實(shí)際的類型可能是 Menu,而我們不知道),這時可能會調(diào)用它的 getPrice() 方法,這是有問題的,邏輯上是走不通的。那么,我們就必須要為 Menu 的這個方法拋出一個異常(異常是一個心機(jī) boy,我們都不太喜歡它)。當(dāng)然,也可以在外部使用 instanceof 關(guān)鍵字處理??墒?#xff0c;這樣的處理總讓我有一種非面向?qū)ο蟮奶幚磉^程,所以還是拋出異常吧。
另外,Menu 中組合了 MenuItem,從這一點(diǎn)來看,倒是有幾分“繼承-組合”的意味。
邏輯實(shí)現(xiàn)
先來看看抽象類,這個是基礎(chǔ):
MenuComponent.java
而在 Menu 的具體類中,雖然是繼承了 MenuComponent,可是它的抽象方法又不能全部重寫。原因上面也說了,這里不贅述了??墒?#xff0c;由于 Java 語法的客觀存在,所以這里我們拋出了一個異常。
Menu.java
基于上面對 Menu 類的說明,這里的 MenuItem 類的實(shí)現(xiàn)過程也是一樣:只重寫能夠重寫的部分,不能重寫的地方拋出一個異常等待上層處理。
MenuItem.java
上面的代碼是整個一般組合模式的關(guān)鍵部分,這是需要注意的是它們的 print() 方法。對于 MenuItem 的 print() 來說,是很常規(guī)的打印,而 Menu 的打印則需要作處理。因?yàn)槲覀冋5睦斫饫镆粋€菜單因?yàn)闀芏嗖藛雾?xiàng),所以,這里我們就把當(dāng)前菜單下的所以菜單打印一遍。不過,這也不是什么難事,因?yàn)樵诿糠?Menu 中都有一個 MenuItem 的列表。好了,問題解決。詳情參見上面的代碼部分。
模式小結(jié)
從上面的例子也可以看出,組合模式在解決有層級關(guān)系時,有著得天獨(dú)厚的優(yōu)勢。思路清晰、代碼優(yōu)雅。唯一的不足是我們要針對不同的情況拋出相應(yīng)的異常。
組合與迭代
對于組合模式息息相關(guān)的另一種模式——迭代模式,它在組合模式中可以說有著重要的地位。在上面的代碼中,有點(diǎn)編程邏輯的人應(yīng)該都可以發(fā)現(xiàn),它們的 print() 方法是對象內(nèi)部的操作。也就是說,如果我想要通過一個 Menu 操作一個 MenuItem 就必須在 Menu 內(nèi)部進(jìn)行實(shí)現(xiàn)。這是不現(xiàn)實(shí)的,因?yàn)樾枨笞兓乃俣?#xff0c;可能隔了幾秒連它的親媽也不認(rèn)識了。正因?yàn)槿绱?#xff0c;所以我們就必須想辦法從外部拿到 Menu 中的 MenuItem。
比如現(xiàn)在我想知道餐廳里所有的素食有哪些,如果我們不去改動原有代碼,那么就可以添加一個外部的迭代邏輯。
深搜的試水之行
或許你又會說,這里根本不需要使用迭代,用一次深搜就 OK 了。是的沒錯,而且對于一個數(shù)據(jù)結(jié)構(gòu)基本功還可以的同學(xué),可以馬上寫出一個深搜的解決方案。這里給出我的深搜方案:
public void showVegetarMenu(MenuComponent menu) {List<MenuComponent> visited = new ArrayList<>();showVegetarMenu(menu, visited);}private void showVegetarMenu(MenuComponent menu, List<MenuComponent> visited) {if (visited.contains(menu)) {return;}if (menu instanceof MenuItem) {if (menu.isVegetarian()) {System.out.println(menu);}return;}List<MenuComponent> children = ((Menu) menu).getChildren();for (int i = 0; i < children.size(); i++) {showVegetarMenu(children.get(i), visited);}}結(jié)果不出意外。能夠使用深搜,已然是逼格滿滿了。不過,使用深搜讓我有一種面向過程編程的感覺,不夠優(yōu)雅。下面就讓我用迭代器來實(shí)現(xiàn)一次華麗的逆轉(zhuǎn)吧。
迭代的逆轉(zhuǎn)
首先我們?yōu)?MenuComponent 添加一個 createIterator() 方法。就像下面這樣:
MenuComponent.java
由于這里添加的是一個抽象的方法,那么在 Menu 和 MenuItem 中就要必須重寫這個 createIterator() 方法。
Menu.java
MenuItem.java
public class MenuItem extends MenuComponent {( ... 省略重復(fù)的 N 條 ... )@Overridepublic Iterator<MenuComponent> createIterator() {return new NullIterator();} }在上面兩段代碼中提到了兩個迭代器類:CompositeIterator、NullIterator。這里有參照書本上的邏輯,不過也有改動,因?yàn)闀镜牡鳑]有通用性,下面會對這一點(diǎn)進(jìn)行說明的。
CompositeIterator.java
這里的棧結(jié)構(gòu)使用得很巧妙,因?yàn)檫@個棧的使用讓我想到在 LeetCode 上的一道算法題,也是使用棧來實(shí)現(xiàn),而且比一般的算法復(fù)雜度低很多,如果我不犯懶的話,應(yīng)該會寫那一篇博客的??瓤?#xff0c;扯遠(yuǎn)了,回到正題。有關(guān)于棧的使用是一些數(shù)據(jù)結(jié)構(gòu)和 Java api 的基礎(chǔ),這里不多說什么了。還有這里的 hasNext() 和 next() 方法,這里要求你對數(shù)據(jù)結(jié)構(gòu)和 Java api(主要是 Stack 這一塊)比較熟悉。所以,如果你看到這個地方有什么不太理解的,可以留言,也可以自行復(fù)習(xí)一下這兩塊內(nèi)容。
NullIterator.java
public class NullIterator implements Iterator<MenuComponent> {@Overridepublic boolean hasNext() {return false;}@Overridepublic MenuComponent next() {return null;}@Overridepublic void remove() {throw new UnsupportedOperationException();} }因?yàn)槊總€菜單項(xiàng)都不可能什么子菜單項(xiàng),也就不存在什么迭代器了,所以在 MenuItem 中就可以返回一個 Null 的迭代器。當(dāng)然,這是理想的做法。你也可以直接返回 null,只是這樣一來,在上層就要多一次判空處理,相比較而言,這樣的實(shí)現(xiàn)更優(yōu)雅。
程序的結(jié)果自然不出所料:
只是,如果你只采用書本上的迭代器來實(shí)現(xiàn),就會出現(xiàn)多級菜單下的菜單項(xiàng)被 show 了 N 遍。而你只能一臉懵逼。
總結(jié)
以上是生活随笔為你收集整理的Java 设计模式——组合模式的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Istio熔断器解析
- 下一篇: oracle-SCN headroom