【HeadFirst設計模式學習筆記】10 組合模式

weixin_33816946發表於2012-12-21

作者:gnuhpc
出處:http://www.cnblogs.com/gnuhpc/

1.我們接著上次的話題,現在有一個新需求就是子選單功能。我們在此使用一個新的模式——組合模式,意思是允許你將物件組合成樹形結構來表現整體-區域性的層級,它能使客戶以一致的方式處理個別物件以及物件的組合。你告訴頂層的動作,它會完成所有相關的操作。這也就是這個模式使用的場景。在這個例子中,分為選單和選單項,一個選單中可能還有選單,而且一個選單中可能還有多個選單項,但是選單項不會再有下一層的包含關係。

2.一開始,我們需要建立一個元件介面,來作為選單和選單項的共同介面,讓我們能夠用統一的做法來處理menu和menuItem。所以我們先建立這樣一個類:

public abstract class MenuComponent {//加粗加下劃線的都是我們組合的選單要用到的方法,而下邊的則是選單項和選單或許都要用到的。
    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 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();
    }
}

請注意,這裡面有些對選單項有意義,而有些則只對選單有意義。預設丟擲不支援操作的異常方便了後邊繼承後對某些操作的實現。

我們利用這個抽象類,進一步得到選單和選單子項這兩個類的設計:

選單:

public class Menu extends MenuComponent {
ArrayList menuComponents = new ArrayList();//這個是來維護一個子選單的具體資料成員。
    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 (MenuComponent)menuComponents.get(i);
    }
    public String getName() {
        return name;
    }
    public String getDescription() {
        return description;
    }
    public void print() {//由於這是一個組合的類,所以它還有成員,那麼列印的時候我們就要使用迭代器,
        System.out.print("/n" + getName());
        System.out.println(", " + getDescription());
        System.out.println("---------------------");
        Iterator iterator = menuComponents.iterator();
        while (iterator.hasNext()) {
MenuComponent menuComponent = //這裡就體現了為什麼選單和選單項都要繼承自這一個統一的抽象類了。
                (MenuComponent)iterator.next();
            menuComponent.print();//因為都實現了print方法,所以可以統一操作。
        }
    }
}

選單子項:這個是葉子節點,不會再有下一級的葉子節點出現了。

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 double getPrice() {
        return price;
    }
    public boolean isVegetarian() {
        return vegetarian;
    }
    public void print() {
        System.out.print("  " + getName());
        if (isVegetarian()) {
            System.out.print("(v)");
        }
        System.out.println(", " + getPrice());
        System.out.println("     -- " + getDescription());
    }
}

現在,我們再建立一個女招待,你會發現這時的女招待是如此輕鬆:

public class Waitress {
    MenuComponent allMenus;
    public Waitress(MenuComponent allMenus) {
        this.allMenus = allMenus;
    }
    public void printMenu() {
        allMenus.print();
    }
}

我們寫一段測試程式碼來建立一個帶有子選單的選單:

