11 CompositePattern(組合模式)
前言:幫助客戶用同樣的方法遍歷不同的集合。
需求:
上次Vander在Panda的幫助下,很好地解決了廚師們的菜單使用不同的集合,解決了不同集合的遍歷問題,但是市場競爭真的太激烈了,Vande在九方購物廣場的Vander奶茶店也迎來了經(jīng)營問題,所以現(xiàn)在需要將奶茶店的菜單加入到燒烤菜單中,但是添加會有什么問題呢。我們來看看現(xiàn)在的結(jié)構(gòu):

很明顯,類型不匹配,無法直接將奶茶菜單加入到Barbecue的子菜單中,別無它法,只能請所有的廚師重新設(shè)計他們的菜單,來到這里很容易就會想到樹形結(jié)構(gòu),需要這種樹形結(jié)構(gòu)能夠容納菜單、子菜單和菜單項,并且需要能夠在每個菜單的各項間游走,需要遍歷整個菜單,包括子菜單也要遍歷。形成的樹如下所示:

正當(dāng)Vander毫無頭緒,不知道這個代碼要怎么寫的時候,Panda大師又來了,這個可以用“組合模式”解決.
下面由我Panda來說明一下組合模式。
組合模式:允許你將對象組合成樹形結(jié)構(gòu)來變現(xiàn)“整體/部分”層次結(jié)構(gòu),組合能讓客戶以一致的方式處理個別對象以及對象組合。一旦有了豐富的大菜單,我們就可以使用這個模式來“統(tǒng)一處理個別對象和組合對象”。意味著有一個樹形結(jié)構(gòu)的菜單(Menu)、子菜單和可能還帶有菜單項的子菜單,任何一個菜單都是一種“組合”,因為它既可以包括其他菜單,也可以包括菜單項(MenuItem)。個別對象只是菜單項——并未持有其他對象。菜單就是“節(jié)點”,而菜單項就是“葉子節(jié)點”。
組合模式讓我們能用樹形方式創(chuàng)建對象的結(jié)構(gòu),樹里面包含了組合以及個別的對象,使用組合結(jié)構(gòu),我們能把相同的操作應(yīng)用在組合和個別對象上。換句話說,就是在大多數(shù)情況下,我們可以忽略對象組合和個別對象之間的差別。用一個淺顯一點的例子把:就是一個print語句,可以print(葉子節(jié)點)、print(根)、print(子菜單)、print(菜單),就是說可以忽略它在樹里面的位置。
說了那么多,Vander還是懵逼的,接著Panda繼續(xù)講解一下組合模式的一般套路,如下圖:

解釋一下以上的圖,葉子跟組合都是繼承于組件,客戶端代碼主要是跟組件打交道,葉子就是菜單中的每個菜單項,組合指的就是菜單(BarbecueMenu、PizzaMenu等)。使用組合迭代器,就是為了可以在遍歷的時候忽略它是菜單還是菜單項,也就是說不管是葉子還是組合對客戶端代碼來說都是透明的。
這么說可能有點抽象,直接使用吧,用組合模式來重新寫菜單:

