一、组合模式(Composite Pattern)

1、模式概述

组合模式 是一种 结构型设计模式,它将对象组合成 树形结构 以表示“部分-整体”的层次结构。组合模式使得用户对 单个对象(叶子)组合对象(容器) 的使用具有一致性。

核心思想“树形结构,统一接口 —— 无论是容器还是内容,操作方式一样。”

组合模式 = 树形结构 + 递归遍历

  • 它不是“简单的包含”,而是“层级的嵌套”,让系统能够以统一的方式处理个体与群体。

2、使用场景

组合模式适用于以下情况:

  • 你想表示 对象的部分-整体层次结构
  • 你希望用户 忽略组合对象与单个对象的不同,统一地使用它们;
  • 需要构建 树形结构,且结构可能动态变化。

关键特征

  • 递归结构:树枝节点可以包含其他树枝或叶子;

  • 透明性:客户端无需关心当前操作的是容器还是原子对象。

典型应用场景:

  • 文件系统(文件夹包含文件夹或文件);
  • 组织架构(部门包含部门或员工);
  • 菜单系统(菜单包含子菜单或菜单项)。

在实际开发中:

  • Java AWT/SwingContainerComponent 是组合模式的经典应用;
  • JSON/XML 解析器常使用组合模式构建文档对象模型(DOM);
  • 电商系统的商品分类(一级分类、二级分类、商品)也常采用此模式。

3、示例程序:文件系统树

  • Component 是统一入口
  • Leaf 和 Composite 实现同一接口,但行为不同
  • Composite 负责递归调用子节点的操作
角色 职责 示例
Component(抽象构件) 定义所有节点(叶子和容器)的公共接口 FileSystemNode
Leaf(叶子) 表示不可再分的原子对象,没有子节点 File
Composite(容器) 表示可包含子节点的复合对象,管理子节点集合 Directory
Client(客户端) 通过 Component 接口操作树形结构 Main
                ┌───────────────────┐
                │  FileSystemNode   │<──────────┐
                │-------------------│           │
                │ + name: String    │           │
                │-------------------│           │
                │ + display(depth)  │           │
                │ + getSize(): long │           │
                └─────────▲─────────┘           │
                          │                     │
        ┌─────────────────┴─────────────────┐   │
        │                                   │   │
┌───────▼───────┐               ┌──────────▼───────────┐
│     File      │               │      Directory       │
│---------------│               │----------------------│
│ - size: long  │               │ - children: List<>   │
│---------------│               │----------------------│
│ + display()   │               │ + add(node)          │
│ + getSize()   │               │ + remove(node)       │
└───────────────┘               │ + getChildren()      │
                                │ + display()          │
                                │ + getSize()          │
                                └──────────────────────┘

1)抽象构件 FileSystemNode

/**
 * 抽象构件(Component)
 * 定义文件系统中所有节点的公共行为
 */
public abstract class FileSystemNode {
    protected String name;

    public FileSystemNode(String name) {
        this.name = name;
    }

    // 获取名称
    public String getName() {
        return name;
    }

    // 显示节点信息(由子类实现)
    public abstract void display(int depth);

    // 获取大小(字节),叶子返回实际大小,容器返回子节点总和
    public abstract long getSize();
}

2)叶子节点 File

/**
 * 叶子(Leaf)
 * 表示一个具体的文件,不可再分
 */
public class File extends FileSystemNode {
    private long size; // 文件大小(字节)

    public File(String name, long size) {
        super(name);
        this.size = size;
    }

    @Override
    public void display(int depth) {
        System.out.println("  ".repeat(depth) + "📄 " + name + " (" + size + " bytes)");
    }

    @Override
    public long getSize() {
        return size;
    }
}

3)容器节点 Directory

/**
 * 容器(Composite)
 * 表示一个目录,可包含文件或其他目录
 */
import java.util.ArrayList;
import java.util.List;

public class Directory extends FileSystemNode {
    
    private final List<FileSystemNode> children = new ArrayList<>();

    public Directory(String name) {
        super(name);
    }

    // 添加子节点
    public void add(FileSystemNode node) {
        children.add(node);
    }

    // 移除子节点
    public void remove(FileSystemNode node) {
        children.remove(node);
    }

    // 获取子节点列表(可选)
    public List<FileSystemNode> getChildren() {
        return new ArrayList<>(children); // 返回副本,保证封装性
    }

    @Override
    public void display(int depth) {
        // 打印当前目录
        System.out.println("  ".repeat(depth) + "📁 " + name + "/");
        // 递归打印所有子节点
        for (FileSystemNode child : children) {
            child.display(depth + 1);
        }
    }

    @Override
    public long getSize() {
        // 目录大小 = 所有子节点大小之和
        return children.stream().mapToLong(FileSystemNode::getSize).sum();
    }
}

4)客户端测试 Main

  • 客户端只通过 FileSystemNode 接口操作;
  • display()getSize()FileDirectory 中行为不同,但调用方式一致;
  • 树结构可无限嵌套(如 docs 下还可加子目录)。
/**
 * 客户端(Client)
 * 构建文件系统树并操作
 */
public class Main {
    public static void main(String[] args) {
        // 创建根目录
        Directory root = new Directory("root");

        // 创建子目录
        Directory docs = new Directory("docs");
        Directory pics = new Directory("pics");

        // 创建文件(叶子)
        File readme = new File("README.md", 1024);
        File report = new File("report.pdf", 2048);
        File photo1 = new File("vacation.jpg", 4096);
        File photo2 = new File("family.png", 3072);

        // 组装树形结构
        docs.add(readme);
        docs.add(report);
        pics.add(photo1);
        pics.add(photo2);

        root.add(docs);
        root.add(pics);

        // 统一操作:显示整个文件系统
        System.out.println("=== 文件系统结构 ===");
        root.display(0);

        // 统一操作:获取总大小
        System.out.println("\n总大小: " + root.getSize() + " bytes");
    }
}

输出:

=== 文件系统结构 ===
📁 root/
  📁 docs/
    📄 README.md (1024 bytes)
    📄 report.pdf (2048 bytes)
  📁 pics/
    📄 vacation.jpg (4096 bytes)
    📄 family.png (3072 bytes)

总大小: 10240 bytes

3、FQA

1)模式优点

  • 定义清晰的层次结构:树形结构直观明了;
  • 统一调用接口:客户端无需区分容器与叶子(如果使用透明式);
  • 扩展性强:增加新的树枝或叶子非常容易(符合开闭原则)。

2)注意事项

  • 设计复杂度:如果业务逻辑简单(无层级关系),使用组合模式属于过度设计;
  • 类型安全性:在透明式组合模式中,叶子节点的 add/remove 方法可能没有意义(需要抛出异常或空实现);
  • 遍历效率:树结构过深时,递归遍历可能导致栈溢出。