public class MenuTestDrive {
    public static void main(String args[]) {
        MenuComponent pancakeHouseMenu =
            new Menu("PANCAKE HOUSE MENU", "Breakfast");
        MenuComponent dinerMenu =
            new Menu("DINER MENU", "Lunch");
        MenuComponent cafeMenu =
            new Menu("CAFE MENU", "Dinner");
        MenuComponent dessertMenu =
            new Menu("DESSERT MENU", "Dessert of course!");
        MenuComponent coffeeMenu = new Menu("COFFEE MENU", "Stuff to go with your afternoon coffee");
        MenuComponent allMenus = new Menu("ALL MENUS", "All menus combined");
        allMenus.add(pancakeHouseMenu);
        allMenus.add(dinerMenu);
        allMenus.add(cafeMenu);
        pancakeHouseMenu.add(new MenuItem(
            "K&B's Pancake Breakfast",
            "Pancakes with scrambled eggs, and toast",
            true,
            2.99));
        pancakeHouseMenu.add(new MenuItem(
            "Regular Pancake Breakfast",
            "Pancakes with fried eggs, sausage",
            false,
            2.99));
        pancakeHouseMenu.add(new MenuItem(
            "Blueberry Pancakes",
            "Pancakes made with fresh blueberries, and blueberry syrup",
            true,
            3.49));
        pancakeHouseMenu.add(new MenuItem(
            "Waffles",
            "Waffles, with your choice of blueberries or strawberries",
            true,
            3.59));

        dinerMenu.add(new MenuItem(
            "Vegetarian BLT",
            "(Fakin') Bacon with lettuce & tomato on whole wheat",
            true,
            2.99));
        dinerMenu.add(new MenuItem(
            "BLT",
            "Bacon with lettuce & tomato on whole wheat",
            false,
            2.99));
        dinerMenu.add(new MenuItem(
            "Soup of the day",
            "A bowl of the soup of the day, with a side of potato salad",
            false,
            3.29));
        dinerMenu.add(new MenuItem(
            "Hotdog",
            "A hot dog, with saurkraut, relish, onions, topped with cheese",
            false,
            3.05));
        dinerMenu.add(new MenuItem(
            "Steamed Veggies and Brown Rice",
            "Steamed vegetables over brown rice",
            true,
            3.99));
        dinerMenu.add(new MenuItem(
            "Pasta",
            "Spaghetti with Marinara Sauce, and a slice of sourdough bread",
            true,
            3.89));
        dinerMenu.add(dessertMenu);
        dessertMenu.add(new MenuItem(
            "Apple Pie",
            "Apple pie with a flakey crust, topped with vanilla icecream",
            true,
            1.59));
        dessertMenu.add(new MenuItem(
            "Cheesecake",
            "Creamy New York cheesecake, with a chocolate graham crust",
            true,
            1.99));
        dessertMenu.add(new MenuItem(
            "Sorbet",
            "A scoop of raspberry and a scoop of lime",
            true,
            1.89));

        cafeMenu.add(new MenuItem(
            "Veggie Burger and Air Fries",
            "Veggie burger on a whole wheat bun, lettuce, tomato, and fries",
            true,
            3.99));
        cafeMenu.add(new MenuItem(
            "Soup of the day",
            "A cup of the soup of the day, with a side salad",
            false,
            3.69));
        cafeMenu.add(new MenuItem(
            "Burrito",
            "A large burrito, with whole pinto beans, salsa, guacamole",
            true,
            4.29));

        cafeMenu.add(coffeeMenu);

        coffeeMenu.add(new MenuItem(
            "Coffee Cake",
            "Crumbly cake topped with cinnamon and walnuts",
            true,
            1.59));
        coffeeMenu.add(new MenuItem(
            "Bagel",
            "Flavors include sesame, poppyseed, cinnamon raisin, pumpkin",
            false,
            0.69));
        coffeeMenu.add(new MenuItem(
            "Biscotti",
            "Three almond or hazelnut biscotti cookies",
            true,
            0.89));
        Waitress waitress = new Waitress(allMenus);
        waitress.printMenu();
    }
}

3.你要是還記得上節我們的單一原則,你就會發現組合模式中一個類有兩個職責——管理層次結構,執行選單操作(可以看我們實現的menucomponent中標有黑體帶下劃線和普通字型的方法,它們承擔著兩種完全不同的職責)。這就引出了一個compromise——單一職責和透明性。所謂透明性,也就是說通過讓元件的介面同時包含一些管理子節點和葉節點的操作,呼叫者就可以將組合和葉子節點一視同仁,這就做到了組合和葉子節點對元件的透明。我們也可以將這樣的職責分屬於組合和葉子節點,不過在操作時我們就需要使用判斷語句結合instanceof操作符處理不同型別的節點。

4.最後我們試圖在組合中加入迭代器,一方面作為上次迭代器模式的複習演練,另一封面迭代器在組合模式中有很重要的應用。

我們在Menucomponent類中加入createIterator方法,而Menu和MenuItem裡也加入相應的方法:

MenuComponent:

public abstract class MenuComponent {
    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 String getName() {
        throw new UnsupportedOperationException();
    }
    public String getDescription() {
        throw new UnsupportedOperationException();
    }
    public double getPrice() {
        throw new UnsupportedOperationException();
    }
    public boolean isVegetarian() {
        throw new UnsupportedOperationException();
    }

    public abstract Iterator createIterator();
    public void print() {
        throw new UnsupportedOperationException();
    }
}

