Skip to content

组合模式(Composite Pattern) ​

一句话记忆口诀:组合模式统一叶子和容器的接口,递归处理树形结构,文件系统和菜单树是最经典的例子。

1. 🏠 生活类比 ​

公司组织架构:公司有多个部门,每个部门有多个员工。计算"薪资总额"时:

  • 员工的薪资 = 自己的工资
  • 部门的薪资 = 所有子部门薪资之和 + 所有员工薪资之和
  • 公司的薪资 = 所有部门薪资之和

无论你拿到的是"员工"还是"部门",都可以统一调用 getSalary() 方法。

2. 💩 烂代码:到处都是 instanceof ​

java
// ❌ 反例:客户端必须区分叶子节点和容器节点
public long calculateSize(Object node) {
    if (node instanceof File) {
        return ((File) node).getSize();
    } else if (node instanceof Directory) {
        long total = 0;
        for (Object child : ((Directory) node).getChildren()) {
            total += calculateSize(child); // 递归
        }
        return total;
    }
    throw new IllegalArgumentException("未知节点类型");
}

// 每次新增节点类型(如软链接),都要修改这个方法!
// 到处都是 instanceof 判断,代码难以维护

问题根因:叶子节点和容器节点接口不统一,客户端必须区分处理,违反开闭原则。

3. ✨ 组合模式方案 ​

4. 💻 完整代码实现 ​

示例中每个 public 顶层类应放入同名的 .java 文件(FileSystemNode.java、FileNode.java、DirectoryNode.java、Main.java),不要整体保存为单个源文件;拆分后各文件需自行添加所需的 import(如 FileSystemNode.java、DirectoryNode.java 需 import java.util.*;)。同时,当前实现假定调用方传入的是合法的树形结构,DirectoryNode 未做防环校验,把目录自身或祖先加入子节点会导致无限递归。

java
import java.util.*;

// ===== 抽象组件:统一叶子和容器的接口 =====
public abstract class FileSystemNode {
    protected final String name;

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

    public abstract long getSize();
    public abstract void print(String prefix);

    // 默认实现抛异常,叶子节点不需要这些操作
    public void add(FileSystemNode node) {
        throw new UnsupportedOperationException("不支持添加子节点");
    }

    public void remove(FileSystemNode node) {
        throw new UnsupportedOperationException("不支持删除子节点");
    }

    public List<FileSystemNode> getChildren() {
        throw new UnsupportedOperationException("不支持获取子节点");
    }
}

// ===== 叶子节点:文件 =====
public class FileNode extends FileSystemNode {
    private final long size;

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

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

    @Override
    public void print(String prefix) {
        System.out.println(prefix + "📄 " + name + " (" + formatSize(size) + ")");
    }

    private String formatSize(long bytes) {
        if (bytes < 1024) return bytes + "B";
        if (bytes < 1024 * 1024) return (bytes / 1024) + "KB";
        return (bytes / 1024 / 1024) + "MB";
    }
}

// ===== 容器节点:目录 =====
public class DirectoryNode extends FileSystemNode {
    private final List<FileSystemNode> children = new ArrayList<>();

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

    @Override
    public void add(FileSystemNode node) {
        children.add(node);
    }

    @Override
    public void remove(FileSystemNode node) {
        children.remove(node);
    }

    @Override
    public List<FileSystemNode> getChildren() {
        return Collections.unmodifiableList(children);
    }

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

    @Override
    public void print(String prefix) {
        System.out.println(prefix + "📁 " + name + " (" + formatSize(getSize()) + ")");
        children.forEach(child -> child.print(prefix + "  "));
    }

    private String formatSize(long bytes) {
        if (bytes < 1024) return bytes + "B";
        if (bytes < 1024 * 1024) return (bytes / 1024) + "KB";
        return (bytes / 1024 / 1024) + "MB";
    }
}

