1. 程式人生 > >Head First設計模式讀書筆記八 第九章下 組合模式

Head First設計模式讀書筆記八 第九章下 組合模式

組合模式+迭代器模式

接著上一節最後的例子,例子的最終list結構圖是這樣的: 這裡寫圖片描述 若要給DinerMenu新加一種Menu(即下面這樣),則需要對現有結構進行較大改動。

這裡寫圖片描述 可以看到,目前的結構中分為兩種結構,一種是menu,是一種容器,可以包含選單項,而第二種是menuItem,是一個節點,只包含該選單項的相關資訊。 如果把這種關係推廣到樹形結構,那麼menu則相當於非葉子節點,而menu則相當於葉子節點。從上圖也很容易看出來。 運用組合模式,則可以實現上面的這種樹形結構,並且方便我們的遍歷。

組合模式示例

示例基於上一節最後的程式碼改進 以MenuComponent為基類

public abstract class MenuComponent {
	//適用於選單項+選單容器
	public String getName() {
		throw new UnsupportedOperationException();
	}
	//適用於選單項+選單容器
	public String getDescription() {
		throw new UnsupportedOperationException();
	}
	//適用於選單項
	public boolean isVegetarian() {
		throw new UnsupportedOperationException();
	}
	//適用於選單項
	public double getPrice() {
		throw new UnsupportedOperationException();
	}
	//適用於選單容器
	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 void print() {
		throw new UnsupportedOperationException();
	}
}

menu和menuItem繼承之

public class Menu extends MenuComponent {
	String name;
	String description;
	ArrayList<MenuComponent> menuItems;

	public Menu(String name, String description) {
		menuItems = new ArrayList<MenuComponent>();
		this.name = name;
		this.description = description;
	}

	@Override
	public void add(MenuComponent menuComponent) {
		menuItems.add(menuComponent);
	}

	@Override
	public void remove(MenuComponent menuComponent) {
		menuItems.remove(menuComponent);
	}

	@Override
	public MenuComponent getChild(int i) {
		return menuItems.get(i);
	}

	@Override
	public String getName() {
		return this.name;
	}

	@Override
	public String getDescription() {
		return this.description;
	}

	@Override
	public void print() {
		System.out.println(" " + getName());
		System.out.println(" "  + getDescription());
		System.out.println(" ------------------------------------ ");
		Iterator<MenuComponent> iterator = menuItems.iterator();
		//重點 遞迴呼叫
		while (iterator.hasNext()) {
			MenuComponent menuComponent = iterator.next();
			menuComponent.print();
		}
	}

}

關鍵在於這裡的遞迴,利用iterator 遍歷Menu中的子項,注意這些子項既可能是Menu也可能時MenuItem。如果時MenuItem,則呼叫的是MenuItem的print方法,如果是Menu,則會呼叫Menu的print,此時則會進入選單的第一次遞迴呼叫。這裡其實運用到多型的思想。

public class MenuItem extends MenuComponent {
	String name;
	String description;
	boolean vegetarian;
	double price;

	public MenuItem(String name, String description, boolean vegetarian,
			double price) {
		super();
		this.name = name;
		this.description = description;
		this.vegetarian = vegetarian;
		this.price = price;
	}

	@Override
	public String getName() {
		return name;
	}

	@Override
	public String getDescription() {
		return description;
	}

	@Override
	public boolean isVegetarian() {
		return vegetarian;
	}

	@Override
	public double getPrice() {
		return price;
	}

	@Override
	public void print() {
		System.out.println(" " + getName());
		System.out.println("isVegetarian " + isVegetarian());

		System.out.println(" " + getPrice());
		System.out.println(" --" + getDescription());
	}

	@Override
	public String toString() {
		return "name =" + name + " description " + description
				+ " isVegetarian " + vegetarian + " price= " + price;
	}
}

改進Waitress

public class Waitress {
	MenuComponent allMenus;

	public Waitress(MenuComponent allMenus) {
		this.allMenus = allMenus;
	}

	public void printMenu() {
		allMenus.print();
	}
}

DinerMenu和PancakeHouseMenu沒有存在必要了,改寫測試類

public class Printer2 {

	public static void main(String[] args) {
		MenuComponent breakfasetMenu = new Menu("PANCAKE HOUSE MENU", "breakfast");
		MenuComponent lunchMenu = new Menu("LUNCH MENU", "lunch");
		MenuComponent dinerMenu = new Menu("DINER MENU", "diner");
		
		MenuComponent allMenuComponent = new Menu("ALL MENUS","All menus combined");
		allMenuComponent.add(dinerMenu);
		allMenuComponent.add(lunchMenu);
		allMenuComponent.add(breakfasetMenu);
		dinerMenu.add(new MenuItem("diner menu item1", "dinermenu item1", true, 1.23));
		dinerMenu.add(new MenuItem("diner menu item2", "dinermenu item2", false, 1.43));
		dinerMenu.add(new MenuItem("diner menu item3", "dinermenu item3", true, 3.23));
		lunchMenu.add(new MenuItem("lunchMenu menu item1", "lunchMenu item1", false, 1.434));
		lunchMenu.add(new MenuItem("lunchMenu menu item2", "lunchMenu item2", true, 6.5));
		breakfasetMenu.add(new MenuItem("breakfasetMenu menu item1", "breakfasetMenu item1", true, 1.2));
		breakfasetMenu.add(new MenuItem("breakfasetMenu menu item2", "breakfasetMenu item2", false, 1.12));
		
		Waitress waitress = new Waitress(allMenuComponent);
		waitress.printMenu();
		
	}

}