Menu:

    public class Menu extends MenuComponent {
ArrayList<MenuComponent> menuComponents = new ArrayList<MenuComponent>();//維護選單和子選單
    String name;
    String description;
    public Menu(String name, String description){
        this.name = name;
        this.description = description;
    }
    @Override
    public void add(MenuComponent menuComponent) {
        menuComponents.add(menuComponent);
    }
    @Override
    public void remove(MenuComponent menuComponent) {
        menuComponents.remove(menuComponent);
    }
    @Override
    public MenuComponent getChild(int i) {
        return menuComponents.get(i);
    }
    @Override
    public String getDescription() {
        return description;
    }
    @Override
    public String getName() {
        return name;
    }
    @Override
    public Iterator<MenuComponent> createIterator() {
        return new CompositeIterator(menuComponents.iterator());

       //若使用後邊簡化版迭代器則直接返回持有引用:return new CompositeIterator(menuComponents);
    }
    @Override
    public void print() {
        System.out.print("/n" + getName());
        System.out.println(", " + getDescription());
        System.out.println("---------------------");
        Iterator<MenuComponent> iter = menuComponents.iterator();
        while(iter.hasNext()){
               MenuComponent menuComponent = iter.next();
               menuComponent.print();

       }
    }
}

我們實現組合模式的一個外部迭代器:這是一個強大的迭代器,它遍歷了元件內所有的選單項,確保所有的子選單都包括進來。

public class CompositeIterator implements Iterator {
    Stack stack = new Stack();
    public CompositeIterator(Iterator iterator) {
        stack.push(iterator);//我們把要遍歷的的頂層組合的迭代器放入一個堆疊。外部迭代器必須維護其在遍歷中的位置。
    }
    public Object next() {
        if (hasNext()) {//當呼叫者要取得下一個元素的時候,我們要確定是不是還有下一個
            Iterator iterator = (Iterator) stack.peek();//要是有的話就從棧頂取出這個迭代器
            MenuComponent component = (MenuComponent) iterator.next();//取出一個元素
            if (component instanceof Menu) {//這個元素若是Menu則需要進一步使用迭代器,所以就放入堆疊中。
                stack.push(component.createIterator());
            }
            return component;
        } else {
            return null;
        }
    }
    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;
            }
        }
    }
    public void remove() {//暫不支援
        throw new UnsupportedOperationException();
    }
}

這個實現有些複雜,這是由於menu中還可能有menu,如果我們限定menu中不能有menu這樣的簡化場景,只能有menuItem那麼從建構函式中傳遞ArrayList<MenuComponent>可以簡化實現:

public class CompositeIterator implements Iterator{
    private ArrayList<MenuComponent> menuComponent;
    private static int position = 0;

    public MenuIterator(ArrayList<MenuComponent> menuComponent)
    {
        this.menuComponent = menuComponent;
    }

    public MenuComponent next()
    {
        return menuComponent.get(position);
    }

    public boolean hasNext()
    {
        if(position<menuComponent.length()&&menuComponent.get(position)!=null)
        {
     return true;
        }
else
{
     return false;
}
    }

    public void remove()
    {//暫不支援
        throw new UnsupportedOperationException();
    }
}

MenuItem:注意,這裡返回了一個叫做NullIterator的物件,因為這是葉子節點。

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 Iterator<MenuComponent> createIterator() {
        return new NullIterator();
    }

    public void print() {
        System.out.print(" " + getName());
        if (isVegetarian()) {
            System.out.print("(v)");
        }
        System.out.print(", " + getPrice());
        System.out.println("  --" + getDescription());
    }

}

這個空迭代器主要是為了葉子節點停止迭代,我們建立了一個沒有什麼可以被迭代的迭代器,這樣是為了統一操作,實現如下:

public class NullIterator implements Iterator {
    public Object next() {
        return null;
    }
    public boolean hasNext() {
        return false;
    }
    public void remove() {
        throw new UnsupportedOperationException();
    }
}

處理的方式也可以是:傳入MenuItem物件,在next中再返回這個物件。

我們利用這個外部的迭代器重新寫一個列印所有素食選單的女招待:

public class Waitress {
    MenuComponent allMenus;
    public Waitress(MenuComponent allMenus) {
        this.allMenus = allMenus;
    }
    public void printMenu() {
        allMenus.print();
    }
    public void printVegetarianMenu() {
        Iterator iterator = allMenus.createIterator(); //獲得這個外部迭代器

        System.out.println("/nVEGETARIAN MENU/n----");
        while (iterator.hasNext()) {
            MenuComponent menuComponent =
                    (MenuComponent)iterator.next();//獲得元素
            try {
                if (menuComponent.isVegetarian()) {//
                    menuComponent.print();
                }
            } catch (UnsupportedOperationException e) {}
        }
    }
}

 

作者:gnuhpc
出處:http://www.cnblogs.com/gnuhpc/

相關文章