這種方式比較簡單,相當(dāng)于Menu跟MenuItem將迭代器實現(xiàn)轉(zhuǎn)移到了內(nèi)部,也就是說遍歷什么的操作都在Menu跟MenuItem自身來完成。
Menu:
public class Menu extends MenuComponent {
ArrayList<MenuComponent> menuComponents = new ArrayList<MenuComponent>();
private String name;
private String desc;
public Menu(String name, String desc) {
this.name = name;
this.desc = desc;
}
public void add(MenuComponent menuComponent) {
menuComponents.add(menuComponent);
}
public void remove(MenuComponent menuComponent) {
menuComponents.remove(menuComponent);
}
public MenuComponent getChild(int i) {
return menuComponents.get(i);
}
public String getName() {
return name;
}
public String getDesc() {
return desc;
}
public void print() {
System.out.println("\nname:" + getName() + "#desc:" + getDesc());
System.out.println("----------------------------------------");
Iterator<MenuComponent> iterator = menuComponents.iterator();
while(iterator.hasNext()) {
MenuComponent menuComponent = iterator.next();
menuComponent.print();
}
}
}
MenuItem:
public class Menu extends MenuComponent {
ArrayList<MenuComponent> menuComponents = new ArrayList<MenuComponent>();
private String name;
private String desc;
public Menu(String name, String desc) {
this.name = name;
this.desc = desc;
}
public void add(MenuComponent menuComponent) {
menuComponents.add(menuComponent);
}
public void remove(MenuComponent menuComponent) {
menuComponents.remove(menuComponent);
}
public MenuComponent getChild(int i) {
return menuComponents.get(i);
}
public String getName() {
return name;
}
public String getDesc() {
return desc;
}
public void print() {
System.out.println("\nname:" + getName() + "#desc:" + getDesc());
System.out.println("----------------------------------------");
Iterator<MenuComponent> iterator = menuComponents.iterator();
while(iterator.hasNext()) {
MenuComponent menuComponent = iterator.next();
menuComponent.print();
}
}
}
MenuComponent:
public abstract class MenuComponent {
public void add(MenuComponent menuComponent) {
throw new UnsupportedOperationException();
}
public void remove(MenuComponent menuComponent) {
throw new UnsupportedOperationException();
}
public MenuComponent getChild(int i) {
throw new UnsupportedOperationException();
}
public String getName() {
throw new UnsupportedOperationException();
}
public String getDesc() {
throw new UnsupportedOperationException();
}
public double getPrice() {
throw new UnsupportedOperationException();
}
public void print() {
throw new UnsupportedOperationException();
}
}
MenuAdmin:
public class MenuAdmin {
private MenuComponent menuComponent;
public MenuAdmin(MenuComponent menuComponent) {
super();
this.menuComponent = menuComponent;
}
public void print() {
menuComponent.print();
}
}
Main:
public class Main {
public static void main(String args[]) {
Menu totalMenu = new Menu("TotalMenu", "TotalMenu");
Menu pizzaMenu = new Menu("PizzaMenu", "PizzaMenu");
Menu barbecueMenu = new Menu("barbecueMenu", "barbecueMenu");
Menu milkTeaMenu = new Menu("milkTeaMenu", "milkTeaMenu");
Menu hotPotMenu = new Menu("hotPotMenu", "hotPotMenu");
pizzaMenu.add(new MenuItem("FruitPizza", "Hawaii Style", 38.0));
pizzaMenu.add(new MenuItem("BuffPizza", "American Style", 28.0));
pizzaMenu.add(new MenuItem("TunaPizza", "Japan Style", 18.0));
milkTeaMenu.add(new MenuItem("Bubble milk tea", "big cup", 10.0));
milkTeaMenu.add(new MenuItem("Coconut milk tea", "middle cup", 8.0));
milkTeaMenu.add(new MenuItem("cheese tea", "middle cup", 18.0));
hotPotMenu.add(new MenuItem("chicken", "with solt", 10));
hotPotMenu.add(new MenuItem("tofu", "with solt", 5));
hotPotMenu.add(new MenuItem("lettuce", "with solt", 10));
barbecueMenu.add(new MenuItem("corn", "with pepper", 10));
barbecueMenu.add(new MenuItem("chicken", "with pepper", 10));
barbecueMenu.add(new MenuItem("fragrant-flowered garlic", "with pepper", 10));
barbecueMenu.add(milkTeaMenu);
totalMenu.add(hotPotMenu);
totalMenu.add(barbecueMenu);
totalMenu.add(pizzaMenu);
totalMenu.print();
}
}
MenuComponent:
public abstract class MenuComponent {
public void add(MenuComponent menuComponent) {
throw new UnsupportedOperationException();
}
public void remove(MenuComponent menuComponent) {
throw new UnsupportedOperationException();
}
public MenuComponent getChild(int i) {
throw new UnsupportedOperationException();
}
public String getName() {
throw new UnsupportedOperationException();
}
public String getDesc() {
throw new UnsupportedOperationException();
}
public double getPrice() {
throw new UnsupportedOperationException();
}
public void print() {
throw new UnsupportedOperationException();
}
}
實現(xiàn)效果:

