定义:
将对象组合成树形结构来表示“部分-整体”的层次结构。组合模式使得客户能以一致的方式处理个别对象和组合对象。
设计类图:
组合模式中的角色:
- Component抽象组件:为组合中所有对象提供一个接口,不管是叶子对象还是组合对象。
- Composite组合节点对象:实现了Component的所有操作,并且持有子节点对象。
- Leaf叶节点对象:叶节点对象没有任何子节点,实现了Component中的某些操作。
组合模式让我们能用树形方式创建对象的结构,树里面包含了组合以及个别的对象。
使用组合结构,我们能把相同的操作应用在组合和个别对象上。换句活说,在大多数情况下,我们可以忽略对象组合和个别对象之问的差别。
示例代码:
public abstract class Component {
protected String name;
public Component(String name) {
this.name = name;
}
public abstract void operation();
public void add(Component c) {
throw new UnsupportedOperationException();
}
public void remove(Component c) {
throw new UnsupportedOperationException();
}
public Component getChild(int i) {
throw new UnsupportedOperationException();
}
public List<Component> getChildren() {
return null;
}
}
public class Composite extends Component {
private List<Component> components = new ArrayList<>();
public Composite(String name) {
super(name);
}
@Override
public void operation() {
System.out.println("组合节点"+name+"的操作");
//调用所有子节点的操作
for (Component component : components) {
component.operation();
}
}
@Override
public void add(Component c) {
components.add(c);
}
@Override
public void remove(Component c) {
components.remove(c);
}
@Override
public Component getChild(int i) {
return components.get(i);
}
@Override
public List<Component> getChildren() {
return components;
}
}
public class Leaf extends Component {
public Leaf(String name) {
super(name);
}
@Override
public void operation() {
System.out.println("叶节点"+name+"的操作");
}
}
public class Client {
public static void main(String[] args) {
//创建根节点对象
Component component = new Composite("component");
//创建两个组合节点对象
Component composite1 = new Composite("composite1");
Component composite2 = new Composite("composite2");
//将两个组合节点对象添加到根节点
component.add(composite1);
component.add(composite2);
//给第一个组合节点对象添加两个叶子节点
Component leaf1 = new Leaf("leaf1");
Component leaf2 = new Leaf("leaf2");
composite1.add(leaf1);
composite1.add(leaf2);
//给第二个组合节点对象添加一个叶子节点和一个组合节点
Component leaf3 = new Leaf("leaf3");
Component composite3 = new Composite("composite3");
composite2.add(leaf3);
composite2.add(composite3);
//给第二个组合节点下面的组合节点添加两个叶子节点
Component leaf4 = new Leaf("leaf4");
Component leaf5 = new Leaf("leaf5");
composite3.add(leaf4);
composite3.add(leaf5);
//执行所有节点的操作
component.operation();
}
}
输出结果:
上述代码中,在组合节点对象Composite的operation()方法中除了执行自身的操作外,还调用了子节点的operation()方法,这样使得客户端可以透明的遍历所有的节点对象的操作,而不用关心操作的是叶子节点还是组合节点,将它们一视同仁。这看上去有点像二叉树的遍历,不过这里并不是二叉树,每个组合节点可以有若干个子节点,而这些子节点,如果是组合节点,则可以继续拥有子节点,如果是叶子节点,那么就终止了。
叶子节点和组合节点可以有相同的操作,如上面代码中的operation()方法,但是叶子节点不具备add、remove以及getChild操作,如果你试图在叶子节点上调用这些方法就会抛出不支持的异常。组合节点可以添加子节点,因此组合节点实现了add、remove以及getChild等操作。组合节点持有一个节点的集合,在组合节点的operation()方法中通过遍历调用持有节点的operation()方法,就像是在递归遍历一样。通过这种方式Client客户端可以透明的访问节点对象,你可以在客户端中调用一个组合节点的operation()方法,也可以调用一个叶子节点的operation()方法,也就是说你根本不需要关心调用的是组合节点还是叶子节点,它们都可以进行相同的操作。
菜单的例子
服务员需要打印菜单,如菜单的名称和价格,但是菜单既可以有子菜单组合,也可以有子菜单项,对于子菜单组合,它的下面又可能会有子菜单项,如饮料菜单和甜点菜单等会包含很多东西,而子菜单项就是一个具体的菜名,不会有子菜单了。现在要打印所有的菜单描述,我们用组合模式来实现这个功能:
实现代码:
/**
* 抽象菜单组件
*/
public abstract class MenuComponent {
public void add(MenuComponent menu) {
throw new UnsupportedOperationException();
}
public void remove(MenuComponent menu) {
throw new UnsupportedOperationException();
}
public MenuComponent getChild(int i) {
throw new UnsupportedOperationException();
}
public String getName() {
throw new UnsupportedOperationException();
}
public double getPrice() {
throw new UnsupportedOperationException();
}
public abstract void print();
}
/**
* 菜单组件
* 菜单组件有菜单名和子菜单,但没有价格,支持添加、删除和打印等操作
*/
public class Menu extends MenuComponent {
private List<MenuComponent> menuList = new ArrayList<>();
private String name;
public Menu(String name) {
this.name = name;
}
@Override
public void add(MenuComponent menu) {
menuList.add(menu);
}
@Override
public void remove(MenuComponent menu) {
menuList.remove(menu);
}
@Override
public MenuComponent getChild(int i) {
return menuList.get(i);
}
@Override
public String getName() {
return name;
}
@Override
public void print() {
System.out.println("--------");
System.out.println(getName());
//打印所有子菜单
for (MenuComponent menu : menuList) {
menu.print();
}
System.out.println("--------");
}
}
/**
* 菜单项
* 菜单项拥有名称和价格,可以打印,但是不支持添加、删除等操作
*/
public class MenuItem extends MenuComponent {
private String name;
private double price;
public MenuItem(String name, double price) {
this.name = name;
this.price = price;
}
@Override
public String getName() {
return name;
}
@Override
public double getPrice() {
return price;
}
@Override
public void print() {
System.out.println(getName() + " -- " + getPrice());
}
}
public class Client {
public static void main(String[] args) {
Menu menu = new Menu("所有菜单");
Menu menu1 = new Menu("子菜单1");
Menu menu2 = new Menu("子菜单2");
Menu menu3 = new Menu("子菜单3");
//给所有菜单添加三个子菜单
menu.add(menu1);
menu.add(menu2);
menu.add(menu3);
//给第二个菜单添加一个菜单项和一个子菜单
menu2.add(new MenuItem("子菜单2--菜单项", 10.0));
Menu menu4 = new Menu("子菜单2--子菜单");
menu2.add(menu4);
menu4.add(new MenuItem("子菜单2--子菜单--菜单项", 20.0));
//打印所有菜单
menu.print();
}
}
打印结果:
在抽象菜单MenuComponent组件中,我们将一些操作默认抛出UnsupportedOperationException异常,如果子类支持该操作就重写实现该操作,如果子类不支持该操作,就不用管它。使用组合模式,打印菜单变得非常容易,而且更好的一点是,你现在可以拿任何一个子菜单来打印结果,而不用管它是具体的菜单项还是里面又包含了子菜单。如果不用组合模式,很难想象有一种方法能很方便的将所有的菜单打印出来。
菜单例子中,既要管理层次结构,又要执行打印操作,是否破坏了单一职责?
严格来说,是的。我们可以这么说,组合模式以单一职责换取透明性。 什么是透明性?通过让组件的接口同时包含一些管理子节点和叶节点的操作,客户就可以将组合和叶节点 一视同仁。也就是说一个元素究竟是组合还是叶节点,对客户是透明的。
现在,我们在 MenuComponent 类中同时具有两种类型的操作. 因为客户有机会对一个元素做一些没有意义的操作(例如试图把菜单添加到菜单项),所以我们失去 一些‘安会性”。这是设计上的抉择;我们当然也可以采用另一种方向的设计,将责任区分开来放在不同的接口中。这么一来,设计上就比较安全,但我们也因此失去了透明性,客户的代码将必须使用条件语句和 instanceof 操作符处理不同类型的节点。
所以, 这是一个很典型的折衷案例。尽管我们受到设计原则的指导,但是,我们总是需要观察某原则对我们的设计所造成的影响。有时候,我们会故意做一些看似违反原则的事情。然而,在某些例子中,这是观点的问题。比方说让管理孩子的操作(例如 add ( )、 remove( )、 getchild ( ) )出现在叶节点中,似乎很不恰当,但是换个视角来看,你可以把叶布点视为没有孩子的节点。
组合模式的扩展
子节点可以指向父节点
组件可以有一个指向父节点的引用,以便在游走时更容易。而且,如果引用某个孩子,你想从树形结构中删除这个孩子,你会需要从父节点中去蒯除它。一旦孩子有了指向父亲的引用,这做起来就很容易。这样做也使得遍历操作可上可下,更加自由灵活。
使用缓存
有时候,如果这个组合结构很复杂,或者遍历的代价太高,那么实现组合节点的缓存就很有帮助。比方说,如果你要不断地遍历一个组合,而且它的每一个子节点都需要进行某些计算,那你就应该使用缓存来临时保存结果,从而省去遍历的开支。
组合模式应用场景
这个应用的地方也比较多,比如大多数系统的UI界面的导航菜单一般都是组合模式,再如Android里面的xml布局都是用的组合模式。在选择是否应用组合模式时,要考虑设计上的抉择,到底是要透明性更多一点,还是安全性更多一点,需要做一个平衡。
参考: