组合模式能让我们以树形方式创建对象的结构,树里面包含了组合以及个别对象。组合模式的优点主要在于能够让我们以相同的操作来处理组合对象及个别对象,而不用对其进行分开讨论。组合模式的整体结构就像一个树形结构,单个对象就是其叶节点,组合对象则为根节点。无论是单个的对象还是组合的对象,我们都称之为组件,单个对象则可以理解为由零个对象组合起来的组件。
实现组合模式的主要做法是首先声明一个抽象组件,该组件中包含所有具体组件所需要的方法。在抽象组件中,一般对各个方法都抛出UnsupportedOperationException,以标识该方法为不可执行的方法,其主要目的是防止不应该实现该方法的组件调用了该方法。然后根据不同的组件类型创建不同的类,让该类继承抽象组件类。对于对象组合的组件,其内部需要维护一个类型为抽象组件类型的聚合对象(列表或者数组等),该聚合对象用来保存组成该组件的子组件,并且组合组件不仅需要实现单个对象组件所应该实现的方法,还需要实现对组件进行添加和删除,以及获取子组件的方法;而对于单个对象的组件,其不需要维护一个聚合对象(其实也可以维护一个长度为零的聚合对象,因为单个对象组件可以理解为子组件为零个的组件),并且其也需要实现其责任范围内的方法。通常对象组合的组件和单个对象的组件所需要实现的方法大致相同,只是由于其责任不同而存在微小差异。组合模式的具体结构图如下:
下面我们就以经典的树形结构——菜单——来介绍组件是如何让客户以一致的方式来处理个别对象以及对象组合。由于组件实际上也相当于一组聚合的对象,因而这里我们也将迭代器模式应用其中,以辅助我们实现组件方法。
在菜单的样式中,一组菜单有多个菜单项,而每一个菜单项又有多个子菜单,子菜单也可能还有孙菜单。这种菜单的组合——根菜单有一组子菜单(直接是一组子菜单的可以理解为有一个空的根菜单),每个子菜单都可能有一组孙菜单——正好就可以使用组合模式作为其实现架构。我们首先创建一个抽象的菜单组件:
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 void print() { throw new UnsupportedOperationException(); } public IteratorcreateIterator() { return new NullIterator(); }}
该组件中声明了添加,删除,获取子节点,获取名称、描述,以及打印的方法。并且可以看出该组件中方法的实现都抛出了UnsupportedOperationException。然后我们创建菜单项类:
public class MenuItem extends MenuComponent { private String name; private String description; public MenuItem(String name, String description) { this.name = name; this.description = description; } public String getName() { return name; } public String getDescription() { return description; } @Override public void print() { System.out.print(" " + getName()); System.out.println(" --" + getDescription()); }}
对于菜单项而言,其有名称,描述和打印行为,因而菜单项类中对这几个方法进行了重写。接着我们创建菜单类:
public class Menu extends MenuComponent { private ArrayListmenuComponents = new ArrayList<>(); private String name; private 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 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("-----------------------"); } @Override public Iterator createIterator() { return new CompositeIterator(menuComponents.iterator()); }}
由于菜单不仅有名称,描述,以及打印行为,其还可以添加、删除和获取子菜单,因而其对着几个方法进行了重写。由于菜单是一个聚合对象,这里为了对其进行迭代操作,因而我们通过迭代器模式让菜单进行迭代操作。因而我们需要创建一个菜单迭代器类来实现迭代行为:
public class CompositeIterator implements Iterator{ private LinkedList > stack = new LinkedList<>(); public CompositeIterator(Iterator iterator) { stack.push(iterator); } @Override public boolean hasNext() { if (stack.isEmpty()) { return false; } Iterator component = stack.peek(); if (!component.hasNext()) { stack.pop(); return hasNext(); } return true; } @Override public MenuComponent next() { if (hasNext()) { Iterator iterator = stack.peek(); MenuComponent component = iterator.next(); if (Menu.class.isInstance(component)) { stack.push(component.createIterator()); } return component; } return null; }}
从上述代码可以看出,由于菜单是一个树形结构,菜单迭代器其实是借助栈(stack)来以中序遍历的方式来展示。此时我们的菜单已经构建完成,下面我们模拟一个客户对菜单进行打印的操作:
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 allMenus = new Menu("ALL MENUS", "All menus combined"); allMenus.add(pancakeHouseMenu); allMenus.add(dinerMenu); allMenus.add(cafeMenu); pancakeHouseMenu.add(new MenuItem("Regular Pancake Breakfast", "Pancakes with fried eggs, sausage")); pancakeHouseMenu.add(new MenuItem("K&B's Pancake Breakfast", "Pancakes with scrambled eggs, and toast")); pancakeHouseMenu.add(new MenuItem("Blueberry Pancakes", "Pancakes made with fresh blueberries")); pancakeHouseMenu.add(new MenuItem("Waffles", "Waffles, with your choice of blueberries or strawberries")); dinerMenu.add(new MenuItem("Pasta", "Spaghetti with Marinara Sauce, and a slice of sourdough bread")); dinerMenu.add(new MenuItem("Vegetarian BLT", "(Fakin') Bacon with lettuce & tomato on whole wheat")); dinerMenu.add(new MenuItem("BLT", "Bacon with lettuce & tomato on whole wheat")); dinerMenu.add(new MenuItem("Soup of the day", "Soup of the day, with a side of potato salad")); dinerMenu.add(new MenuItem("Hotdog", "A hot dog, with saurkraut, relish, onions, topped with cheese")); dinerMenu.add(dessertMenu); dessertMenu.add(new MenuItem("Apple Pie", "Apple pie with a flakey crust, topped with vanilla ice cream")); cafeMenu.add(new MenuItem("Veggie Burger and Air Fries", "Veggie burger on a whole wheat bun, lettuce, tomato, and fries")); cafeMenu.add(new MenuItem("Soup of the day", "A cup of the soup of the day, with a side salad")); cafeMenu.add(new MenuItem("Burrito", "A large burrito, with whole piinto beans, salsa, guacamole")); Iteratoriterator = allMenus.createIterator(); while (iterator.hasNext()) { MenuComponent component = iterator.next(); component.print(); } }}
可以看出,对于一套菜单的打印,我们只需要对根菜单执行打印操作即可,这种操作方式简化了客户对于菜单和菜单项的操作,也即客户可以将组合对象和叶节点一视同仁。
最后,我们需要对组合模式进行一点说明:①为了保持透明性,组合内所有的对象都必须实现相同的接口,否则客户就必须操心哪个对象是用哪个接口,这就失去了组合模式的意义,并且这也意味着有些对象具有一些没有意义的方法调用(这里非法调用会抛出UnsupportedOperatioinException),有时候你可以让这样的方法不做事,或者返回null或false,这样就保证了安全性;②组件可以有一个指向父亲的指针,以便在游走时更容易,而且,如果引用某个孩子,你想从树形结构中删除这个孩子,你会需要父亲去删除它;③如果对孩子的次序有要求,进行增加和删除元素的时候则需要更加复杂的管理方式;④如果组合结构很复杂,或者遍历的代价很高,那么,实现组合节点的缓存会很有帮助。