《Head First 设计模式》读书笔记——组合模式

承接上篇博客,我们需要修改所有的菜单,以满足可以添加子菜单,但是我们由于已经把整个项目的框架搭好了,不可能重新实现菜单,所以我们只能修改,那么我们需要做些什么呢?

  • 树形结构,可以容纳菜单、子菜单和菜单项。
  • 可以在这个结构中的每个元素中游走,比如类似于迭代器一样的装置。
  • 可以遍历单个菜单项,比如只遍历甜点菜单,也可以遍历整个菜单所有菜单项。

我们可以使用组合模式来解决这个问题,首先我们看看组合模式的定义。

组合模式定义

组合模式允许你将对象组合成树形结构来表现「整体/部分」层次结构。组合能让客户以一致的方式处理个别对象以及对象组合。

这个模式可以创建一个树形结构,在同一个结构中处理嵌套菜单和菜单项组。通过将菜单和项放在相同的结构中,我们创建了一个「整体/部分」层次结构,即由菜单和菜单项组成的对象树。

使用一个 Component 接口定义这些组件和叶子节点的操作,比如 add()、remove()、setChild()、operation() 等等。叶子节点也继承了这些方法,但因为它没有子节点,所以这些方法可能没什么意义,但它通过 operation() 方法完成它自己的任务。而组件具有子节点,所以这几个方法都需要使用。而客户可以通过 Component 接口操作组合中的对象。

现在我们来利用组合模式设计菜单。

设计菜单

女侍者是我们的客户,她将使用菜单组件接口访问菜单和菜单项。菜单组件除了提供 getName()、getDescription()、getPrice()、isVegetarian() 几个方法外,我们新添加 print()、add()、remove() 和 setChild() 几个方法。后面新添加的方法对于菜单项没有意义,因为它是叶子节点,下面并没有任何组件,而菜单组件要包含其他的叶子节点,所以就需要这些方法。现在我们来看看菜单组件代码:

第一个抽象类 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 void print() {
        throw new UnsupportedOperationException();
    }
}

接着我们继续实现来看菜单项类,它是叶子节点,它实现组合内元素的行为。需要继承上面的 MenuComponent 抽象类。

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.print(" " + getName());
        if (isVegetarian()) {
            System.out.print("(v)");
        }
        System.out.println(", " + getPrice());
        System.out.println("    -- " + getDescription());
    }
}

下面是组合菜单,我们已经有了菜单项,还需要组合类,同样需要继承 MenuComponent 抽象类,重写一些方法。

public class NewMenu extends MenuComponent {
    ArrayList menuComponents = new ArrayList();
    String name;
    String description;

    public NewMenu(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 (MenuComponent) menuComponents.get(i);
    }

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

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

    @Override
    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();
        }
    }
}

这个类由于是组合类,所以需要一个数据集合,也就是它的孩子们,我们使用 ArrayList 来存储,重写一些方法,需要注意的是 print() 方法,我们可以使用迭代器,来调用集合中元素的 print() 方法,遇到叶子节点就直接打印,遇到菜单就继续遍历它的集合属性。

现在我们修改女侍者类,添加一个菜单节点的属性,添加一个构造方法,再修改 printMenu() 方法,调用的是它内在的菜单节点的 print() 方法。

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

现在我们开始测试。

	public static void main(String[] args) {
		NewMenu pancakeHouseMenu = new NewMenu("PANCAKE HOUSE MENU", "Breakfast");
        NewMenu dinerMenu = new NewMenu("DINER MENU", "Lunch");
        NewMenu cafeMenu = new NewMenu("CAFE MENU", "Dinner");
        NewMenu dessertMenu = new NewMenu("DESSERT MENU", "Dessert of course!");

        NewMenu allMenus = new NewMenu("ALL MENUS", "All menus combined");

        allMenus.add(pancakeHouseMenu);
        allMenus.add(dinerMenu);
        allMenus.add(cafeMenu);
        pancakeHouseMenu.add(new MenuItem("Veggie Burger and Air Fries",
                "Veggie burger on a whole wheat bun, lettuce, tomato, and fries",
                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 ice cream",
                true,
                1.59));

        Waitress waitress = new Waitress(allMenus);
        waitress.printMenu();
	}

女招待的代码越来越简单,现在我们只需要在创建女侍者对象时传入一个菜单根节点,也就是最顶层的菜单组件交给她,我们就可以通过 printMenu() 方法打印菜单的具体功能。

我们的测试方法中,先创建了所有的菜单组件对象,然后创建的是要交给女侍者的 allMenus 对象,把每个菜单对象都添加到 allMenus 中,然后再在菜单对象中添加一些菜单项,最后创建女侍者对象,调用 printMenu() 方法打印菜单。

在整个系统中,组合模式以单一责任设计原则换取透明性,通过让组件的接口同时包含一些管理子节点和叶节点的操作,客户就可以将组合与叶节点一视同仁。

前面我们也提过,女招待还可能提供菜单中的素食项,这时候就需要使用迭代器。我们先创建一个组合迭代器。

public 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 {
            Iterator iterator = (Iterator) stack.peek();
            if (!iterator.hasNext()) {
                stack.pop();
                return hasNext();
            } else {
                return true;
            }
        }
    }

    @Override
    public Object next() {
        if (hasNext()) {
            Iterator iterator = (Iterator) stack.peek();
            MenuComponent component = (MenuComponent) iterator.next();
            stack.push(component.createIterator());
            return component;
        } else {
            return null;
        }
    }
}

通过一个栈存储迭代器。然后修改 MenuComponent 类,新增一个 createIterator() 方法,并修改子类的实现。

// MenuComponent.java

	public Iterator createIterator() {
        throw new UnsupportedOperationException();
    }

// NewMenu.java
	Iterator<MenuComponent> iterator = null;
	
    public Iterator createIterator() {
        if (iterator == null) {
            iterator = new CompositeIterator(menuComponents.iterator());
        }
        return iterator;    
    }

// MenuItem.java
    public Iterator createIterator() {
        return new NullIterator();
    }

// NullIterator.java
public class NullIterator implements Iterator {

    @Override
    public boolean hasNext() {
        return false;
    }

    @Override
    public Object next() {
        return null;
    }

    public void remove() {
        throw new UnsupportedOperationException();
    }
}

菜单返回的是 CompositeIterator 对象,而菜单项返回的是空的迭代器。因为空迭代器的 hasNext() 方法,所以就不进行迭代。之所以不返回 null,就是因为不需要客户代码中进行一个返回值的判断,省去了一个步骤。

女侍者的素食打印菜单代码。

    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) {}
        }
    }

这个方法先是获取到 allMenus 的迭代器,然后遍历每个元素,调用元素的 isVegetarian() 方法,如果为 true,就调用它的 print() 方法。只有菜单项的 print() 方法可以被调用,因为菜单的 isVegetarian() 方法是一直抛出异常的,所以不做任何操作,继续遍历。

到这里组合模式与适配器模式结合使用结束。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值