延續之前反覆器模式的菜單例子,雖然已經簡化了程式碼,但是 Waitress 裡還是要呼叫 printMenu() 多次,看起來滿醜的,要是未來有新菜單加入,Waitress 程式碼勢必要修改,這算不算是「違反開放關閉守則」?是否有什麼方式可以將菜單合併,或是只傳給 Waitress 一個反覆器,而此反覆器可以在所有菜單間遊走呢?
最簡單的改法,就是把菜單全包進一個 ArrayList,然後取得 ArrayList 的反覆器,這樣Waitress 有再多的菜單也不怕了:
最簡單的改法,就是把菜單全包進一個 ArrayList,然後取得 ArrayList 的反覆器,這樣Waitress 有再多的菜單也不怕了:
public class Waitress {
private ArrayList<Menu> mMenus;
public Waitress(ArrayList<Menu> menus)
{
this.mMenus = menus;
}
public void printMenu()
{
Iterator menuIterator = menus.iterator();
while(menuIterator.hasNext())
{
Menu menu = (Menu) menuIterator.next();
printMenu(menu.createIterator());
}
}
public void printMenu(Iterator iterator)
{
while(iterator.hasNext())
{
MenuItem menuItem = (MenuItem)iterator.next();
System.out.print(menuItem.getName() + ", ");
System.out.println(menuItem.getPrice());
}
}
}
看起來好像很不錯,但其實有一個大問題,假如午餐菜單裡希望有一個「甜點」的副菜單時要怎麼辦呢?依現有的設計,我們無法支援菜單中的菜單,因此我們勢必要重新設計我們的程式了。在新設計中,我們需要達到以下功能:- 需要某種樹狀結構,可以容納菜單、副菜單、及菜單項目。
- 需要能在每個菜單的各個項目遊走,像反覆器一樣方便。
- 能夠彈性在菜單項目間遊走,如只要在甜點菜單內遊走。
合成模式允許你將物件合成樹狀結構,呈現「部份 / 整體」的階層關係。合成能讓客戶程式碼以一致的方式處理個別物件,以及合成的物件。
Compose objects into tree structures to represent part-whole hierarchies. Composite lets clients treat individual objects and compositions of objects uniformly.
Compose objects into tree structures to represent part-whole hierarchies. Composite lets clients treat individual objects and compositions of objects uniformly.
我們可以用目前的菜單問題來思考此設計模式。此模式能夠建立一個樹狀結構,處理巢狀菜單及菜單項目。菜單及菜單項目放在相同的結構中,就建立了「部份 / 整體」的階層關係,也就是可以把整個菜單視為一個大整體。
一旦有了這個整體的大菜單,就可以採用「一致的方式處理個別的物件及合成的物件」,這個意思是我們有了這個樹狀結構的菜單 (菜單可以包含菜單或項目),任一個菜單都是一種「合成」,因為菜單可包含菜單及菜單項目。而個別物件就是菜單項目,並未持有其他物件。而因為可以用一致的方式處理個別和合成的物件,在大多數情形下,可以忽略合成及個別物件之間的差異。
從類別圖中可看到,客戶使用 Component 介面處理合成中的物件。 Component 是定義樹狀結構中的物件所要具備的一切,不管合成或各別物件的節點都有的行為。Leaf 就是個別物件,也繼承了 Component 裡的行為,有些行為對 Leaf 可能沒有意義。Composite 主要是要定義具有子節點,且和 Leaf 一樣,Component 的一些行為可能對 Composite 沒有意義。介紹完合成模式後,就來開始改寫程式吧:
// 所有元件都要實作的介面。
// 因為有些方法對個別元件有意義,
// 有些對合成元件有意義,
// 我們就提供預設的實作,
// 次類別不想使用的方法就可以使用預設行為
public 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();
}
}
// 一定要繼承 MenuComponent, 因為這是共通的介面
public class MenuItem extends MenuComponent {
private String mName;
private String mDescription;
private boolean mIsVegetarian;
private double mPrice;
public MenuItem(String name, String description,
boolean vegetarian, double price)
{
this.mName = name;
this.mDescription = description;
this.mIsVegetarian = vegetarian;
this.mPrice = price;
}
// 省略一些回傳數值的方法…
@Override
public void print()
{
System.out.print(" " + getName());
if(isVegetarian())
{
System.out.print("(v)");
}
System.out.println(" " + getPrice());
System.out.println(" --" + getDescription());
}
}
// Menu 跟 MenuItem 一樣, 都是 MenuComponent
public class Menu extends MenuComponent {
// Menu 可以有任意數目的子類別,
// 使用 MenuComponent 就可以同時記錄菜單跟菜單項目
private ArrayList<MenuComponent> mMenuComponents;
private String mName;
private String mDescription;
// 現在給每個菜單一個名字,
// 之前是每個菜單類別就是此菜單的名字
public menu(String name, String description)
{
mMenuComponents = new ArrayList<MenuComponentt>();
this.mName = name;
this.mDescription = description;
}
@Override
public void add(MenuComponent menuComponent)
{
mMenuComponents.add(menuComponent);
}
@Override
public void remove(MenuComponent menuComponent)
{
mMenuComponents.remove(menuComponent);
}
@Override
public void getChild(int i)
{
return (MenuComponent) mMenuComponents.get(i);
}
// 省略一些方法...
@Override
public void print()
{
System.out.print("\n" + getName());
System.out.println(", " + getDescription());
System.out.println("-----------------");
// 因為菜單是一個合成物件,
// 且菜單跟菜單項目都有實做 print(),
// 所以這樣做可以完整列出菜單及其子菜單的所有項目。
// 在反覆期間,如果遇到另一個菜單物件,
// 呼叫其 print() 會造成另一個反覆
Iterator iterator = mMenuComponents.iterator();
while(iterator.hasNext())
{
MenuComponent menuComponent =
(MenuComponent)iterator.next();
menuComponent.print();
}
}
}
public class Waitress {
private MenuComponent mAllMenus;
// 現在 Waitress 程式碼變得很簡單
// 我們只需將最上層的菜單傳給 Waitress 就可以
public Waitress(MenuComponent allMenus)
{
this.mAllMenus = allMenus;
}
public void printMenu()
{
// 現在只要呼叫一次 print()
// 就可以印出所有菜單層級的所有項目
mAllMenus.print();
}
}
看到這邊,可能有人有疑問,一開始的說法是「一個類別,一個責任」,但現在卻有一個設計模式,可以讓類別管理階層,又要進行菜單的操作?這其實是一種取捨,合成模式以單一責任的設計守則,換取透明性 (transparency)。讓元件介面同時包含子節點 (菜單)及葉節點 (菜單項目) 的操作,這樣客戶就能將子葉節點視為同一個物件,一個元素是子節點還是葉節點,客戶不用知道。
上述的例子,MenuComponent 有兩種操作方式,失去一些安全性,因為客戶有機會做無意義的操作,我們當然也能換一個設計,把責任切開分成不同介面,雖然設計上較為安全,但客戶就需要多一些條件判斷來處理不同的節點。
最後來看一下合成模式加上反覆器的威力吧。其實前面的例子已經在 print() 裡面使用過反覆器,除此之外,也能使用反覆器走訪整個合成內部。比方說,可以走訪整個菜單項目,挑出素食項目。首先先在共同介面 MenuComponent 多加一個方法 createIterator(),接下來就都看程式碼吧。
最後來看一下合成模式加上反覆器的威力吧。其實前面的例子已經在 print() 裡面使用過反覆器,除此之外,也能使用反覆器走訪整個合成內部。比方說,可以走訪整個菜單項目,挑出素食項目。首先先在共同介面 MenuComponent 多加一個方法 createIterator(),接下來就都看程式碼吧。
public class Menu extends MenuComponent {
// 其他部份程式碼不用改
@Override
public Iterator createIterator()
{
return new CompositeIterator(mMenuComponents.iterator());
}
}
public class CompositeIterator implements Iterator {
private Stack<Iterator> mStack;
// 將我們欲走訪的最上層合成節點的反覆器,
// 當成參數傳進來
public CompositeIterator(Iterator iterator)
{
mStack = new Stack<Iterator>();
mStack.push(iterator);
}
@Override
public boolean hasNext()
{
// 空的就表示沒有任何菜單
if(mStack.empty())
{
return false;
}
else
{
Iterator iterator = mStack.peek();
if(!iterator.hasNext())
{
mStack.pop();
return hasNext();
}
else
{
return true;
}
}
}
@Override
public Object next()
{
// 先確認是否有下一個能取
if(hasNext())
{
Iterator iterator = mStack.peek();
MenuComponent component =
(MenuComponent)iterator.next();
// 如果目前元素是一個菜單,
// 我們就是有了另一個合成節點,
if(component instanceof Menu)
{
mStack.push(((Menu) component).createIterator());
}
return component;
}
return null;
}
}
public class MenuItem extends MenuComponent {
// 其他部份程式碼不用改
@Override
public Iterator createIterator()
{
// 因為菜單項目沒什麼可以遊走,
// 要是回傳 nul, 客戶就要多條件式判斷
// 因此設計一個什麼事都沒做的 Iterator
return new NullIterator();
}
}
public class NullIterator implements Iterator {
@Override
public boolean hasNext()
{
return false;
}
@Override
public Object next()
{
return null;
}
}
public class Waitress {
private MenuComponent mAllMenus;
// 現在 Waitress 程式碼變得很簡單
// 我們只需將最上層的菜單傳給 Waitress 就可以
public Waitress(MenuComponent allMenus)
{
this.mAllMenus = allMenus;
}
public void printMenu()
{
// 現在只要呼叫一次 print()
// 就可以印出所有菜單層級的所有項目
mAllMenus.print();
}
// 現在可以多一個方法來印出所有是素食的菜單
public void printVegetarianMenu()
{
Iterator iterator = mAllMenus.createIterator();
System.out.println("\nVEGETARIAN MENU\n------");
while(iterator.hasNext())
{
MenuComponent menuComponent =
(MenuComponent)iterator.next();
try
{
if(menuComponent.isVegetarian())
{
menuComponent.print();
}
}
// 例外發生,就補捉這個例外,然後繼續反覆遊走
catch (UnsupportedOperationException e) {}
}
}
}
上面例子要特別說明的是, try/catch 一般是用來做錯誤處理的,而不是程式邏輯之用。但假如不這麼做,我們就只能檢查元件型態,確定是菜單項目才呼叫 print(),而這樣就失去透明性。雖然也可以改寫 isVegetarian(),讓它永遠回傳 flase,但這樣意義上有些扭曲,會變成了:菜單不是素食。
用上述例子的方式,是為了清楚表達 isVegetarian() 是菜單沒支援的方法,這意義不等同於菜單不是素食,且也允許未來有人為菜單實作一個合理的 isVegetarian()
參考資料:
深入淺出設計模式(Head First Design Patterns)
深入淺出設計模式(Head First Design Patterns)

留言
張貼留言