第一種方式是不是違反了單一責(zé)任原則,想想看Menu里面是不是使用了迭代器來進(jìn)行菜單的遍歷操作,第一種方式用單一責(zé)任原則來換取了透明性(通過讓組件的接口同時包含一些管理子節(jié)點和葉節(jié)點的操作,客戶可以將組合和葉節(jié)點一視同仁。)
接下來,繼續(xù)說第二種方式。第二種方式是把遍歷的操作實現(xiàn)到了外部,也就是用外部迭代器來完成,我們先來看看圖:

這個是當(dāng)前菜單的樹狀圖,有點復(fù)雜,我們簡化一下,假設(shè)主菜單就只有一個BarbecueMenu:

下面我們用圖示的方法,大概畫一下整個遍歷的過程:

相當(dāng)于將BarbecueMenuIterator(普通的list迭代器),遍歷到MilkteaMenu的時候,這個時候調(diào)用next()獲取到的就是MilkteaMenu這個組合,然后由于它里面還有很多葉子,所以將這個組合放入堆棧中,再次next的時候,獲取到這個組合迭代器的普通迭代器,再繼續(xù)遍歷,最后這個遍歷完了,就將MenuteaMenuIterator從堆棧中移除,然后MilkteaCompositeIterator也移出,然后發(fā)現(xiàn)BarbecueMenuIterator也遍歷完了,也移除,最后整個堆棧就空了,就算遍歷完畢了,這里的位置信息不像之前的普通迭代器那樣用pos來標(biāo)記了,這里直接用棧來實現(xiàn)位置信息的存儲。
下面我們上代碼:(代碼看上去不多,但是確實不好理解,請仔細(xì)體會遞歸的味道)
MenuComponent:(與第一種方式一致)
public class MenuItem extends MenuComponent {
private String name;
private String desc;
private double price;
public MenuItem(String name, String desc, double price) {
super();
this.name = name;
this.desc = desc;
this.price = price;
}
public String getName() {
return name;
}
public String getDesc() {
return desc;
}
public double getPrice() {
return price;
}
public void print() {
System.out.println("name:" + getName() + "#desc:" + getDesc() + "#price:" + getPrice());
}
@Override
public Iterator<MenuComponent> createIteractor() {
return new NullIterator();
}
}
Menu:
public class Menu extends MenuComponent {
ArrayList<MenuComponent> menuComponents = new ArrayList<MenuComponent>();
private String name;
private String desc;
public Menu(String name, String desc) {
this.name = name;
this.desc = desc;
}
public void add(MenuComponent menuComponent) {
menuComponents.add(menuComponent);
}
public void remove(MenuComponent menuComponent) {
menuComponents.remove(menuComponent);
}
public MenuComponent getChild(int i) {
return menuComponents.get(i);
}
public String getName() {
return name;
}
public String getDesc() {
return desc;
}
public void print() {
System.out.println("name:" + getName() + "#desc:" + getDesc());
}
@Override
public Iterator<MenuComponent> createIteractor() {
return new CompositeIterator(menuComponents.iterator());
}
}
NullIterator:
public class NullIterator implements Iterator<MenuComponent> {
public boolean hasNext() {
return false;
}
public MenuComponent next() {
return null;
}
}
ComponentIterator:(重中之中)
public class CompositeIterator implements Iterator<MenuComponent> {
private Stack<Iterator<MenuComponent>> stack = new Stack<Iterator<MenuComponent>>();
public CompositeIterator(Iterator<MenuComponent> iterator) {
stack.push(iterator);
}
public boolean hasNext() {
if(stack.empty()) {
return false;
} else {
Iterator<MenuComponent> iterator = stack.peek();
if(!iterator.hasNext()) {
stack.pop();
return hasNext();
} else {
return true;
}
}
}
public MenuComponent next() {
if(hasNext()) {
Iterator<MenuComponent> itemIterator = stack.peek();
MenuComponent component = itemIterator.next();
if(component instanceof Menu) {
stack.push(component.createIteractor());
}
return component;
}
return null;
}
}
MenuAdmin:
public class MenuAdmin {
private MenuComponent menuComponent;
public MenuAdmin(MenuComponent menuComponent) {
super();
this.menuComponent = menuComponent;
}
public void print() {
Iterator<MenuComponent> iterator =menuComponent.createIteractor();
while(iterator.hasNext()) {
MenuComponent menuComponent = iterator.next();
menuComponent.print();
}
}
}
Main:
public class Main {
public static void main(String args[]) {
Menu totalMenu = new Menu("TotalMenu", "TotalMenu");
Menu pizzaMenu = new Menu("PizzaMenu", "PizzaMenu");
Menu barbecueMenu = new Menu("barbecueMenu", "barbecueMenu");
Menu milkTeaMenu = new Menu("milkTeaMenu", "milkTeaMenu");
Menu hotPotMenu = new Menu("hotPotMenu", "hotPotMenu");
pizzaMenu.add(new MenuItem("FruitPizza", "Hawaii Style", 38.0));
pizzaMenu.add(new MenuItem("BuffPizza", "American Style", 28.0));
pizzaMenu.add(new MenuItem("TunaPizza", "Japan Style", 18.0));
milkTeaMenu.add(new MenuItem("Bubble milk tea", "big cup", 10.0));
milkTeaMenu.add(new MenuItem("Coconut milk tea", "middle cup", 8.0));
milkTeaMenu.add(new MenuItem("cheese tea", "middle cup", 18.0));
hotPotMenu.add(new MenuItem("chicken", "with solt", 10));
hotPotMenu.add(new MenuItem("tofu", "with solt", 5));
hotPotMenu.add(new MenuItem("lettuce", "with solt", 10));
barbecueMenu.add(new MenuItem("corn", "with pepper", 10));
barbecueMenu.add(new MenuItem("chicken", "with pepper", 10));
barbecueMenu.add(new MenuItem("fragrant-flowered garlic", "with pepper", 10));
barbecueMenu.add(milkTeaMenu);
totalMenu.add(hotPotMenu);
totalMenu.add(barbecueMenu);
totalMenu.add(pizzaMenu);
MenuAdmin menuAdmin = new MenuAdmin(totalMenu);
menuAdmin.print();
}
}
實現(xiàn)效果:

這里有個小細(xì)節(jié)說明一下,MenuItem使用了NullIterator,由于菜單項已經(jīng)是子節(jié)點了,沒有什么好遍歷的了,這里如果不用NullIterator,也可以直接返回null,如果直接返回null的話,就需要加入條件判斷。但是如果用了NullIterator,就相當(dāng)于創(chuàng)建了一個沒有作用的迭代器。這個迭代器的hasNext()永遠(yuǎn)都是false,這樣操作起來更具有統(tǒng)一性。
以上的代碼最主要就是CompositeIterator的部分,需要自己好好理解一番。
下面我們先來總結(jié)一下我們學(xué)過的一些模式:
| 模式 | 敘述 |
|---|---|
| 策略 | 封裝可互換的行為,并使用委托決定用哪個行為。 |
| 適配器 | 改變一個或多個類的接口 |
| 迭代器 | 提供一個方式遍歷集合,無需暴露集合的實現(xiàn) |
| 外觀 | 簡化一群類的接口 |
| 組合 | 客戶可以將對象的集合以及個別對象一視同仁 |
| 觀察者 | 當(dāng)某個狀態(tài)發(fā)生變化的時候,允許一群對象被通知到。 |
最后又到了喜聞樂見的總結(jié)部分,我們又來總結(jié)我們現(xiàn)在現(xiàn)有的設(shè)計模式武器。
面向?qū)ο蠡A(chǔ)
抽象、封裝、多態(tài)、繼承
九大設(shè)計原則
設(shè)計原則一:封裝變化
設(shè)計原則二:針對接口編程,不針對實現(xiàn)編程
設(shè)計原則三:多用組合,少用繼承
設(shè)計原則四:為交互對象之間的松耦合設(shè)計而努力
設(shè)計原則五:對擴(kuò)展開放,對修改關(guān)閉
設(shè)計原則六:依賴抽象,不要依賴于具體的類
設(shè)計原則七:只和你的密友談話
設(shè)計原則八:別找我,我有需要會找你
設(shè)計原則九:類應(yīng)該只有一個改變的理由
模式
組合模式:允許你將對象組成樹形結(jié)構(gòu)來表現(xiàn)整體/部分的層次結(jié)構(gòu)。組合能讓客戶以一致的方式處理個別對象和對象組合。