將有階層關係的類別狀態給扁平化,給使用者看到的是一致性的關係
若沒有合成模式會怎樣?
假如現在我們來到一間自助吧,有一個菜單類別,但這個菜單類別裡面還會有甜點這個子菜單
大概長下面這樣
於是我們大概會這樣實作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70
| class BuffetMenu { String name; ArrayList<String> items; DessertMenu dessertMenu;
BuffetMenu() { this.name = "自助餐菜單"; this.items = new ArrayList<>(); this.dessertMenu = new DessertMenu(); this.init(); }
private void init() { this.items.add("牛肉麵"); this.items.add("海鮮飯"); this.items.add("牛排"); }
public void setMenuItem(String item) { this.items.add(item); }
public void showMenuItem() { for(String item: items) { System.out.println(this.name + ":" + item); } }
public void print() { System.out.println("開始介紹" + this.name); showMenuItem(); } }
class DessertMenu { String name; ArrayList<String> items; ChocolateMenu chocolateMenu;
DessertMenu() { this.name = "甜點菜單"; this.items = new ArrayList<>(); this.init(); }
private void init() { this.items.add("雞蛋餅乾"); this.items.add("巧克力"); this.items.add("棉花糖"); }
public void setMenuItem(String item) { this.items.add(item); }
public void showMenuItem() { for(String item: items) { System.out.println(this.name + ":" + item); } }
public void print() { System.out.println("開始介紹" + this.name); showMenuItem(); } }
|
可以看到DessertMenu類別包含在BuffetMenu類別了
我們現在有位服務生提供介紹菜單內容,定義如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| class Waitress { BuffetMenu buffetMenu;
Waitress(BuffetMenu buffetMenu) { this.buffetMenu = buffetMenu; }
public void introduceMenu() { this.buffetMenu.print(); this.buffetMenu.showMenuItem(); this.buffetMenu.dessertMenu.print(); this.buffetMenu.dessertMenu.showMenuItem(); } }
|
然後我們請服務生開始介紹菜單
1 2 3 4 5 6 7 8 9
| public class main { public static void main(String[] args) { BuffetMenu buffetMenu = new BuffetMenu(); Waitress waitress = new Waitress(buffetMenu);
waitress.introduceMenu(); } }
|
1 2 3 4 5 6 7 8
| 開始介紹自助餐菜單 自助餐菜單:牛肉麵 自助餐菜單:海鮮飯 自助餐菜單:牛排 開始介紹甜點菜單 甜點菜單:雞蛋餅乾 甜點菜單:棉花糖 甜點菜單:巧克力
|
但假如現在變成這樣呢:甜點內的巧克力又有不同類型的巧克力,那我們又要再修改上面的程式碼,將Chocolate類別加入到Dessert類別內
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64
| class DessertMenu { String name; ArrayList<String> items; ChocolateMenu chocolateMenu;
DessertMenu() { this.chocolateMenu = new ChocolateMenu(); this.name = "甜點菜單"; this.items = new ArrayList<>(); this.init(); }
private void init() { this.items.add("雞蛋餅乾"); this.items.add("棉花糖"); }
public void setMenuItem(String item) { this.items.add(item); }
public void showMenuItem() { for(String item: items) { System.out.println(this.name + ":" + item); } }
public void print() { System.out.println("開始介紹" + this.name); } }
class ChocolateMenu { String name; ArrayList<String> items;
ChocolateMenu() { this.name = "巧克力菜單"; items = new ArrayList<>(); this.init(); }
private void init() { this.items.add("白巧克力"); this.items.add("黑巧克力"); this.items.add("酒釀巧克力"); }
public void setMenuItem(String item) { this.items.add(item); }
public void showMenuItem() { for(String item: items) { System.out.println(this.name + ":" + item); } }
public void print() { System.out.println("開始介紹" + this.name); } }
|
這時要修改服務生的腦袋,使其知道巧克力菜單的內容。。。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| class Waitress { BuffetMenu buffetMenu;
Waitress(BuffetMenu buffetMenu) { this.buffetMenu = buffetMenu; }
public void introduceMenu() { this.buffetMenu.print(); this.buffetMenu.dessertMenu.print(); this.buffetMenu.dessertMenu.chocolateMenu.print(); } }
|
但這時候就會發現,每次若新增一個子菜單時,呼叫者就需要直接對其操作,才能夠印出該子菜單的項目列表
一但子菜單內又有子菜單,那整個程式碼的維護的代價就會非常的高
1 2 3
| 可以看到子菜單內若又有子菜單,要印出該名稱的話,那這行程式碼會越來越長。。。
this.buffetMenu.dessertMenu.chocolateMenu.print();
|
維護修改的代價
舉個例子,我們想要叫服務生說明,哪些菜/甜點是素食的,這時會改code改到想哭
解決服務生不用笨拙地存取菜單與其子菜單們:合成模式登場
合成模式主要幫我們建立一個類別關係,建立起父類別與子類別的階層關係,並讓客戶端以一致的方式存取樹狀結構內的各個類別
先講結論,以上的菜單們,套用合成模式後,服務生不用透過以下方式存取子菜單並介紹出來了,而是直接可以用一致的方式將所有菜單與子菜單都呼叫出來
1 2 3 4 5 6 7 8 9 10 11
| public void introduceMenu() { public void introduceMenu() { this.buffetMenu.print(); this.buffetMenu.showMenuItem(); this.buffetMenu.dessertMenu.print(); this.buffetMenu.dessertMenu.showMenuItem(); this.buffetMenu.dessertMenu.chocolateMenu.print(); this.buffetMenu.dessertMenu.chocolateMenu.showMenuItem(); }
|
這時我們先來看 合成模式 的類別關係圖
- Component類別:定義了其所有底下葉節點與合成類別的結構
- 所有的Leaf節點與Composite節點都會繼承(實作)該Component類別
- Component是老大哥,老大哥會的下面的也會學到
- Leaf類別:會實作Component類別,但由於Leaf節點沒有子類別,所以只會有operation()的方法供呼叫
- Composite類別:合成類別,將以下兩點做合成
- 所要執行的功能
operation()
- 管理子類別的方法
add()
remove()
getChild()
對應Composite類別關係圖到我們上述所畫的菜單關係圖,就會長得如下:
把上面所提到的這張圖作轉換
就會變成這樣子 (省略Chocolate類別的菜單項目)
定義MenuComponent, 提供菜單基本的方法
這邊主要不是定義成Interface, 而是抽象類別的主要原因是因為,我們會給 子菜單去繼承菜單所具有的方法
但又因會給菜單項目(MenuItem)繼承(如 牛肉麵, 海鮮飯, 牛排),
但菜單項目不需要像是 add()
, remove()
等方法,所以會在該方法內先拋出 UnsupportedOperationException()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| abstract class MenuComponent { void add(MenuComponent menuComponent) { throw new UnsupportedOperationException(); }
void remove(MenuComponent menuComponent) { throw new UnsupportedOperationException(); }
MenuComponent getChild(int i) { throw new UnsupportedOperationException(); }
String getName() { throw new UnsupportedOperationException(); }
String getDescription() { throw new UnsupportedOperationException(); }
double getPrice() { throw new UnsupportedOperationException(); }
boolean isVegetarian() { throw new UnsupportedOperationException(); }
void print() { throw new UnsupportedOperationException(); } }
|
可以看到我們定義了以下方法,並等待真正的菜單類別來實作其細節
- add(): 可新增 實作MenuComponent的物件
- remove(): 移除 實作MenuComponent的物件
- getChild(): 取得Menu內的菜單項目/子菜單
- getName(): 取得菜單名稱
- getPrice: 取得菜單項目價格
- isVegetarian(): 確認是否為素食
- print(): 印出自己的所有菜單項目/子菜單內容
1 2 3 4 5
| 還記得最上面的若要請服務生確認菜色是否是素食,但要到處修改的慘痛的經驗嗎?
這時候若在共通的介面,定義好一個isVegetarian(), 請菜色自行提供是否是素食,是的話回傳true, 不是就回傳false, 如此一來服務生就可以在呼叫print()時,可以直接判斷這道項目是否是素食囉
關於print()的實現細節後面會介紹
|
開始實作菜單元件:定義自助式菜單
接著開始實作自助餐的完整菜單吧!
BuffetMenu.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55
| class BuffetMenu extends MenuComponent { ArrayList<MenuComponent> menuItems = new ArrayList<>(); String name;
BuffetMenu() { this.name = "自助式菜單"; }
@Override void add(MenuComponent menuComponent) { this.menuItems.add(menuComponent); }
@Override void remove(MenuComponent menuComponent) { this.menuItems.remove(menuComponent); }
@Override MenuComponent getChild(int i) { return this.menuItems.get(i); }
@Override String getName() { return this.name; }
@Override String getDescription() { return "自助式菜單提供了主菜們(牛肉麵, 海鮮飯, 牛排), 以及甜點菜單, 請盡情享用!"; }
@Override void print() { System.out.println(this.getName()); System.out.println(this.getDescription());
Iterator iterator = menuItems.iterator();
while(iterator.hasNext()) { MenuComponent menuComponent = (MenuComponent)iterator.next(); menuComponent.print(); } } }
|
可以看到在BuffetMenu中,已經除了 getPrice()
和isVegetarian()
這兩項菜單不用去實作,其他都有定義邏輯
這邊要注意的是 print()
我們使用 iterator, 將菜單內的MenuComponent
一一取出來,並且在呼叫其print()
, 依序對自助式菜單內的菜單項目做介紹這樣
定義菜單項目
接著我們定義一個牛肉麵類別 BeefNoodles
, 實作 MenuComponent
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| class BeefNoodles extends MenuComponent { String name;
@Override String getName() { return this.name; }
@Override String getDescription() { return "好吃的牛肉麵, 道地的四川口味~~"; }
@Override double getPrice() { return 120; }
@Override boolean isVegetarian() { return false; }
@Override void print() { System.out.println(this.getName()); System.out.println(this.getDescription()); System.out.println(this.getPrice()); } }
|
由於牛肉麵是菜單的項目,所以只要實作跟他自己有關的方法就好,在print()
也只要就印出自己名稱是什麼,以及描述, 價格。
實作子菜單: 甜點菜單
跟剛剛實作自助餐的細節非常類似
DessertMenu.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54
| class DessertMenu extends MenuComponent { ArrayList<MenuComponent> menuItems = new ArrayList<>(); String name;
DessertMenu() { this.name = "甜點菜單"; }
@Override void add(MenuComponent menuComponent) { this.menuItems.add(menuComponent); }
@Override void remove(MenuComponent menuComponent) { this.menuItems.remove(menuComponent); }
@Override MenuComponent getChild(int i) { return this.menuItems.get(i); }
@Override String getName() { return this.name; }
@Override String getDescription() { return "甜點菜單提供了一星米其林主廚所製作的甜點們!"; }
@Override void print() { System.out.println(this.getName()); System.out.println(this.getDescription());
Iterator iterator = menuItems.iterator();
while(iterator.hasNext()) { MenuComponent menuComponent = (MenuComponent)iterator.next(); menuComponent.print(); } } }
|
最後也是一樣,將甜點項目都實作一輪,不過這邊都很類似就不敘述了
重新定義服務生
可以看到服務生很快樂, 只要呼叫MenuComponent的print()
, 就把所有菜單都介紹出來囉
1 2 3 4 5 6 7 8 9 10 11 12
| class Waitress { MenuComponent menuComponent;
Waitress(MenuComponent menuComponent) { this.menuComponent = menuComponent; }
public void introduceMenu() { this.menuComponent.print(); } }
|
客戶開始進來,請服務生介紹
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| public class main { public static void main(String[] args) { MenuComponent buffetMenu = new BuffetMenu(); MenuComponent beefNoodles = new BeefNoodles(); MenuComponent seaRice = new SeaRice(); MenuComponent steak = new Steak(); buffetMenu.add(beefNoodles); buffetMenu.add(seaRice); buffetMenu.add(steak); MenuComponent dessertMenu = new DessertMenu(); MenuComponent eggCookie = new EggCookie(); MenuComponent cottonCandy = new CottonCandy(); dessertMenu.add(eggCookie); dessertMenu.add(cottonCandy); buffetMenu.add(dessertMenu);
Waitress waitress = new Waitress(buffetMenu); waitress.introduceMenu(); } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| 自助式菜單 自助式菜單提供了主菜們(牛肉麵, 海鮮飯, 牛排), 以及甜點菜單, 請盡情享用! 牛肉麵 好吃的牛肉麵, 道地的四川口味~~ 120.0 海鮮飯 好吃的海鮮飯~~ 200.0 牛排 好吃的牛排~~ 400.0 甜點菜單 甜點菜單提供了一星米其林主廚所製作的甜點們! 雞蛋餅乾 雞蛋口味的餅單, 大人小孩都愛~~ 60.0 棉花糖 棉花糖好滋味~~ 30.0
|
小結
透過合成模式, 將取得菜單與子菜單的動作給一致化了
服務生只要呼叫實作MenuComponent的菜單與子菜單們, 就可以一口氣將所有菜單與子菜單的項目們全都介紹出來!
未來若要擴充子菜單的項目,也不用擔心需要再修改服務生的類別,只要新的菜單有實作MenuComponent就好
達到 "對修改關閉, 對擴充開放"的原則!
將每個菜單的Iterator給合併起來: IteratorComposite
還記得剛剛有提到如何判斷該菜單是否是素食的情況呢??
(待續)