组合模式是什么?

组合模式(Composite Pattern)是一种结构型的设计模式,它允许我们将对象组成树状结构来表现“整体-部分”的层级结构。组合能让客户端以一致的方式处理个体对象与对象组合。
树状结构是一种常见的数据结构,例如:文件系统、系统菜单等。文件系统由目录和文件组成,目录的内容可以是文件,也可以是目录。组合模式让我们在处理这类层级的数据结构时,模糊了简单元素与复杂元素的差异,客户端可以像处理简单元素那样来处理复杂元素。

UML 类图

用 UML 类图来描述组合模式的结构,在模式中各个角色之间的关系:
image.png
根据上图,总结了模式中各个角色的职责以及它们之间的关系:

  • 部件抽象了树状结构中的简单元素(叶节点)和复杂元素(组合),描述了它们的共同操作。
  • 叶节点是树状结构中的基本元素,它不包含任何子节点。
  • 组合也称为容器,它包含了一组叶节点或其他组合,但它不知道这些子元素的具体类型,它只通过抽象的部件与子元素交互。组合接收到操作请求时,会将工作分配给所有的子元素,聚合它们的计算结果,再将最终的结果返回给客户端。
  • 客户端通过部件与任意的元素交互,因此它能够以相同的方式操作树状结构中的简单元素和复杂元素。

案例

让我们通过一个案例来帮助我们进一步理解组合模式。我们来模拟实现 tree 命令,tree 命令的功能是以树状的形式显示指定目录的内容,例如:

  1. $ tree BOOK/
  2. BOOK/
  3. ├── COMPUTER
  4. ├── Effecitve\ Java.pdf
  5. └── Introduction\ to\ Algorithms.pdf
  6. └── FICTION
  7. ├── Magic\ Novel
  8. ├── A\ Song\ of\ Ice\ and\ Fire.epub
  9. └── Harry\ Potter.epub
  10. └── One\ Hundred\ Years\ of\ Solitude.txt

首先,我们需要定义一个抽象的文件部件,以描述所有部件的共同操作:tree。

  1. public abstract class AbstractFile {
  2. protected String name;
  3. // constructor, getter and setter
  4. /**
  5. * list contents of directories in a tree-like format.
  6. */
  7. public final void tree() {
  8. tree(0);
  9. }
  10. protected void tree(int deep) {
  11. StringBuilder lineBuilder = new StringBuilder();
  12. if (deep > 0) {
  13. for (int i = 0; i < deep - 1; i++) {
  14. lineBuilder.append(" ");
  15. }
  16. lineBuilder.append("└── ");
  17. }
  18. lineBuilder.append(this.name);
  19. System.out.println(lineBuilder.toString());
  20. };
  21. }

普通文件(即叶节点)扩展了抽象文件,同时可定义自己独有的操作,如打开、保存等。

  1. public class File extends AbstractFile {
  2. // constructor
  3. public byte[] open() throws IOException {
  4. // Our code for opening the file
  5. }
  6. public void save(byte[] data) throws IOException {
  7. // Our code for saving the file
  8. }
  9. // other file operations
  10. }

目录(组合节点)也是扩展抽象文件,在 tree 方法会遍历所有的子元素,将任务分发给它们。

  1. public class Directory extends AbstractFile {
  2. private List<AbstractFile> files = new ArrayList<>();
  3. // constructor
  4. public void add(AbstractFile file) {
  5. files.add(file);
  6. }
  7. public void remove(AbstractFile file) {
  8. files.remove(file);
  9. }
  10. @Override
  11. protected void tree(int deep) {
  12. super.tree(deep);
  13. files.forEach(files -> files.tree(deep + 1));
  14. }
  15. }

最后我们通过一个用例来模拟 tree 命令的功能:

  1. public class CompositeMain {
  2. public static void main(String[] args) {
  3. Directory book = new Directory("BOOK");
  4. Directory computer = new Directory("COMPUTER");
  5. Directory fiction = new Directory("FICTION");
  6. Directory magicNovel = new Directory("Magic Novel");
  7. File effectiveJava = new File("Effective Java.pdf");
  8. File introductionToAlgorithms = new File("Introduction to Algorithms.pdf");
  9. File oneHundredYearsOfSolitude = new File("One Hundred Years of Solitude.txt");
  10. File aSongOfIceAndFire = new File("A Song of Ice and Fire.epub");
  11. File harryPotter = new File("Harry Potter.epub");
  12. book.add(computer);
  13. book.add(fiction);
  14. computer.add(introductionToAlgorithms);
  15. computer.add(effectiveJava);
  16. fiction.add(magicNovel);
  17. fiction.add(oneHundredYearsOfSolitude);
  18. magicNovel.add(aSongOfIceAndFire);
  19. magicNovel.add(harryPotter);
  20. book.tree();
  21. }
  22. }

案例源码

可在 GitHub 上查看上述案例的完整代码。

参考资料

本文参考的资料如下: