前言 如果应用的核心模型能用树状结构表示, 在应用中使用组合模式才有价值. 例如: 你有两类对象: 产品
和 盒子
,一个盒子中可以包含多个 产品或者几个较小的 盒子 . 这些小 盒子中同样可以包含一些 产品或更小的 盒子 , 以此类推.
假设你希望在这些类的基础上开发一个定购系统. 订单中可以包含无包装的简单产品, 也可以包含装满产品的盒子……以及其他盒子. 此时你会如何计算每张订单的总价格呢?
订单中可能包括各种产品,这些产品放置在盒子中,然后又被放入一层又一层更大的盒子中.整个结构看上去像一颗树.
你可以尝试直接计算: 打开所有盒子, 找到每件产品, 然后计算总价. 这在真实世界中或许可行, 但在程序中, 你并不能简单地使用循环语句来完成该工作. 你必须事先知道所有 产品和 盒子的类别, 所有盒子的嵌套层数以及其他繁杂的细节信息. 因此, 直接计算极不方便, 甚至完全不可行.
解决方案 组合模式建议使用一个通用接口来与产品
和盒子
进行交互, 并且在该接口中声明一个计算总价的方法.
那么方法该如何设计呢? 对于一个产品, 该方法直接返回其价格; 对于一个盒子, 该方法遍历盒子中的所有项目, 询问每个项目的价格, 然后返回该盒子的总价格. 如果其中某个项目是小一号的盒子, 那么当前盒子也会遍历其中的所有项目, 以此类推, 直到计算出所有内部组成部分的价格. 你甚至可以在盒子的最终价格中增加额外费用, 作为该盒子的包装费用.
组合模式以递归的方式处理对象树中的所有项目. 该方式的最大优点在于你无需了解构成树状结构的对象的具体类. 你也无需了解对象是简单的产品还是复杂的盒子. 你只需调用通用接口以相同的方式对其进行处理即可. 当你调用该方法后, 对象会将请求沿着树结构传递下去.
示例代码
Entry.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 package com.edu.tju.GOF.Composite;public abstract class Entry { public abstract String getName () ; public abstract int getSize () ; public Entry add (Entry entry) throws FileTreatmentException { throw new FileTreatmentException(); } public void printList () { printList("" ); } protected abstract void printList (String prefix) ; public String toString () { return getName() + " (" + getSize() + ")" ; } }
File.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 package com.edu.tju.GOF.Composite;public class File extends Entry { private String name; private int size; public File (String name, int size) { this .name = name; this .size = size; } public String getName () { return name; } public int getSize () { return size; } protected void printList (String prefix) { System.out.println(prefix + "/" + this ); } }
Directory.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 package com.edu.tju.GOF.Composite;import java.util.ArrayList;import java.util.Iterator;public class Directory extends Entry { private String name; private ArrayList directory = new ArrayList(); public Directory (String name) { this .name = name; } public String getName () { return name; } public int getSize () { int size = 0 ; Iterator it = directory.iterator(); while (it.hasNext()) { Entry entry = (Entry) it.next(); size += entry.getSize(); } return size; } public Entry add (Entry entry) { directory.add(entry); return this ; } protected void printList (String prefix) { System.out.println(prefix + "/" + this ); Iterator it = directory.iterator(); while (it.hasNext()) { Entry entry = (Entry) it.next(); entry.printList(prefix + "/" + name); } } }
异常类 FileTreatmentException.java
1 2 3 4 5 6 7 8 9 10 package com.edu.tju.GOF.Composite;public class FileTreatmentException extends RuntimeException { public FileTreatmentException () { } public FileTreatmentException (String msg) { super (msg); } }
Main.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 package com.edu.tju.GOF.Composite;public class Main { public static void main (String[] args) { Directory root = new Directory("根目录" ); Directory life = new Directory("我的生活" ); File eat = new File("吃火锅" , 100 ); File sleep = new File("睡觉" , 100 ); File study = new File("学习" , 100 ); life.add(eat); life.add(sleep); life.add(study); Directory work = new Directory("我的工作" ); File write = new File("写博客" , 200 ); File paper = new File("写论文" , 200 ); File homework = new File("写家庭作业" , 200 ); work.add(write); work.add(paper); work.add(homework); Directory relax = new Directory("我的休闲" ); File music = new File("音乐" , 200 ); File wz = new File("王者荣耀" , 500 ); File cj = new File("和平精英" , 300 ); relax.add(music); relax.add(wz); relax.add(cj); Directory read = new Directory("我的阅读" ); File book = new File("学习书籍" , 200 ); File novel = new File("娱乐小说" , 200 ); read.add(book); read.add(novel); root.add(life); root.add(work); root.add(relax); root.add(read); root.printList("D:" ); System.out.println("=================" ); work.printList("work" ); System.out.println("=================" ); novel.printList("novel" ); } }
组合模式的角色
组件 (Component) 接口描述了树中简单项目和复杂项目所共有的操作.
叶节点 (Leaf) 是树的基本结构, 它不包含子项目. 一般情况下, 叶节点最终会完成大部分的实际工作, 因为它们无法将工作指派给其他部分.
容器 (Container)——又名 “组合 (Composite)”——是包含叶节点或其他容器等子项目的单位. 容器不知道其子项目所属的具体类, 它只通过通用的组件接口与其子项目交互. 容器接收到请求后会将工作分配给自己的子项目, 处理中间结果, 然后将最终结果返回给客户端.
客户端 (Client) 通过组件接口与所有项目交互. 因此, 客户端能以相同方式与树状结构中的简单或复杂项目交互.
适用场景
如果你需要实现树状对象结构, 可以使用组合模式. 组合模式为你提供了两种共享公共接口的基本元素类型: 简单叶节点和复杂容器. 容器中可以包含叶节点和其他容器. 这使得你可以构建树状嵌套递归对象结构.
如果你希望客户端代码以相同方式处理简单和复杂元素, 可以使用该模式. 组合模式中定义的所有元素共用同一个接口. 在这一接口的帮助下, 客户端不必在意其所使用的对象的具体类.
优缺点
优点
你可以利用多态和递归机制更方便地使用复杂树结构.
开闭原则. 无需更改现有代码, 你就可以在应用中添加新元素, 使其成为对象树的一部分.
缺点
开闭原则. 无需更改现有代码, 你就可以在应用中添加新元素, 使其成为对象树的一部分.
与其他模式的关系
桥接模式、 状态模式和策略模式 (在某种程度上包括适配器模式) 模式的接口非常相似. 实际上, 它们都基于组合模式——即将工作委派给其他对象, 不过也各自解决了不同的问题. 模式并不只是以特定方式组织代码的配方, 你还可以使用它们来和其他开发者讨论模式所解决的问题.
可以使用迭代器模式来遍历组合树.
可以使用访问者模式对整个组合树执行操作.
可以使用享元模式实现组合树的共享叶节点以节省内存.