測試結果:

 ALL MENUS
 All menus combined
 ------------------------------------ 
 DINER MENU
 diner
 ------------------------------------ 
 diner menu item1
isVegetarian true
 1.23
 --dinermenu item1
 diner menu item2
isVegetarian false
 1.43
 --dinermenu item2
 diner menu item3
isVegetarian true
 3.23
 --dinermenu item3
 LUNCH MENU
 lunch
 ------------------------------------ 
 lunchMenu menu item1
isVegetarian false
 1.434
 --lunchMenu item1
 lunchMenu menu item2
isVegetarian true
 6.5
 --lunchMenu item2
 PANCAKE HOUSE MENU
 breakfast
 ------------------------------------ 
 breakfasetMenu menu item1
isVegetarian true
 1.2
 --breakfasetMenu item1
 breakfasetMenu menu item2
isVegetarian false
 1.12
 --breakfasetMenu item2

完成時的類結構: 這裡寫圖片描述 可以看到,在組合模式中,Menu和MenuItem都是一個個節點,都可以參與遍歷。注意:Waitess其實一個Menu節點,為了與上面的例子結合起來看,我將他寫成了Waitress,其實重點應該在Waitress中包含的allMenus例項。 組合模式的思想

關鍵點:繼承同一個抽象類。 可以看到Menu和MenuItem都繼承了抽象父類MenuComponent,這樣做 可以忽略容器和葉子節點的節點的區別。至於為什麼這麼做,是方便選單項和子項的統一遍歷(不需要區分是Menu還是MenuItem----多型的功勞) 試想一下,如果只有Menu的話,遍歷順序是什麼樣的呢?從上面的輸出結果,不難想象,這種遍歷,與二叉樹的先序遍歷極為類似。 至於MenuItem則可以想象為不能擁有子節點的Menu。 以上總結為兩點: 1.組合模式如何實現:容器(MENU)與節點(MENUITEM)繼承同一個介面 2.組合模式目的:實現更簡潔的遍歷

等一下,MenuComponent似乎違背了單一職責原則? 通常,我們推薦一個類只幹一件事,但是在MenuComponent似乎既幹了選單的事情也幹了選單項的事情,如何解釋呢? 沒錯,組合模式的確違反了單一職責原則,但是這是有目的的。 組合模式通過這種方式讓容器節點和葉子節點一視同仁的遍歷和操作,使用者不需要知道他操作的是容器節點還是葉子節點。 如果我們不採用這種方式,那麼客戶勢必需要使用instanceof來判斷、分別處理葉子和容器節點,這樣透明性變差(使用者需要知道節點的內部結構)。因此這裡的操作是犧牲單一職責換取透明性(使用者不需要知道他操作的是容器節點還是葉子節點)。 在日常開發中,類似的權衡處理方式是時間換空間或者空間換時間的演算法,這樣應該更容易理解吧?

組合模式終極版:組合(外部)迭代器

1.改進MenuComponent,新增新方法

	//新增方法 適用於選單+選單項
	public Iterator<MenuComponent> createIterator(){
		throw new UnsupportedOperationException();
	}

2.為Menu和MenuItem各自實現該方法 Menu:

	@Override
	public Iterator<MenuComponent> createIterator() {
		return menuItems.iterator();
	}

MenuItem:

	@Override
	public Iterator<MenuComponent> createIterator() {
		return new NullIterator();
	}

3.NullIterator:(空迭代器)

public class NullIterator implements Iterator<MenuComponent> {

	public boolean hasNext() {
		return false;
	}

	public MenuComponent next() {
		return null;
	}

}

4.組合迭代器登場

public class CompositeIterator implements Iterator<MenuComponent> {

	Stack<Iterator<MenuComponent>> stack = new Stack<Iterator<MenuComponent>>();//儲存iterator的棧

	public CompositeIterator(Iterator<MenuComponent> iterator) {
		stack.push(iterator);
	}

	public boolean hasNext() {
		if (stack.isEmpty()) {
			// if stack is null, does not have next
			return false;
		} else {
			// get the top of stack
			Iterator<MenuComponent> iterator = stack.peek();
			if (!iterator.hasNext()) {
				//如果棧頂iterator沒有元素 彈出該iterator,遞迴判斷是否hasNext
				if(iterator instanceof CompositeIterator){
					System.out.println("pop>>>>>>>>>>>>>>>>>>>"+((CompositeIterator)stack.pop()));
				}else{
					System.out.println("pop>>>>>>>>>>>>>>>>>>>"+iterator.getClass());
					stack.pop();
				}
				
				return hasNext();
			} else {
				//棧頂iterator有next元素,返回true
				return true;
			}
		}
	}

