组合模式——
允许你将对象组合成树形结构来表现“整体/部分”层次结构。组合能让客户以一致的方式处理个别对象以及对象组合。
本文的例子沿用了前一节《Head First 设计模式总结(九) 迭代器模式》中菜单和菜单项的例子。
组合模式能够创建一个树形结构:
本例是准备在同一个结构中处理嵌套菜单和菜单项组,实现为菜单项添加子菜单,比如说给餐厅菜单(DinnerMenu)添加一个甜点子菜单。通过将菜单和项放在相同的结构中,我们创建了一个“整体/部分”层次结构,即由菜单和菜单项组成的对象树。但是可以将它视为一个整体,像是一个丰富的大菜单。
下面是组合模式的类图:
下面我们利用组合模式来设计新的菜单,为之前存在的菜单添加子菜单:
[注] 这里的MenuComponent接口就是Component(组件接口),MemuItem是leaf(叶节点),它只覆盖了MemuComponent中对它有意义的方法(一些用于描述菜品信息的方法)。Menu是Composite(组合),它也只覆盖了MenuComponent中对它自己有意义的方法(用于增加或者减少菜单项的方法)。
所有组件都必须实现MenuComponent接口,然而叶子节点和组合节点的角色不同,所以有些方法可能并不适合某种节点,面对这种情况,有时候最好是抛出运行时异常(throw new UnsupportedOperationException()),通过抛出该异常,如果菜单项或菜单不支持某个操作,它们就不需要做任何事,直接继承默认实现即可。
先给出MenuComponent抽象类的代码:(它是所有composite和leaf都必须实现的接口,即组合模式类图中的component接口)
public abstract class MenuComponent {
public void add(MenuComponent menuComponent){
throw new UnsupportedOperationException();
}
public void remove(){
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();
}
}
接下来,实现菜单项MenuItem,它是组合类图里的leaf。它实现component类元素的行为。
public class MenuItem extends MenuComponent {
String name;
String description;
boolean isVegetarian;
double price;
public MenuItem(String name,String description,boolean isVegetarian,double price){
this.name = name;
this.description = description;
this.isVegetarian = isVegetarian;
this.price = price;
}
@Override
public String getName() {
return name;
}
@Override
public String getDescription() {
return description;
}
@Override
public boolean isVegetarian() {
return isVegetarian;
}
@Override
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());
}
}
现在已经有了leaf,下面我们需要弄一个composite,composite可以持有leaf或者其他composite,在本实例中,它是Menu,它可以包含MenuItem和其他Menu,下面是实现的一个Menu类:
import java.util.ArrayList;
import java.util.Iterator;
/*
* this is the composite node
* */
public class Menu extends MenuComponent {
//创建一个ArrayList当作容器,用于装MenuItem或者其他子菜单,MenuItem和其他菜单项都是MenuComponent类型
ArrayList menuComponents = new ArrayList();
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);
}
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.println("\n" + getName());
System.out.println(", " + getDescription());
System.out.println("-----------------------");
/*
*这是递归的思想
* 这里用到了迭代器,用它遍历所有组件,遍历的时候可能遇到MenuItem,也可能遇到其他菜单,但是它们都有print()方法,当遍历到它们时,不管是谁,在那调用print()方法准没错
* */
Iterator iterator = menuComponents.iterator();
while (iterator.hasNext()){
MenuComponent menuComponent = (MenuComponent) iterator.next();
menuComponent.print();
}
}
}
采用了递归之后,打印菜品就非常方便了,和Iterator配合使用可以很方便的打印MenuItem或者子菜单中的MenuItem,现在Waitress的代码变得非常简单:
public class Waitress {
MenuComponent allMenus;
public Waitress(MenuComponent allMenus){
this.allMenus = allMenus;
}
public void printMenu(){
allMenus.print();
}
public static void main(String[] args){
//创建所有的 Menu 对象
MenuComponent pancakeHouseMenu = new Menu("Pancake House Menu","Breakfast");
MenuComponent dinnerMenu = new Menu("Dinner Menu","Lunch");
MenuComponent cafeMenu = new Menu("Cafe Menu","Dinner");
MenuComponent dessertMenu = new Menu("Dessert Menu","Dessert of course!");
//我们需要一个最顶层的菜单,它是allMenus
MenuComponent allMenus = new Menu("All Menus","Include all menus");
//这里的add()方法是composite的add()方法,即Menu类的add()方法,它将所有Menu都添加到顶层菜单allMenus的容器中
allMenus.add(pancakeHouseMenu);
allMenus.add(dinnerMenu);
allMenus.add(cafeMenu);
//给dinnerMenu添加一个菜单项 Pasta
dinnerMenu.add(new MenuItem("Pasta","Spaghetti with Marinara Sauce , and a slice of sourdough bread",true,3.89));
//给dinnerMenu添加一个子菜单
dinnerMenu.add(dessertMenu);
//给子菜单dessertMenu添加一个菜单项 Apple Pie
dessertMenu.add(new MenuItem("Apple Pie","Apple Pie with a flakey crust",true,1.59));
//构建服务员对象,让她打印菜品信息
Waitress waitress = new Waitress(allMenus);
waitress.printMenu();
}
}
整个项目运转起来之后,菜单组合的类图如下:
也许这里会有一些疑问,之前有个说法是“一个类,一个责任”,组合模式却是一个类有两个责任。组合模式不但要管理层次结构,还要执行菜单的操作。
实际上,组合模式用单一责任设计原则换取了透明性。通过让组件的接口同时包含一些管理子节点和叶节点的操作,客户就可以将composite和leaf一视同仁,一个元素究竟是composite还是leaf,对于客户是透明的。
MenuComponent类中同时具有两种类型的操作。因为客户有机会对一个元素做一些不恰当或者无意义的操作(例如试图将菜单添加到菜单项中),所以我们失去了一些“安全性”。这是设计上的抉择;我们也可以采用另一种设计:将责任区分开来放在不同的接口中,这样一来,设计上就比较安全,但我们也因此失去了透明性,客户的代码将必须用条件语句和instanceof操作符处理不同类型的节点。所以这是一个典型的折衷案例。
尽管我们收到设计原则的指导,但是我们总是需要观察某原则对我们的设计所造成的影响。有时候我们会故意做一些看似违反原则的事情。然而,在某些例子中,这是观点的问题;比方说,让管理孩子的操作(例如add()、remove()、getChild())出现在叶节点中,似乎很不恰当,但是换个视角来看,你可以把叶节点视为没有孩子的节点。