1. 程式人生 > 其它 >開源企業平臺Odoo 15社群版之專案管理應用模組功能簡介

開源企業平臺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語句來保證他們對正確的物件呼叫了正確的方法。通常,他們只需要對整個結構呼叫一個方法並執行操作就可以了.

要點

  • 組合模式提供一個結構,可同時包容個別物件和組合物件。
  • 組合模式允許客戶對個別物件以及組合物件一視同仁。
  • 組合結構內的任意物件稱為元件,元件可以是組合,也可以是葉節點。在實現組合模式時,有許多設計上的折衷。你要根據需要平衡透明性和安全性。