其他分享
首页 > 其他分享> > 设计模式之组合模式

设计模式之组合模式

作者:互联网

需求分析

餐厅的菜单管理系统需要有煎饼屋菜单和披萨菜单。现在希望在披萨菜单中能够加上一份餐后甜点的子菜单。

我们需要一下改变:

我们首先想到的是采用树形结构:
image.png

组合模式让我们能用树形方式创建对象的结构,树里面包含了组合以及个别的对象。使用组合结构,我们能把相同的操作应用在组合的个别对象上,换句话说,在大多数情况下,我们可以忽略对象组合和个别对象之间的差别。

组合模式定义

组合模式允许将对象组合成属性结构来表现“整体/部分”层次结构,组合能让客户以一致的方式处理个别对象以及对象组合。

组合模式能创建一个树形结构
image.png
image.png

组合模式类图

无标题2.png
注:组件、组合、树? 组合包含组件。组件有两种:组合与叶节点元素。听起来象递归是不是? 组合持有一群孩子,这孩子可以是别的组合或者叶节点元素。

利用组合设计菜单

设计思路

我们需要创建一个组件接口MenuComponent来作为菜单和菜单项的共同接口,让我们能够用统一的做法来处理菜单和菜单项。来看看设计的类图:

image.png
菜单组件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();
    }

}

在运行时菜单组合是什么样的:
image.png
组合模式以单一责任设计原则换取透明性。通过让组件的接口同时包含一些管理子节点和叶节点的操作,客户就可以将组合和叶节点一视同仁。也就是说,一个元素究竟是组合还是叶节点,对客户是透明的。
现在,我们在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()方法呢。

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语句来保证他们对正确的对象调用了正确的方法。通常,他们只需要对整个结构调用一个方法并执行操作就可以了.

要点

标签:菜单,组合,MenuComponent,模式,菜单项,new,设计模式,public
来源: https://www.cnblogs.com/pursuingdreams/p/15721067.html