承接上篇博客,我们需要修改所有的菜单,以满足可以添加子菜单,但是我们由于已经把整个项目的框架搭好了,不可能重新实现菜单,所以我们只能修改,那么我们需要做些什么呢?
- 树形结构,可以容纳菜单、子菜单和菜单项。
- 可以在这个结构中的每个元素中游走,比如类似于迭代器一样的装置。
- 可以遍历单个菜单项,比如只遍历甜点菜单,也可以遍历整个菜单所有菜单项。
我们可以使用组合模式来解决这个问题,首先我们看看组合模式的定义。
组合模式定义
组合模式允许你将对象组合成树形结构来表现「整体/部分」层次结构。组合能让客户以一致的方式处理个别对象以及对象组合。
这个模式可以创建一个树形结构,在同一个结构中处理嵌套菜单和菜单项组。通过将菜单和项放在相同的结构中,我们创建了一个「整体/部分」层次结构,即由菜单和菜单项组成的对象树。
使用一个 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() 方法是一直抛出异常的,所以不做任何操作,继续遍历。
到这里组合模式与适配器模式结合使用结束。