開源企業平臺Odoo 15社群版之專案管理應用模組功能簡介
需求分析
餐廳的選單管理系統需要有煎餅屋選單和披薩選單。現在希望在披薩選單中能夠加上一份餐後甜點的子選單。
我們需要一下改變:
- 需要某種樹形結構,可以容納選單、子選單和選單項;
- 需要確定能夠在每個選單的各個項之間遊走,而且至少像用迭代器一樣方便;
- 需要能夠更有彈性地在選單項之間遊走。比方說,可能只需要遍歷甜點選單,或者可以便利整個選單;
我們首先想到的是採用樹形結構:
組合模式讓我們能用樹形方式建立物件的結構,樹裡面包含了組合以及個別的物件。使用組合結構,我們能把相同的操作應用在組合的個別物件上,換句話說,在大多數情況下,我們可以忽略物件組合和個別物件之間的差別。
組合模式定義
組合模式
允許將物件組合成屬性結構來表現“整體/部分”層次結構,組合能讓客戶以一致的方式處理個別對象以及物件組合。
組合模式能建立一個樹形結構
組合模式類圖
注:元件、組合、樹? 組合包含元件。元件有兩種:組合與葉節點元素。聽起來象遞迴是不是? 組合持有一群孩子,這孩子可以是別的組合或者葉節點元素。
利用組合設計選單
設計思路
我們需要建立一個元件介面MenuComponent來作為選單和選單項的共同介面,讓我們能夠用統一的做法來處理選單和選單項。來看看設計的類圖:
選單元件MenuComponent提供了一個介面,讓選單項和選單共同使用。因為我們希望能夠為這些方法提供預設的實現,所以我們在這裡可以把MenuComponent介面換成一個抽象類。
在這個類中,有顯示選單資訊的方法getName()等,還有操縱元件的方法add(), remove(), getChild()等。選單項MenuItem覆蓋了顯示選單資訊的方法,而選單Menu覆蓋了一些對他有意義的方法。
程式碼實現
1. 實現選單元件 所有的元件都必須實現MenuComponent介面;然而,葉節點和組合節點的角色不同,所以有些方法 可能並不適合某種節點。面對這種情況,有時候,你最好是丟擲執行時異常。 public abstract class MenuComponent { // add,remove,getchild // 把組合方法組織在一起,即新增、刪除和取得選單元件 public void add(MenuComponent component) { throw new UnsupportedOperationException(); } public void remove(MenuComponent component) { throw new UnsupportedOperationException(); } public MenuComponent getChild(int i) { throw new UnsupportedOperationException(); } // 操作方法:他們被選單項使用。 public String getName() { throw new UnsupportedOperationException(); } public String getDescription() { throw new UnsupportedOperationException(); } public double getPrice() { throw new UnsupportedOperationException(); } public boolean isVegetarian() { throw new UnsupportedOperationException(); } public void print() { throw new UnsupportedOperationException(); } } 2. 實現選單項類。這是組合類圖裡的葉類,它實現組合內元素的行為。 public class MenuItem extends MenuComponent { String name; String description; boolean vegetarian; double price; public MenuItem(String name, String description, boolean vegetarian, double price) { this.name = name; this.description = description; this.vegetarian = vegetarian; this.price = price; } public String getName() { return name; } public String getDescription() { return description; } public boolean isVegetarian() { return vegetarian; } public double getPrice() { return price; } public void print() { System.out.println(" " + getName()); if (isVegetarian()) { System.out.println("(V)"); } System.out.println(", " + getPrice()); System.out.println(" -- " + getDescription()); } } 3. 實現組含選單 public class Menu extends MenuComponent { // 選單可以有任意數的孩子,都必須屬子MenuComponent型別,使用ArrayList記錄它們。 ArrayList<MenuComponent> menuComponents = new ArrayList<MenuComponent>(); String name; String description; public Menu(String name, String description) { this.name = name; this.description = description; } 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 getDescription() { return description; } public void print() { System.out.println("\n" + getName()); System.out.println(", " + getDescription()); System.out.println("----------------------"); // 在遍歷期間,如果遇到另一個選單物件,它的print()方法會開始另一個遍歷,依次類推。 Iterator<MenuComponent> iterator = menuComponents.iterator(); while(iterator.hasNext()) { MenuComponent menuComponent = iterator.next(); menuComponent.print(); } } } 4. 更新女招待的程式碼 public class Waitress { MenuComponent allMenus; public Waitress(MenuComponent allMenus) { this.allMenus = allMenus; } public void printMenu() { allMenus.print(); } } 5. 編寫測試程式 public class Client { public static void main(String[] args) { // 建立選單物件 MenuComponent pancakeHouseMenu = new Menu("煎餅屋選單", "提供各種煎餅。"); MenuComponent pizzaHouseMenu = new Menu("披薩屋選單", "提供各種披薩。"); MenuComponent cafeMenu = new Menu("咖啡屋選單", "提供各種咖啡"); // 建立一個頂層的選單 MenuComponent allMenus = new Menu("All Menus", "All menus combined"); // 把所有選單都新增到頂層選單 allMenus.add(pancakeHouseMenu); allMenus.add(pizzaHouseMenu); allMenus.add(cafeMenu); // 在這裡加入選單項 pancakeHouseMenu.add(new MenuItem("蘋果煎餅", "香甜蘋果煎餅", true, 5.99)); pizzaHouseMenu.add(new MenuItem("至尊披薩", "義大利至尊咖啡", false, 12.89)); cafeMenu.add(new MenuItem("美式咖啡", "香濃美式咖啡", true, 3.89)); Waitress waitress = new Waitress(allMenus); waitress.printMenu(); } }
在執行時選單組合是什麼樣的:
組合模式以單一責任設計原則換取透明性。通過讓元件的介面同時包含一些管理子節點和葉節點的操作,客戶就可以將組合和葉節點一視同仁。也就是說,一個元素究竟是組合還是葉節點,對客戶是透明的。
現在,我們在MenuComponent類中同時具有兩種型別的操作。因為客戶有機會對一個元素做一些不恰當或是沒有意義的操作,所以我們失去了一些安全性。
組合迭代器
我們現在再擴充套件一下,這種組合菜單如何設計迭代器呢?細心的朋友應該觀察到,我們剛才使用的迭代都是遞迴呼叫的選單項和選單內部迭代的方式。現在我們想設計一個外部迭代的方式怎麼辦?譬如出現一個新需求:服務員需要打印出蔬菜性質的所有食品選單。
首先,我們給MenuComponent加上判斷蔬菜類食品的方法,然後在選單項中進行重寫:
public abstract class MenuComponent {
…………
/**
* 判斷是否為蔬菜類食品
*/
public boolean isVegetarian() {
throw new UnsupportedOperationException();
}
}
/**
* 選單項
*/
public class MenuItem extends MenuComponent{
String name;
double price;
/**蔬菜類食品標誌*/
boolean vegetarian;
…………
public boolean isVegetarian() {
return vegetarian;
}
public void setVegetarian(boolean vegetarian) {
this.vegetarian = vegetarian;
}
}
這個CompositeIterator是一個不可小覷的迭代器,它的工作是遍歷元件內的選單項,而且確保所有的子選單(以及子子選單……)都被包括進來。
//跟所有的迭代器一樣,我們實現Iterator介面。
class CompositeIterator implements Iterator {
Stack stack = new Stack();
/**
*將我們要遍歷的頂層組合的迭代器傳入,我們把它拋進一個堆疊資料結構中
*/
public CompositeIterator(Iterator iterator) {
stack.push(iterator);
}
@Override
public boolean hasNext() {
//想要知道是否還有下一個元素,我們檢查堆疊是否被清空,如果已經空了,就表示沒有下一個元素了
if (stack.empty()) {
return false;
} else {
/**
*否則我們就從堆疊的頂層中取出迭代器,看看是否還有下一個元素,
*如果它沒有元素,我們將它彈出堆疊,然後遞迴呼叫hasNext()。
*/
Iterator iterator = (Iterator) stack.peek();
if (!iterator.hasNext()) {
stack.pop();
return hasNext();
} else {
//否則,便是還有下一個元素
return true;
}
}
}
@Override
public Object next() {
//好了,當客戶想要取得下一個元素時候,我們先呼叫hasNext()來確定時候還有下一個。
if (hasNext()) {
//如果還有下一個元素,我們就從堆疊中取出目前的迭代器,然後取得它的下一個元素
Iterator iterator = (Iterator) stack.peek();
MenuComponent component = (MenuComponent) iterator.next();
/**
*如果元素是一個選單,我們有了另一個需要被包含進遍歷中的組合,
*所以我們將它丟進對戰中,不管是不是選單,我們都返回該元件。
*/
if (component instanceof Menu) {
stack.push(component.createIterator());
}
return component;
} else {
return null;
}
}
@Override
public void remove() {
// 我們不支援刪除,這裡只有遍歷
throw new UnsupportedOperationException();
}
}
在我們寫MenuComponent類的print方法的時候,我們利用了一個迭代器遍歷元件內的每個項,如果遇到的是選單,我們就會遞迴地呼叫print()方法處理它,換句話說,MenuComponent是在“內部”自行處理遍歷。
但是在上頁的程式碼中,我們實現的是一個“外部”的迭代器,所以有許多需要追蹤的事情。外部迭代器必須維護它在遍歷中的位置,以便外部客戶可以通過hasNext()和next()來驅動遍歷。在這個例子中,我們的程式碼也必須維護組合遞迴結構的位置,這也就是為什麼當我們在組合層次結構中上上下下時,使用堆疊來維護我們的位置。
空迭代器
選單項沒什麼可以遍歷的,那麼我們要如何實現選單項的createIterator()方法呢。
- 1:返回null。我們可以讓createIterator()方法返回null,但是如果這麼做,我們的客戶程式碼就需要條件語句來判斷返回值是否為null;
- 2:返回一個迭代器,而這個迭代器的hasNext()永遠返回false。這個是更好的方案,客戶不用再擔心返回值是否為null。我們等於建立了一個迭代器,其作用是“沒作用”。
class NullIterator implements Iterator{
@Override
public boolean hasNext() {
// 當hasNext呼叫時,永遠返回false
return false;
}
@Override
public Object next() {
return null;
}
@Override
public void remove() {
throw new UnsupportedOperationException();
}
}
給我素食選單
public class Waitress {
MenuComponent allMenus;
public Waitress(MenuComponent allMenus) {
this.allMenus = allMenus;
}
public void printMenu() {
allMenus.print();
}
public void printVegetarianMenu() {
Iterator<MenuComponent> iterator = allMenus.createIterator();
System.out.println("\nVEGETARIAN MENU\n----");
while (iterator.hasNext()) {
MenuComponent menuComponent = iterator.next();
try {
// 判斷素食,選單會拋異常,捕獲了就能正常的遍歷選單項
// 雖說可以 instanceof 進行執行時的型別檢查,但是這樣就會失去選單和選單項的透明性,我們只需要關注他們的介面就行
// 或者可以選擇讓選單的 isVegetarian() 返回 false,這樣也能保證程式的透明性
if (menuComponent.isVegetarian()) {
menuComponent.print();
}
} catch (UnsupportedOperationException e) { }
// 只能呼叫選單項的 print(),不能呼叫選單的 print()。因為這裡是靠迭代器實現的,如果呼叫選單的 print() 會重複列印
}
}
}
使用場景:當你有數個物件的集合,它們彼此之間有“整體/部分”的關係,並且你想用一致的方式對待這些物件時,你就需要使用組合模式。
組合使用的結構:通常是用樹形結構,也就是一種層次結構。根就是頂層的組合,然後往下是它的孩子,最末端是葉節點。
優點:我認為我讓客戶生活得更加簡單。我的客戶不再需要操心面對的是組合物件還是葉節點物件了,所以就不需要寫一大堆if語句來保證他們對正確的物件呼叫了正確的方法。通常,他們只需要對整個結構呼叫一個方法並執行操作就可以了.
要點
- 組合模式提供一個結構,可同時包容個別物件和組合物件。
- 組合模式允許客戶對個別物件以及組合物件一視同仁。
- 組合結構內的任意物件稱為元件,元件可以是組合,也可以是葉節點。在實現組合模式時,有許多設計上的折衷。你要根據需要平衡透明性和安全性。