// ===== 使用示例 =====
public class Main {
    public static void main(String[] args) {
        // 构建文件系统树
        DirectoryNode root = new DirectoryNode("project");

        DirectoryNode src = new DirectoryNode("src");
        src.add(new FileNode("Main.java", 2048));
        src.add(new FileNode("Utils.java", 1024));

        DirectoryNode test = new DirectoryNode("test");
        test.add(new FileNode("MainTest.java", 1536));

        root.add(src);
        root.add(test);
        root.add(new FileNode("README.md", 512));
        root.add(new FileNode("pom.xml", 3072));

        // 统一接口,无需区分文件和目录
        root.print("");
        System.out.println("总大小: " + root.getSize() + "B");

        // 输出:
        // 📁 project (8192B)
        //   📁 src (3072B)
        //     📄 Main.java (2KB)
        //     📄 Utils.java (1KB)
        //   📁 test (1536B)
        //     📄 MainTest.java (1KB)
        //   📄 README.md (512B)
        //   📄 pom.xml (3KB)
    }
}

4.1 进阶示例:组织架构薪资计算 ​

薪资以分为单位的 long 存储,避免 double 的二进制浮点精度误差;金额类型属于业务领域的设计选择,并非组合模式的必要组成部分。

java
// ===== 抽象组件 =====
public abstract class OrganizationComponent {
    protected String name;
    protected String title;

    public OrganizationComponent(String name, String title) {
        this.name = name;
        this.title = title;
    }

    public abstract long calculateSalary();
    public abstract int headcount();

    public void add(OrganizationComponent component) {
        throw new UnsupportedOperationException();
    }
}

// ===== 叶子:员工 =====
public class Employee extends OrganizationComponent {
    private long salary; // 以分为单位,避免浮点精度误差

    public Employee(String name, String title, long salary) {
        super(name, title);
        this.salary = salary;
    }

    @Override
    public long calculateSalary() { return salary; }

    @Override
    public int headcount() { return 1; }
}

// ===== 容器:部门 =====
public class Department extends OrganizationComponent {
    private List<OrganizationComponent> members = new ArrayList<>();

    public Department(String name, String title) {
        super(name, title);
    }

    @Override
    public void add(OrganizationComponent component) {
        members.add(component);
    }

    @Override
    public long calculateSalary() {
        return members.stream()
                .mapToLong(OrganizationComponent::calculateSalary)
                .sum();
    }

    @Override
    public int headcount() {
        return members.stream()
                .mapToInt(OrganizationComponent::headcount)
                .sum();
    }
}

// ===== 使用 =====
Department company = new Department("科技公司", "公司");

Department techDept = new Department("技术部", "部门");
techDept.add(new Employee("张三", "Java 工程师", 2500000));
techDept.add(new Employee("李四", "前端工程师", 2000000));

Department salesDept = new Department("销售部", "部门");
salesDept.add(new Employee("王五", "销售经理", 3000000));

company.add(techDept);
company.add(salesDept);

System.out.println("公司总人数: " + company.headcount());     // 3
System.out.println("公司月薪总额: " + company.calculateSalary()); // 7500000 分 = 75000 元

5. 🔧 框架应用 ​

框架/类说明
java.awt.Container / ComponentSwing 组件树:Container 可以包含 Component(包括其他 Container)
javax.swing.JMenu / JMenuItem菜单树:JMenu 可以包含 JMenuItem 或其他 JMenu
MyBatis SqlNode动态 SQL 的 <if>、<choose>、<foreach> 组成树形结构
Jackson JsonNodeObjectNode 包含子节点,ValueNode 是叶子
Spring CompositeCacheManager统一 CacheManager 接口,组合多个委托的缓存管理器
HTML DOMDocument → Element → Text Node 树形结构

6. ⚠️ 适用场景 ​

适合:

  • 需要表示部分-整体的树形结构
  • 希望客户端统一处理单个对象和组合对象
  • 文件系统、菜单、组织架构、UI 组件树

不适合:

  • 不是树形结构
  • 叶子和容器的行为差异很大,统一接口没有意义

6.1 安全组合 vs 透明组合 ​

方式add/remove 放在哪优点缺点
透明组合抽象组件中客户端统一处理叶子节点被迫实现无意义方法
安全组合只在容器中接口干净客户端需要区分类型

JDK AWT 属于安全组合:添加子组件的 add() 方法定义在 java.awt.Container 中,而非共同抽象 java.awt.Component 上,叶子节点(如 Label、Button)不暴露子节点管理操作。

Released under the MIT License.