	public MenuComponent next() {
		System.out.println("xxxxx");
			//取出棧頂iterator
			Iterator<MenuComponent> iterator = stack.peek();
			//取出棧頂iterator的next
			MenuComponent menuComponent = iterator.next();
			System.out.println("hasNEx");
			if (menuComponent instanceof Menu) {
				//如果是Menu型別 還需要放入棧內 為了後面的遍歷
				System.out.println("<<<<<<<<<<<<<<<<<<<<<push"+menuComponent.getName()+" "+stack.push(menuComponent.createIterator()));
			}
			//不管是Menu還是MenuItem都返回
			return menuComponent;
	}

}

5.當然侍者類也要改進

public class Waitress {
	MenuComponent allMenus;

	public Waitress(MenuComponent allMenus) {
		this.allMenus = allMenus;
	}

	public void printMenu() {
		allMenus.print();
	}
	public void printVegetarianMenu(){
		//得到allMenu的Iterator 其型別為CompositeIterator(所有Menu的createIterator返回的實際例項型別為CompositeIterator)
		//遍歷之得到 儲存在 allMenus的MenuComponent
		Iterator<MenuComponent> iterator = allMenus.createIterator();
		System.out.println(" printVegetarianMenu  ");
		while(iterator.hasNext()){//呼叫的CompositeIterator的hasNext
			MenuComponent menuComponent =iterator.next();
			System.out.println("for next....");
			try{
				if(menuComponent.isVegetarian()){
					menuComponent.print();
				}
			}catch(UnsupportedOperationException e){
				System.err.println("Not a menuItem");
			}
		}
	}
}
/**
遍歷過程:
1.對allMenus遍歷 將breakfasetMenu和lunchMenu放入stack                              
2.對breakfasetMenu進行遍歷,判斷其子元素是否是蔬菜,遍歷完畢,從stack彈出breakfasetMenu
3.對lunchMenu進行遍歷,判斷子元素是否是蔬菜,遇到dinerMenu時放入stack                        
4.對dinerMenu進行遍歷,判斷子元素是否是蔬菜,遍歷完畢,彈出dinerMenu
5.對lunchMenu遍歷完畢,彈出lunchMenu.
6.對allMenus遍歷完畢,彈出allMenus,遍歷終了
 */

遍歷圖: 在這裡插入圖片描述 不過dinner貌似會被push兩次,所以會輸出兩次。看來幾遍沒找到原因。。以後再看看吧。。 log:

 printVegetarianMenu  
Not a menuItem
<<<<<<<<<<<<<<<<<<<<<pushPANCAKE HOUSE MENU [email protected]
for next....
for next....
 ====================
 breakfasetMenu menu item1
isVegetarian true
 1.2
 breakfasetMenu item1
 ====================
for next....
pop>>>>>>>>>>>>>>>>>>>class java.util.ArrayList$Itr
pop>>>>>>>>>>>>>>>>>>>[email protected]
<<<<<<<<<<<<<<<<<<<<<pushLUNCH MENU [email protected]
for next....
Not a menuItem
for next....
<<<<<<<<<<<<<<<<<<<<<pushDINER MENU [email protected]
<<<<<<<<<<<<<<<<<<<<<pushDINER MENU [email protected]
for next....
Not a menuItem
for next....
 ====================
 diner menu item1
isVegetarian true
 1.23
 dinermenu item1
 ====================
for next....
for next....
 ====================
 diner menu item3
isVegetarian true
 3.23
 dinermenu item3
 ====================
pop>>>>>>>>>>>>>>>>>>>class java.util.ArrayList$Itr
pop>>>>>>>>>>>>>>>>>>>[email protected]
for next....
 ====================
 diner menu item1
isVegetarian true
 1.23
 dinermenu item1
 ====================
for next....
for next....
 ====================
 diner menu item3
isVegetarian true
 3.23
 dinermenu item3
 ====================
pop>>>>>>>>>>>>>>>>>>>class java.util.ArrayList$Itr
pop>>>>>>>>>>>>>>>>>>>[email protected]
for next....
 ====================
 lunchMenu menu item2
isVegetarian true
 6.5
 lunchMenu item2
 ====================
pop>>>>>>>>>>>>>>>>>>>class java.util.ArrayList$Itr
pop>>>>>>>>>>>>>>>>>>>[email protected]
pop>>>>>>>>>>>>>>>>>>>class java.util.ArrayList$Itr

這個最後的迭代器稍顯複雜,其目的是為了構建一個外部迭代器,但是個人覺得上一個內部迭代器完全夠用了,畢竟對於大多數集合類,有已經支援增強for迴圈了,這個可用可以不用Iterator似乎沒那麼大作用了。至於這個push兩次的問題,有時間再看看吧。。。