结构型设计模式-组合模式

3 阅读21分钟

概述

组合模式(Composite Pattern)是结构型设计模式中的经典之作,其核心定义是:将对象组合成树形结构以表示“部分-整体”的层次结构,使得客户端对单个对象和组合对象的使用具有一致性。这一模式通过抽象出统一的组件接口,让叶子节点与容器节点遵循相同的交互契约,从而将客户端代码从繁琐的类型判断中解放出来。

组合模式所解决的核心问题极具现实意义:在实际开发中,树形结构无处不在——文件系统、组织架构、菜单导航、GUI控件、表达式求值等等。如果客户端需要时刻通过 instanceof 区分叶子与容器,代码将充斥着重复的条件分支和递归逻辑,扩展性与可维护性急剧下降。组合模式通过引入 Component 抽象层,将“管理子节点”与“执行业务操作”两类职责统一声明,使客户端可以面向抽象编程,无需感知具体节点类型,递归处理被自然地封装在容器节点内部。

本文将从最原始的 instanceof 判断方式出发,逐步演进至透明组合模式与安全组合模式的双重构,再深入 JDK、Spring、MyBatis 等主流框架的源码级实现,并拓展至分布式环境下的元数据树、权限树、路由树等高级应用场景。文章还精心准备了五大典型场景的独立可运行 Demo、十余道专家级面试题,以及丰富的 Mermaid 图表辅助理解,力求为读者呈现一幅组合模式的完整知识图谱。


一、模式定义与结构

1.1 GoF 标准定义

Composite Pattern:Compose objects into tree structures to represent part-whole hierarchies. Composite lets clients treat individual objects and compositions of objects uniformly.

将对象组合成树形结构以表示“部分-整体”的层次结构。组合模式使得客户端对单个对象和组合对象的使用具有一致性。

1.2 UML 类图

classDiagram
    class Component {
        <<abstract>>
        +operation()
        +add(Component)
        +remove(Component)
        +getChild(int)
    }

    class Leaf {
        +operation()
        +add(Component)
        +remove(Component)
        +getChild(int)
    }

    class Composite {
        -children : List~Component~
        +operation()
        +add(Component)
        +remove(Component)
        +getChild(int)
    }

    Component <|-- Leaf
    Component <|-- Composite
    Composite o-- Component : children

1.3 类图说明与变体权衡

上述 UML 类图展示了透明组合模式的标准结构。抽象类 Component 统一声明了业务方法 operation() 以及管理子节点的方法 add()remove()getChild()Leaf 类代表叶子节点,它实现了 operation() 以执行具体业务逻辑,但对于子节点管理方法,通常采用空实现或抛出 UnsupportedOperationExceptionComposite 类代表容器节点,内部维护一个 List<Component> 集合,在其 operation() 方法中递归遍历所有子节点并调用它们的 operation(),同时提供对子节点的增删查管理。

组合模式在实际应用中有两种变体:透明组合模式安全组合模式,二者在设计上存在显著权衡。透明组合模式将管理子节点的方法置于 Component 接口中,使得客户端可以完全一致地对待叶子和容器——客户端代码无需类型判断,直接调用 add()remove()。但这一致性的代价是叶子节点必须实现这些它本不该支持的方法,从而引入了运行时异常的风险,违反了接口隔离原则。安全组合模式则采取了不同的策略:Component 接口只声明业务方法(如 operation()),而将管理子节点的方法单独下沉到 Composite 接口中。这样一来,叶子节点完全不必实现无关方法,类型安全性得到保障。然而,客户端在需要管理子节点时必须明确知道对象是 Composite 类型,失去了完全透明的操作能力。实际项目中,透明组合因其客户端代码简洁而更受欢迎,通常辅以文档说明或默认空实现来降低异常风险;安全组合则适用于对类型安全要求极高、且管理操作与业务操作调用者明显分离的场景。

1.4 角色职责详解

  • Component(抽象组件):声明树形结构中所有节点的公共接口。在透明组合中,它还声明了管理子节点的方法。它是客户端统一访问的入口,定义了叶子与容器的行为契约。
  • Leaf(叶子节点):表示树形结构中的末端节点,没有子节点。它实现业务方法的具体行为。对于管理子节点的方法,通常抛出异常或提供空实现以表明其不支持此类操作。
  • Composite(容器节点):内部持有子组件的集合(通常为 List<Component>)。它实现业务方法时,通过遍历子集合并委托调用子节点的业务方法,实现递归聚合。同时提供完整的子节点管理能力,负责维护树形结构的动态变化。

二、代码演进与实现

2.1 不使用模式的原始代码

在未应用组合模式时,我们通常需要构建简单的树形结构,比如一个公司组织架构。假设有 Employee(普通员工,叶子)和 Department(部门,容器)两个类,它们没有统一的父接口。客户端在计算总薪资或打印组织图时,必须先用 instanceof 判断节点类型,再分别处理。

import java.util.ArrayList;
import java.util.List;

// 叶子节点:普通员工
class Employee {
    private String name;
    private double salary;

    public Employee(String name, double salary) {
        this.name = name;
        this.salary = salary;
    }

    public double getSalary() {
        return salary;
    }

    public String getName() {
        return name;
    }
}

// 容器节点:部门
class Department {
    private String name;
    private List<Employee> employees = new ArrayList<>();
    private List<Department> subDepartments = new ArrayList<>();

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

    public void addEmployee(Employee e) {
        employees.add(e);
    }

    public void addSubDepartment(Department d) {
        subDepartments.add(d);
    }

    public List<Employee> getEmployees() {
        return employees;
    }

    public List<Department> getSubDepartments() {
        return subDepartments;
    }

    public String getName() {
        return name;
    }
}

// 客户端代码:充斥着instanceof判断与重复的递归逻辑
public class ClientWithoutPattern {
    public static double calculateTotalSalary(Object node) {
        if (node instanceof Employee) {
            return ((Employee) node).getSalary();
        } else if (node instanceof Department) {
            Department dept = (Department) node;
            double total = 0;
            // 累加本部门员工薪资
            for (Employee emp : dept.getEmployees()) {
                total += calculateTotalSalary(emp);
            }
            // 递归处理子部门
            for (Department sub : dept.getSubDepartments()) {
                total += calculateTotalSalary(sub);
            }
            return total;
        }
        return 0;
    }

    public static void printStructure(Object node, int level) {
        String indent = "  ".repeat(level);
        if (node instanceof Employee) {
            Employee emp = (Employee) node;
            System.out.println(indent + "- " + emp.getName() + " ($" + emp.getSalary() + ")");
        } else if (node instanceof Department) {
            Department dept = (Department) node;
            System.out.println(indent + "+ " + dept.getName());
            for (Employee emp : dept.getEmployees()) {
                printStructure(emp, level + 1);
            }
            for (Department sub : dept.getSubDepartments()) {
                printStructure(sub, level + 1);
            }
        }
    }

    public static void main(String[] args) {
        Department root = new Department("总公司");
        Department deptIT = new Department("IT部");
        deptIT.addEmployee(new Employee("张三", 8000));
        deptIT.addEmployee(new Employee("李四", 9000));
        Department deptHR = new Department("HR部");
        deptHR.addEmployee(new Employee("王五", 7000));
        root.addSubDepartment(deptIT);
        root.addSubDepartment(deptHR);
        root.addEmployee(new Employee("CEO", 50000));

        System.out.println("组织架构:");
        printStructure(root, 0);
        System.out.println("总薪资:" + calculateTotalSalary(root));
    }
}

问题分析:上述代码虽然能够运行,但问题重重。首先,calculateTotalSalaryprintStructure 方法内部都包含对 instanceof 的类型判断,且递归逻辑完全重复。其次,若新增一种节点类型(例如外包团队 OutsourceTeam),所有客户端代码都需要增加对应的 else if 分支,违背了开闭原则。最后,客户端必须理解叶子与容器的内部结构差异,导致代码耦合度高,难以复用。

2.2 透明组合模式重构

透明组合模式通过抽象 Component 类,将所有节点统一为一种类型。客户端仅依赖 Component,无需关心具体是 Leaf 还是 Composite

import java.util.ArrayList;
import java.util.List;

// 抽象组件:声明统一接口(包含管理子节点的方法)
abstract class Component {
    protected String name;

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

    public String getName() {
        return name;
    }

    // 业务方法
    public abstract double getSalary();

    public abstract void print(int level);

    // 管理子节点的方法(透明组合:叶子也要实现)
    public void add(Component c) {
        throw new UnsupportedOperationException("当前节点不支持添加子节点");
    }

    public void remove(Component c) {
        throw new UnsupportedOperationException("当前节点不支持移除子节点");
    }

    public Component getChild(int index) {
        throw new UnsupportedOperationException("当前节点不支持获取子节点");
    }
}

// 叶子节点:员工
class EmployeeLeaf extends Component {
    private double salary;

    public EmployeeLeaf(String name, double salary) {
        super(name);
        this.salary = salary;
    }

    @Override
    public double getSalary() {
        return salary;
    }

    @Override
    public void print(int level) {
        System.out.println("  ".repeat(level) + "- " + name + " ($" + salary + ")");
    }

    // 管理方法继承自父类,默认抛出异常,表示不支持
}

// 容器节点:部门
class DepartmentComposite extends Component {
    private List<Component> children = new ArrayList<>();

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

    @Override
    public double getSalary() {
        // 递归计算所有子节点的薪资总和
        double total = 0;
        for (Component child : children) {
            total += child.getSalary();
        }
        return total;
    }

    @Override
    public void print(int level) {
        System.out.println("  ".repeat(level) + "+ " + name);
        for (Component child : children) {
            child.print(level + 1);
        }
    }

    @Override
    public void add(Component c) {
        children.add(c);
    }

    @Override
    public void remove(Component c) {
        children.remove(c);
    }

    @Override
    public Component getChild(int index) {
        return children.get(index);
    }
}

// 客户端:面向Component编程,无需类型判断
public class TransparentCompositeDemo {
    public static void main(String[] args) {
        Component root = new DepartmentComposite("总公司");
        Component deptIT = new DepartmentComposite("IT部");
        deptIT.add(new EmployeeLeaf("张三", 8000));
        deptIT.add(new EmployeeLeaf("李四", 9000));

        Component deptHR = new DepartmentComposite("HR部");
        deptHR.add(new EmployeeLeaf("王五", 7000));

        root.add(deptIT);
        root.add(deptHR);
        root.add(new EmployeeLeaf("CEO", 50000));

        System.out.println("组织架构:");
        root.print(0);
        System.out.println("总薪资:" + root.getSalary());

        // 即使对叶子调用add会抛出异常,但编译期检查通过
        // root.getChild(0).add(new EmployeeLeaf("非法", 0)); // 运行时异常
    }
}

透明组合优势:客户端代码极度简洁,root.getSalary()root.print(0) 统一调用,无需任何条件判断。树形结构的递归操作完全封装在 Composite 内部,新增节点类型只需继承 Component,对客户端透明。缺点在于叶子节点继承了它不需要的管理方法,若客户端误调用将引发运行时异常。

2.3 安全组合模式重构

安全组合模式将管理子节点的方法从 Component 中移除,只在 Composite 中声明。这样叶子节点就不会暴露无关接口,类型安全得到保证。

import java.util.ArrayList;
import java.util.List;

// 抽象组件:只声明业务方法
interface SafeComponent {
    double getSalary();
    void print(int level);
    String getName();
}

// 叶子节点:员工
class SafeEmployeeLeaf implements SafeComponent {
    private String name;
    private double salary;

    public SafeEmployeeLeaf(String name, double salary) {
        this.name = name;
        this.salary = salary;
    }

    @Override
    public double getSalary() {
        return salary;
    }

    @Override
    public void print(int level) {
        System.out.println("  ".repeat(level) + "- " + name + " ($" + salary + ")");
    }

    @Override
    public String getName() {
        return name;
    }
}

// 容器节点:部门,额外提供管理方法
class SafeDepartmentComposite implements SafeComponent {
    private String name;
    private List<SafeComponent> children = new ArrayList<>();

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

    @Override
    public double getSalary() {
        double total = 0;
        for (SafeComponent child : children) {
            total += child.getSalary();
        }
        return total;
    }

    @Override
    public void print(int level) {
        System.out.println("  ".repeat(level) + "+ " + name);
        for (SafeComponent child : children) {
            child.print(level + 1);
        }
    }

    @Override
    public String getName() {
        return name;
    }

    // 管理子节点的方法仅在Composite中定义
    public void add(SafeComponent c) {
        children.add(c);
    }

    public void remove(SafeComponent c) {
        children.remove(c);
    }

    public SafeComponent getChild(int index) {
        return children.get(index);
    }
}

// 客户端:管理子节点时必须明确知道Composite类型
public class SafeCompositeDemo {
    public static void main(String[] args) {
        SafeDepartmentComposite root = new SafeDepartmentComposite("总公司");
        SafeDepartmentComposite deptIT = new SafeDepartmentComposite("IT部");
        deptIT.add(new SafeEmployeeLeaf("张三", 8000));
        deptIT.add(new SafeEmployeeLeaf("李四", 9000));

        SafeDepartmentComposite deptHR = new SafeDepartmentComposite("HR部");
        deptHR.add(new SafeEmployeeLeaf("王五", 7000));

        root.add(deptIT);
        root.add(deptHR);
        root.add(new SafeEmployeeLeaf("CEO", 50000));

        System.out.println("组织架构:");
        root.print(0);
        System.out.println("总薪资:" + root.getSalary());

        // 叶子节点没有add方法,编译期即可发现错误
        // SafeEmployeeLeaf leaf = new SafeEmployeeLeaf("test", 100);
        // leaf.add(...); // 编译错误
    }
}

对比总结:透明组合模式追求接口统一性,客户端代码更简洁,适合大多数需要频繁操作树形结构且管理操作与业务操作混合调用的场景。安全组合模式则严格遵循单一职责原则,叶子节点接口干净,但客户端必须使用 instanceof 或保持对 Composite 的引用才能管理子节点。实际开发中可根据团队规范与具体需求灵活选择。

2.4 组合模式的进阶特性

a. 使用迭代器遍历树形结构

组合模式常与迭代器模式结合,为客户端提供遍历树形结构的统一方式。可以在 Composite 中实现 Iterable 接口,返回一个深度优先或广度优先的迭代器。

// 为透明组合的Component增加迭代器支持(简化示例)
abstract class IterableComponent implements Iterable<Component> {
    // ... 原有成员
    public abstract Iterator<Component> iterator();
}

class IterableDepartmentComposite extends DepartmentComposite {
    @Override
    public Iterator<Component> iterator() {
        return new CompositeIterator(children.iterator());
    }
}

// 深度优先迭代器
class CompositeIterator implements Iterator<Component> {
    private Stack<Iterator<Component>> stack = new Stack<>();

    public CompositeIterator(Iterator<Component> iterator) {
        stack.push(iterator);
    }

    @Override
    public boolean hasNext() {
        // 实现略...
        return false;
    }

    @Override
    public Component next() {
        // 实现略...
        return null;
    }
}

b. 结合访问者模式处理差异化操作

当需要对树中不同类型的节点执行不同操作(如对部门进行预算审批,对员工进行个税计算)时,访问者模式是组合模式的天然搭档。在 Component 中定义 accept(Visitor v) 方法,叶子与容器分别实现,访问者则提供针对不同类型节点的 visit 重载。

interface Visitor {
    void visit(EmployeeLeaf leaf);
    void visit(DepartmentComposite composite);
}

abstract class VisitableComponent {
    public abstract void accept(Visitor v);
}

class VisitableEmployeeLeaf extends EmployeeLeaf {
    @Override
    public void accept(Visitor v) {
        v.visit(this);
    }
}
// 客户端:visitor.visit(root) 即可完成对整个树的操作

c. 缓存计算结果

对于频繁查询的聚合信息(如目录总大小),可在 Composite 中增加缓存字段,并在子节点变化时标记脏数据,避免每次都递归遍历。这是性能优化的常用手段。

class CachedDepartmentComposite extends DepartmentComposite {
    private double cachedSalary;
    private boolean isCacheValid = false;

    @Override
    public double getSalary() {
        if (isCacheValid) {
            return cachedSalary;
        }
        cachedSalary = super.getSalary();
        isCacheValid = true;
        return cachedSalary;
    }

    @Override
    public void add(Component c) {
        super.add(c);
        isCacheValid = false;
    }

    @Override
    public void remove(Component c) {
        super.remove(c);
        isCacheValid = false;
    }
}

2.5 递归调用时序图

sequenceDiagram
    participant Client
    participant Composite as root:DepartmentComposite
    participant Child1 as deptIT:DepartmentComposite
    participant Leaf1 as emp1:EmployeeLeaf
    participant Leaf2 as emp2:EmployeeLeaf
    participant Child2 as deptHR:DepartmentComposite
    participant Leaf3 as emp3:EmployeeLeaf

    Client->>Composite: getSalary()
    activate Composite
    Composite->>Composite: 初始化 total = 0
    Composite->>Child1: getSalary()
    activate Child1
    Child1->>Leaf1: getSalary()
    activate Leaf1
    Leaf1-->>Child1: 8000
    deactivate Leaf1
    Child1->>Leaf2: getSalary()
    activate Leaf2
    Leaf2-->>Child1: 9000
    deactivate Leaf2
    Child1-->>Composite: 17000
    deactivate Child1
    Composite->>Child2: getSalary()
    activate Child2
    Child2->>Leaf3: getSalary()
    activate Leaf3
    Leaf3-->>Child2: 7000
    deactivate Leaf3
    Child2-->>Composite: 7000
    deactivate Child2
    Composite->>Composite: total += 17000 + 7000 + ...
    Composite-->>Client: 74000
    deactivate Composite

时序图说明:该图展示了一次 getSalary() 调用的完整递归过程。客户端仅与根节点 DepartmentComposite 交互,根节点内部遍历其 children 列表。对于每个子节点,若为 EmployeeLeaf,则直接返回其薪资;若为嵌套的 DepartmentComposite,则递归进入其 getSalary() 方法。这种深度优先的递归调用对客户端完全透明,客户端无需知道树的结构与深度。时序图清晰地揭示了组合模式的核心机制:容器节点通过委托调用将请求沿树向下传递,最终在叶子节点执行具体操作,并将结果逐层向上汇总。这种设计使得添加新的节点类型或改变树的组织方式都不会影响客户端代码,完美诠释了“对扩展开放、对修改关闭”的原则。


三、源码级应用分析

3.1 JDK 中的组合模式

1. java.awt.ComponentContainer

AWT/Swing 是组合模式最经典的教科书式实现。java.awt.Component 是抽象组件类,定义了所有 GUI 组件的公共行为(如 paintsetVisiblegetParent)。java.awt.Container 继承自 Component,并内部维护 List<Component>,提供了 addremovegetComponent 等容器管理方法。Swing 中的 JButtonJLabel 等叶子组件直接继承 Component(实际为 JComponent),而 JPanelJFrame 等容器则继承自 Container。这正是安全组合模式的典型实现,因为叶子组件并不暴露 add 方法。

2. java.util.Map 与嵌套 Map

Map 接口本身没有显式定义组合模式,但实际使用中经常通过 Map<String, Object> 构建树形结构(如 JSON 对象)。其中 Object 既可以是基本类型(叶子),也可以是另一个 Map(容器)。虽然未使用统一抽象,但递归处理的思想与组合模式一致。例如 Spring 的 CompositePropertySource 正是对这种嵌套结构的规范化。

3. java.io.File

File 类既可表示文件(叶子)也可表示目录(容器),并且提供了统一的方法如 length()delete()listFiles()。虽然 File 没有显式的 Component 抽象父类,但其设计完全遵循组合模式的精神:客户端无需区分文件与目录即可执行操作。例如 file.listFiles() 对文件返回 null,对目录返回文件数组,客户端可通过判断处理,但 File 本身提供了 isDirectory()isFile() 来辅助。JDK 7 引入的 NIO.2 java.nio.file.Path 进一步强化了这一思想。

4. javax.faces.component.UIComponent

JSF(JavaServer Faces)框架中的 UIComponent 是组合模式的又一经典应用。UIComponentBase 作为抽象基类,UIInputUIOutput 等叶子组件以及 UIPanelUIForm 等容器组件均继承自它。UIComponent 提供了 getChildren()getFacets() 方法来管理子组件,渲染时递归遍历整个组件树生成 HTML。

3.2 Spring 框架深度剖析

1. CompositePropertySource

Spring 的 PropertySource 体系用于抽象配置源(如 properties 文件、环境变量)。CompositePropertySource 是一个典型的组合模式实现,它内部持有 Set<PropertySource<?>> 集合,并重写 getProperty(String name) 方法,遍历所有子源查找属性值。

public class CompositePropertySource extends EnumerablePropertySource<Collection<PropertySource<?>>> {
    private final Set<PropertySource<?>> propertySources = new LinkedHashSet<>();

    @Override
    public Object getProperty(String name) {
        for (PropertySource<?> propertySource : this.propertySources) {
            Object candidate = propertySource.getProperty(name);
            if (candidate != null) {
                return candidate;
            }
        }
        return null;
    }
}

客户端可以统一使用 CompositePropertySource 聚合多个配置源,而无需关心具体来源,这正是组合模式“一致对待部分与整体”的体现。

2. CompositeCacheManager

Spring Cache 抽象中的 CompositeCacheManager 组合了多个 CacheManager 实例,按照顺序尝试获取缓存。其 getCache 方法遍历内部管理器列表,返回第一个非空缓存。这在多级缓存(如本地缓存 + Redis)场景中尤为实用。

3. HandlerMethodArgumentResolverComposite

Spring MVC 在处理控制器方法参数时,使用 HandlerMethodArgumentResolverComposite 聚合了所有 HandlerMethodArgumentResolver 实现。它实现 supportsParameterresolveArgument 方法时,内部遍历解析器列表,找到第一个支持该参数的解析器并委托其处理。

4. ViewResolverComposite

与上述类似,ViewResolverComposite 组合多个 ViewResolver,依次尝试解析视图名称,实现视图解析的链式组合。

3.3 MyBatis 框架中的 SqlNode 体系

MyBatis 的动态 SQL 功能高度依赖组合模式。SqlNode 接口定义了所有动态 SQL 节点的行为:

public interface SqlNode {
    boolean apply(DynamicContext context);
}

MixedSqlNode 是典型的容器节点,内部持有 List<SqlNode>,其 apply 方法遍历子节点并依次调用:

public class MixedSqlNode implements SqlNode {
    private final List<SqlNode> contents;

    public MixedSqlNode(List<SqlNode> contents) {
        this.contents = contents;
    }

    @Override
    public boolean apply(DynamicContext context) {
        contents.forEach(node -> node.apply(context));
        return true;
    }
}

叶子节点包括 TextSqlNode(静态文本)、IfSqlNode(条件判断)、ForEachSqlNode(循环)等。它们各自实现 apply 方法执行特定的逻辑,并将生成的 SQL 片段追加到 DynamicContext 中。MyBatis 通过组合模式优雅地构建了动态 SQL 的抽象语法树(AST),使得 <if><where><foreach> 等标签可以任意嵌套,极大增强了 SQL 的动态构造能力。

3.4 其他框架应用

  • XML DOM(org.w3c.dom.Node)Node 接口代表文档树中的节点,Element 可包含子节点,Text 为叶子。方法 getChildNodes()appendChild() 构成标准组合结构。
  • 文件系统(如 Apache Commons VFS):抽象 FileObjectFileContent 为叶子,Folder 为容器。
  • 菜单权限树:常见后台管理系统的权限模块,菜单 Menu 包含子菜单或按钮,统一继承 MenuComponent
  • 组织架构树:部门与员工的树形表示,已在示例中详述。

四、分布式环境下的组合模式

组合模式不仅适用于单机内存中的树形结构,在分布式系统中同样大放异彩。通过将树节点抽象为可序列化的接口,组合模式可以优雅地表示分布式文件系统元数据、网关路由规则、配置中心层级、权限体系以及任务编排 DAG。

4.1 分布式文件系统元数据树(HDFS INode 体系)

HDFS 的元数据管理采用了类似组合模式的设计。INode 是文件系统命名空间的抽象基类,INodeFile 代表文件(叶子),INodeDirectory 代表目录(容器)。INodeDirectory 内部维护 children 列表(实际为 INodeDirectoryWithQuota 等子类)。客户端通过 FileSystem API 操作文件或目录时,底层统一调用 INode 的方法,由 NameNode 递归遍历元数据树完成路径解析、权限检查等操作。

4.2 微服务网关路由规则树

Spring Cloud Gateway 的路由定义 RouteDefinition 包含 List<PredicateDefinition>List<FilterDefinition>,本质上是一种树形组合。每个 PredicateDefinition 可视为条件叶子,而 Route 整体作为容器聚合了断言与过滤器。更复杂的场景中,可以自定义 CompositePredicateCompositeFilter,实现逻辑与(AND)、或(OR)组合,形成嵌套的路由规则树。

4.3 分布式配置中心配置树(Apollo)

Apollo 配置中心以 Namespace(命名空间)→ Cluster(集群)→ Item(配置项)的层级组织配置。虽然未显式使用组合模式接口,但其数据结构天然是树形。我们可以抽象出 ConfigComponent 接口,由 ConfigItem(叶子)和 ConfigNamespace(容器)实现。客户端获取配置时,从根节点递归向下查找,并支持配置继承与覆盖。

4.4 组织架构与权限系统的分布式树(RBAC)

在大型分布式 RBAC 系统中,部门、角色、权限三者均可建模为组合结构。例如,一个角色可以包含多个子角色和权限项。通过定义 PermissionComponent 接口,PermissionLeaf 代表具体权限(如 user:create),PermissionComposite 代表角色或部门。当进行权限校验时,递归遍历用户所属的角色树,只要任一叶子权限匹配即通过。

4.5 分布式任务编排 DAG

工作流引擎(如 Apache Airflow、Netflix Conductor)中的任务节点可构成有向无环图(DAG)。组合模式可用于表示任务容器:SerialContainer(串行执行子任务)、ParallelContainer(并行执行子任务)、ConditionalContainer(条件分支)。每个容器内部持有 List<Task>,通过统一接口 execute() 触发子任务的执行。

4.6 分布式权限校验示例

import java.util.ArrayList;
import java.util.List;

// 权限组件抽象
interface PermissionComponent {
    boolean hasPermission(String permission);
    String getName();
}

// 叶子权限
class PermissionLeaf implements PermissionComponent {
    private String name;
    private String permissionKey;

    public PermissionLeaf(String name, String permissionKey) {
        this.name = name;
        this.permissionKey = permissionKey;
    }

    @Override
    public boolean hasPermission(String permission) {
        return this.permissionKey.equals(permission);
    }

    @Override
    public String getName() {
        return name;
    }
}

// 组合权限(角色/部门)
class PermissionComposite implements PermissionComponent {
    private String name;
    private List<PermissionComponent> children = new ArrayList<>();

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

    public void add(PermissionComponent component) {
        children.add(component);
    }

    @Override
    public boolean hasPermission(String permission) {
        // 递归检查任一子节点拥有权限即返回 true
        for (PermissionComponent child : children) {
            if (child.hasPermission(permission)) {
                return true;
            }
        }
        return false;
    }

    @Override
    public String getName() {
        return name;
    }
}

// 分布式权限校验服务(模拟)
class PermissionService {
    // 从远程缓存或数据库加载用户权限树
    public PermissionComponent loadUserPermissionTree(String userId) {
        PermissionComposite root = new PermissionComposite("用户" + userId + "权限根");
        PermissionComposite roleAdmin = new PermissionComposite("管理员角色");
        roleAdmin.add(new PermissionLeaf("创建用户", "user:create"));
        roleAdmin.add(new PermissionLeaf("删除用户", "user:delete"));

        PermissionComposite roleUser = new PermissionComposite("普通用户角色");
        roleUser.add(new PermissionLeaf("查看信息", "info:view"));

        root.add(roleAdmin);
        root.add(roleUser);
        return root;
    }

    public boolean checkPermission(String userId, String permission) {
        PermissionComponent userTree = loadUserPermissionTree(userId);
        return userTree.hasPermission(permission);
    }
}

public class DistributedPermissionDemo {
    public static void main(String[] args) {
        PermissionService service = new PermissionService();
        System.out.println(service.checkPermission("1001", "user:delete")); // true
        System.out.println(service.checkPermission("1001", "order:create")); // false
    }
}

4.7 分布式权限树递归校验架构图

flowchart TD
    A[权限检查请求<br>userId + permission] --> B[权限服务]
    B --> C[加载用户权限树<br>从Redis/DB]
    C --> D[PermissionComposite<br>根节点]
    D --> E{遍历子节点}
    E --> F[角色节点<br>PermissionComposite]
    F --> E
    E --> G[权限叶子<br>PermissionLeaf]
    G --> H{匹配权限Key?}
    H -->|是| I[返回 true]
    H -->|否| E
    I --> J[汇总结果]
    J --> K[返回鉴权结果]

架构图说明:该流程图描绘了分布式权限校验中组合模式的工作机制。权限检查请求首先到达权限服务,服务从分布式缓存(如 Redis)或数据库中加载用户的权限树结构。根节点为 PermissionComposite,其内部递归包含了角色组合节点与权限叶子节点。服务调用根节点的 hasPermission(permission) 方法,该方法内部遍历所有子节点:若子节点为组合节点,则继续递归;若为叶子节点,则比对权限 Key。一旦任意叶子匹配成功,立即短路返回 true。整个递归过程对调用方完全透明,调用方只需面对统一的 PermissionComponent 接口。该设计天然支持权限继承与聚合,同时可方便地添加新的权限类型(如带条件的权限叶子),体现了组合模式在分布式系统中的强大适应力。


五、对比辨析

5.1 组合模式 vs 装饰器模式

维度组合模式装饰器模式
核心意图表示“部分-整体”层次结构,使单个对象和组合对象使用一致动态地给对象添加额外职责,且不改变其接口
子节点数量容器节点通常持有多个子节点(List)装饰器通常只持有一个被装饰对象的引用
结构形态树形结构,深度和广度可变链式结构,可层层包装
典型应用文件系统、GUI 组件、组织架构Java I/O 流、Collections.synchronizedList

5.2 组合模式 vs 责任链模式

组合模式通过树形递归处理请求,责任链模式通过线性链表传递请求。组合模式中请求可由多个节点共同处理(汇总),而责任链通常由单一节点处理并终止。不过,二者可结合:组合节点可将请求沿树向下传递,形成树形责任链。

5.3 组合模式 vs 访问者模式

组合模式提供树形结构,访问者模式定义对树中不同节点类型的操作。二者常结合使用:在 Component 中定义 accept(Visitor),容器节点负责遍历子节点并调用其 accept,从而实现对整棵树的访问操作。组合模式是“数据结构”,访问者模式是“算法操作”。

5.4 组合模式 vs 迭代器模式

组合模式内部常使用迭代器模式遍历子节点。例如 Compositeoperation 方法中通过 for-each 循环遍历 children 列表。可进一步封装为内部迭代器或外部迭代器,为客户端提供统一的遍历接口。

5.5 透明组合 vs 安全组合

对比项透明组合安全组合
接口定义Component 包含管理子节点方法Component 仅含业务方法,管理方法在 Composite
叶子节点必须实现管理方法(通常抛异常或空实现)无需实现管理方法,接口纯净
类型安全运行时才能发现误用编译期即可发现误用
客户端复杂度无需类型判断,代码简洁管理子节点时必须转换为 Composite
适用场景客户端频繁混合调用业务与管理操作管理操作与业务操作明确分离,或对类型安全要求高

六、适用场景分析(重点强化)

以下每个场景均提供独立的完整可运行 Demo 代码,并配以 Mermaid 图表和详细文字说明。

场景一:文件系统目录树

Demo 代码

import java.util.ArrayList;
import java.util.List;

// 抽象组件:文件系统节点
abstract class FileComponent {
    protected String name;

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

    public String getName() {
        return name;
    }

    // 业务方法
    public abstract long getSize();
    public abstract void printStructure(int level);
    public abstract FileComponent search(String name);

    // 管理方法(透明组合)
    public void add(FileComponent c) {
        throw new UnsupportedOperationException();
    }
    public void remove(FileComponent c) {
        throw new UnsupportedOperationException();
    }
}

// 叶子节点:文件
class FileLeaf extends FileComponent {
    private long size; // 字节

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

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

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

    @Override
    public FileComponent search(String name) {
        return this.name.equals(name) ? this : null;
    }
}

// 容器节点:目录
class DirectoryComposite extends FileComponent {
    private List<FileComponent> children = new ArrayList<>();

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

    @Override
    public long getSize() {
        long total = 0;
        for (FileComponent child : children) {
            total += child.getSize();
        }
        return total;
    }

    @Override
    public void printStructure(int level) {
        System.out.println("  ".repeat(level) + "📁 " + name + "/ (" + getSize() + " bytes)");
        for (FileComponent child : children) {
            child.printStructure(level + 1);
        }
    }

    @Override
    public FileComponent search(String name) {
        if (this.name.equals(name)) return this;
        for (FileComponent child : children) {
            FileComponent found = child.search(name);
            if (found != null) return found;
        }
        return null;
    }

    @Override
    public void add(FileComponent c) {
        children.add(c);
    }

    @Override
    public void remove(FileComponent c) {
        children.remove(c);
    }
}

// 客户端
public class FileSystemDemo {
    public static void main(String[] args) {
        DirectoryComposite root = new DirectoryComposite("root");
        DirectoryComposite home = new DirectoryComposite("home");
        DirectoryComposite user = new DirectoryComposite("alice");
        FileLeaf file1 = new FileLeaf("readme.txt", 1024);
        FileLeaf file2 = new FileLeaf("photo.jpg", 204800);
        FileLeaf file3 = new FileLeaf("config.yml", 512);

        user.add(file1);
        user.add(file2);
        home.add(user);
        home.add(file3);
        root.add(home);
        root.add(new DirectoryComposite("tmp"));

        System.out.println("=== 文件系统目录树 ===");
        root.printStructure(0);

        System.out.println("\n=== 搜索文件 'photo.jpg' ===");
        FileComponent found = root.search("photo.jpg");
        if (found != null) {
            found.printStructure(0);
        }

        System.out.println("\n=== 计算 home 目录总大小 ===");
        System.out.println("home 大小: " + home.getSize() + " bytes");
    }
}

Mermaid 类图

classDiagram
    class FileComponent {
        <<abstract>>
        #String name
        +getName() String
        +getSize() long
        +printStructure(int) void
        +search(String) FileComponent
        +add(FileComponent) void
        +remove(FileComponent) void
    }

    class FileLeaf {
        -long size
        +getSize() long
        +printStructure(int) void
        +search(String) FileComponent
    }

    class DirectoryComposite {
        -List~FileComponent~ children
        +getSize() long
        +printStructure(int) void
        +search(String) FileComponent
        +add(FileComponent) void
        +remove(FileComponent) void
    }

    FileComponent <|-- FileLeaf
    FileComponent <|-- DirectoryComposite
    DirectoryComposite o-- FileComponent : children

文字说明:文件系统是组合模式的经典应用场景。FileComponent 抽象类定义了文件和目录的统一接口,包括 getSize()printStructure()search()FileLeaf 代表具体文件,直接返回自身大小;DirectoryComposite 代表目录,其 getSize() 通过递归遍历所有子节点累加大小,printStructure() 以缩进方式展示树形层级,search() 进行深度优先的名称匹配。客户端无需区分文件与目录,直接调用 root.getSize() 即可获取整个文件树的总大小。这种设计避免了在业务逻辑中反复使用 instanceof 判断,当需要扩展新的节点类型(如符号链接)时,只需新增一个 FileComponent 子类,对现有客户端代码零侵入。此外,通过合理设计 add()/remove() 方法(可增加循环引用检测),可以有效避免目录包含自身的死循环问题。

场景二:组织架构与员工管理

Demo 代码

import java.util.ArrayList;
import java.util.List;

// 抽象组件
abstract class EmployeeComponent {
    protected String name;
    protected String title;

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

    public abstract double getTotalSalary();
    public abstract void printStructure(int level);

    public void add(EmployeeComponent e) {
        throw new UnsupportedOperationException();
    }
    public void remove(EmployeeComponent e) {
        throw new UnsupportedOperationException();
    }
}

// 叶子节点:个体员工
class IndividualEmployee extends EmployeeComponent {
    private double salary;
    private String type; // 全职/兼职

    public IndividualEmployee(String name, String title, double salary, String type) {
        super(name, title);
        this.salary = salary;
        this.type = type;
    }

    @Override
    public double getTotalSalary() {
        return salary;
    }

    @Override
    public void printStructure(int level) {
        System.out.println("  ".repeat(level) + "👤 " + name + " [" + title + "] (" + type + ") - $" + salary);
    }
}

// 容器节点:部门
class DepartmentComposite extends EmployeeComponent {
    private List<EmployeeComponent> members = new ArrayList<>();

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

    @Override
    public double getTotalSalary() {
        double total = 0;
        for (EmployeeComponent member : members) {
            total += member.getTotalSalary();
        }
        return total;
    }

    @Override
    public void printStructure(int level) {
        System.out.println("  ".repeat(level) + "🏢 " + name + " [" + title + "] (总薪资: $" + getTotalSalary() + ")");
        for (EmployeeComponent member : members) {
            member.printStructure(level + 1);
        }
    }

    @Override
    public void add(EmployeeComponent e) {
        members.add(e);
    }

    @Override
    public void remove(EmployeeComponent e) {
        members.remove(e);
    }
}

public class OrgChartDemo {
    public static void main(String[] args) {
        DepartmentComposite company = new DepartmentComposite("XYZ科技", "公司");
        DepartmentComposite engDept = new DepartmentComposite("工程部", "部门");
        DepartmentComposite hrDept = new DepartmentComposite("人力资源部", "部门");

        engDept.add(new IndividualEmployee("张伟", "技术总监", 30000, "全职"));
        engDept.add(new IndividualEmployee("李莉", "高级工程师", 20000, "全职"));
        engDept.add(new IndividualEmployee("王强", "实习生", 5000, "兼职"));

        hrDept.add(new IndividualEmployee("赵芳", "HR经理", 18000, "全职"));
        hrDept.add(new IndividualEmployee("孙梅", "招聘专员", 12000, "全职"));

        company.add(engDept);
        company.add(hrDept);
        company.add(new IndividualEmployee("陈晨", "CEO", 80000, "全职"));

        System.out.println("=== 组织架构图 ===");
        company.printStructure(0);
        System.out.println("\n公司总薪资支出: $" + company.getTotalSalary());
    }
}

Mermaid 时序图

sequenceDiagram
    participant CEO as CEO:IndividualEmployee
    participant Company as company:DepartmentComposite
    participant EngDept as engDept:DepartmentComposite
    participant HRDept as hrDept:DepartmentComposite
    participant Emp1 as 张伟:IndividualEmployee
    participant Emp2 as 李莉:IndividualEmployee
    participant Emp3 as 赵芳:IndividualEmployee

    CEO->>Company: getTotalSalary()
    activate Company
    Company->>EngDept: getTotalSalary()
    activate EngDept
    EngDept->>Emp1: getTotalSalary() -> 30000
    EngDept->>Emp2: getTotalSalary() -> 20000
    EngDept-->>Company: 50000
    deactivate EngDept
    Company->>HRDept: getTotalSalary()
    activate HRDept
    HRDept->>Emp3: getTotalSalary() -> 18000
    HRDept-->>Company: 18000
    deactivate HRDept
    Company-->>CEO: 148000 (含CEO自身)
    deactivate Company

文字说明:该时序图展示了从 CEO(叶子节点)调用根部门 getTotalSalary() 的递归过程。尽管 CEO 本身是叶子,但通过持有对公司的引用,它能够触发整个组织树的薪资计算。Company 容器收到请求后,遍历其 members 列表,依次调用子部门的 getTotalSalary()。每个子部门继续向下递归,直至叶子员工返回其个体薪资。最终结果逐层汇总返回给调用者。组合模式在此场景中的优势尤为突出:无论组织架构如何调整(部门合并、拆分),计算总薪资的代码无需任何修改。同时,通过统一抽象,不同类型的员工(全职、兼职、外包)只要实现 getTotalSalary() 方法,即可无缝融入现有体系。这符合“开闭原则”,也为未来扩展(如按工时计算薪资的临时工)预留了接口。

场景三:菜单与导航系统

Demo 代码

import java.util.ArrayList;
import java.util.List;

// 抽象菜单组件
abstract class MenuComponent {
    protected String name;
    protected String url;
    protected boolean visible = true;

    public MenuComponent(String name, String url) {
        this.name = name;
        this.url = url;
    }

    public abstract String render(); // 渲染 HTML
    public abstract boolean hasPermission(String userRole);

    public void add(MenuComponent menu) {
        throw new UnsupportedOperationException();
    }
    public void remove(MenuComponent menu) {
        throw new UnsupportedOperationException();
    }

    public void setVisible(boolean visible) {
        this.visible = visible;
    }
    public boolean isVisible() {
        return visible;
    }
}

// 叶子节点:菜单项
class MenuItem extends MenuComponent {
    public MenuItem(String name, String url) {
        super(name, url);
    }

    @Override
    public String render() {
        if (!visible) return "";
        return "<li><a href='" + url + "'>" + name + "</a></li>";
    }

    @Override
    public boolean hasPermission(String userRole) {
        // 简化的权限判断:管理员可见所有,普通用户不可见 admin 菜单
        if (userRole.equals("ADMIN")) return true;
        return !url.startsWith("/admin");
    }
}

// 容器节点:菜单组
class MenuComposite extends MenuComponent {
    private List<MenuComponent> children = new ArrayList<>();

    public MenuComposite(String name, String url) {
        super(name, url);
    }

    @Override
    public String render() {
        if (!visible) return "";
        StringBuilder sb = new StringBuilder();
        sb.append("<li>");
        sb.append("<span>").append(name).append("</span>");
        sb.append("<ul>");
        for (MenuComponent child : children) {
            // 权限过滤:仅当用户有权限时才渲染
            if (child.isVisible() && child.hasPermission(getCurrentUserRole())) {
                sb.append(child.render());
            }
        }
        sb.append("</ul></li>");
        return sb.toString();
    }

    @Override
    public boolean hasPermission(String userRole) {
        // 容器只要有任一子项有权限即可见
        for (MenuComponent child : children) {
            if (child.hasPermission(userRole)) return true;
        }
        return false;
    }

    @Override
    public void add(MenuComponent menu) {
        children.add(menu);
    }

    @Override
    public void remove(MenuComponent menu) {
        children.remove(menu);
    }

    // 模拟获取当前用户角色
    private String getCurrentUserRole() {
        return "USER"; // 实际项目中从上下文获取
    }
}

public class MenuDemo {
    public static void main(String[] args) {
        MenuComposite root = new MenuComposite("主导航", "#");
        MenuComposite systemMenu = new MenuComposite("系统管理", "/admin");
        systemMenu.add(new MenuItem("用户管理", "/admin/users"));
        systemMenu.add(new MenuItem("角色管理", "/admin/roles"));

        MenuComposite reportMenu = new MenuComposite("报表中心", "/reports");
        reportMenu.add(new MenuItem("销售报表", "/reports/sales"));
        reportMenu.add(new MenuItem("流量分析", "/reports/traffic"));

        root.add(new MenuItem("首页", "/home"));
        root.add(systemMenu);
        root.add(reportMenu);
        root.add(new MenuItem("关于我们", "/about"));

        System.out.println("=== 渲染菜单 HTML(普通用户视角,系统管理不可见)===");
        System.out.println("<ul>" + root.render() + "</ul>");
    }
}

Mermaid 流程图

flowchart TD
    Start(["调用 root.render"]) --> CheckVisible{"根菜单可见?"}
    CheckVisible -->|"是"| RenderSpan["渲染 <span>根菜单名</span>"]
    RenderSpan --> OpenUL["输出 <ul>"]
    OpenUL --> LoopStart{"遍历子菜单"}
    LoopStart --> CheckChildPerm["检查子菜单权限 hasPermission"]
    CheckChildPerm -->|"无权限"| LoopStart
    CheckChildPerm -->|"有权限"| RenderChild["调用 child.render"]
    RenderChild --> IsComposite{"子菜单是 MenuComposite?"}
    IsComposite -->|"是"| Recursive["递归进入容器渲染"]
    Recursive --> LoopStart
    IsComposite -->|"否"| OutputItem["输出 <li>菜单项</li>"]
    OutputItem --> LoopStart
    LoopStart -->|"遍历结束"| CloseUL["输出 </ul>"]
    CloseUL --> End(["返回 HTML 字符串"])
    CheckVisible -->|"否"| End

文字说明:该流程图描述了菜单渲染的递归过程。从根菜单开始,首先检查其可见性,若不可见则直接返回空字符串。可见情况下,渲染菜单组标题,并遍历其所有子菜单。对于每个子菜单,先调用 hasPermission(userRole) 进行权限过滤,若无权限则跳过;有权限时调用 child.render()。若子节点是 MenuComposite,则流程递归进入其内部,重复相同步骤,最终生成嵌套的 <ul>/<li> 结构。组合模式在此场景中统一了叶子菜单项与菜单组的渲染接口,并且权限过滤逻辑可被透明地织入递归过程。结合装饰器模式,还可进一步实现菜单的缓存、国际化等增强功能。前端渲染引擎无需理解菜单树的深度与权限细节,仅需调用根节点的 render() 方法即可获得完整的 HTML 片段,极大降低了视图层与业务逻辑的耦合。

场景四:表达式树求值

Demo 代码

import java.util.ArrayList;
import java.util.List;

// 表达式抽象组件
interface ExpressionComponent {
    double evaluate();
}

// 叶子节点:数字
class NumberLeaf implements ExpressionComponent {
    private double value;

    public NumberLeaf(double value) {
        this.value = value;
    }

    @Override
    public double evaluate() {
        return value;
    }
}

// 容器节点:加法运算
class AddComposite implements ExpressionComponent {
    private List<ExpressionComponent> operands = new ArrayList<>();

    public void addOperand(ExpressionComponent operand) {
        operands.add(operand);
    }

    @Override
    public double evaluate() {
        double sum = 0;
        for (ExpressionComponent operand : operands) {
            sum += operand.evaluate();
        }
        return sum;
    }
}

// 容器节点:乘法运算(为简化,此处固定为二元)
class MultiplyComposite implements ExpressionComponent {
    private ExpressionComponent left;
    private ExpressionComponent right;

    public MultiplyComposite(ExpressionComponent left, ExpressionComponent right) {
        this.left = left;
        this.right = right;
    }

    @Override
    public double evaluate() {
        return left.evaluate() * right.evaluate();
    }
}

// 减法、除法类似,略...

public class ExpressionDemo {
    public static void main(String[] args) {
        // 构建表达式: (3 + 5) * 2 + 10
        AddComposite innerAdd = new AddComposite();
        innerAdd.addOperand(new NumberLeaf(3));
        innerAdd.addOperand(new NumberLeaf(5));

        MultiplyComposite mul = new MultiplyComposite(innerAdd, new NumberLeaf(2));

        AddComposite rootAdd = new AddComposite();
        rootAdd.addOperand(mul);
        rootAdd.addOperand(new NumberLeaf(10));

        System.out.println("(3 + 5) * 2 + 10 = " + rootAdd.evaluate()); // 26
    }
}

Mermaid 类图

classDiagram
    class ExpressionComponent {
        <<interface>>
        +evaluate() double
    }

    class NumberLeaf {
        -double value
        +evaluate() double
    }

    class AddComposite {
        -List~ExpressionComponent~ operands
        +addOperand(ExpressionComponent) void
        +evaluate() double
    }

    class MultiplyComposite {
        -ExpressionComponent left
        -ExpressionComponent right
        +evaluate() double
    }

    ExpressionComponent <|.. NumberLeaf
    ExpressionComponent <|.. AddComposite
    ExpressionComponent <|.. MultiplyComposite
    AddComposite o-- ExpressionComponent : operands
    MultiplyComposite o-- ExpressionComponent : left/right

文字说明:表达式树求值是组合模式与解释器模式结合的经典案例。ExpressionComponent 接口定义了统一的 evaluate() 方法。NumberLeaf 作为叶子直接返回数值;AddCompositeMultiplyComposite 作为容器,内部持有子表达式引用,并在 evaluate() 中递归计算并聚合结果。此处的组合模式提供了构建抽象语法树(AST)的数据结构能力,而解释器模式则负责定义语法规则和求值逻辑。二者协同工作:组合模式负责树形结构的物理组织,解释器模式负责语义解析与执行。通过这种方式,我们可以轻松扩展新的运算符(如 SubtractComposite),且客户端构建与求值代码完全解耦。在真实编译器或规则引擎中,表达式节点可能多达数十种,组合模式确保了系统的高度可扩展性与一致性。

场景五:GUI 容器与组件

Demo 代码

import java.util.ArrayList;
import java.util.List;

// 抽象UI组件
abstract class UIComponent {
    protected String id;
    protected int x, y, width, height;

    public UIComponent(String id, int x, int y, int width, int height) {
        this.id = id;
        this.x = x;
        this.y = y;
        this.width = width;
        this.height = height;
    }

    public abstract void paint(); // 绘制自身
    public abstract void handleEvent(String event); // 事件处理

    public void add(UIComponent c) {
        throw new UnsupportedOperationException();
    }
    public void remove(UIComponent c) {
        throw new UnsupportedOperationException();
    }
}

// 叶子:按钮
class ButtonLeaf extends UIComponent {
    private String label;

    public ButtonLeaf(String id, int x, int y, String label) {
        super(id, x, y, 80, 30);
        this.label = label;
    }

    @Override
    public void paint() {
        System.out.println("绘制按钮 [" + id + "] 位置(" + x + "," + y + ") 文本: " + label);
    }

    @Override
    public void handleEvent(String event) {
        System.out.println("按钮 [" + id + "] 响应事件: " + event);
    }
}

// 叶子:标签
class LabelLeaf extends UIComponent {
    private String text;

    public LabelLeaf(String id, int x, int y, String text) {
        super(id, x, y, 100, 20);
        this.text = text;
    }

    @Override
    public void paint() {
        System.out.println("绘制标签 [" + id + "] 位置(" + x + "," + y + ") 内容: " + text);
    }

    @Override
    public void handleEvent(String event) {
        // 标签通常不处理事件
    }
}

// 容器:面板
class PanelComposite extends UIComponent {
    private List<UIComponent> children = new ArrayList<>();

    public PanelComposite(String id, int x, int y, int width, int height) {
        super(id, x, y, width, height);
    }

    @Override
    public void paint() {
        System.out.println("绘制面板 [" + id + "] 区域(" + x + "," + y + "," + width + "," + height + ")");
        for (UIComponent child : children) {
            child.paint();
        }
    }

    @Override
    public void handleEvent(String event) {
        // 事件冒泡:先尝试让子组件处理,若未处理则自身处理
        boolean handled = false;
        for (UIComponent child : children) {
            child.handleEvent(event);
            // 简化:假设按钮处理后返回 true,此处省略
        }
        if (!handled) {
            System.out.println("面板 [" + id + "] 处理事件: " + event);
        }
    }

    @Override
    public void add(UIComponent c) {
        children.add(c);
    }

    @Override
    public void remove(UIComponent c) {
        children.remove(c);
    }
}

public class GUIDemo {
    public static void main(String[] args) {
        PanelComposite mainPanel = new PanelComposite("main", 0, 0, 800, 600);
        PanelComposite toolBar = new PanelComposite("toolbar", 0, 0, 800, 50);
        toolBar.add(new ButtonLeaf("btnSave", 10, 10, "保存"));
        toolBar.add(new ButtonLeaf("btnLoad", 100, 10, "加载"));

        PanelComposite content = new PanelComposite("content", 0, 50, 800, 550);
        content.add(new LabelLeaf("lblTitle", 20, 20, "欢迎使用本系统"));
        content.add(new ButtonLeaf("btnOK", 20, 60, "确定"));

        mainPanel.add(toolBar);
        mainPanel.add(content);

        System.out.println("=== 渲染UI ===");
        mainPanel.paint();

        System.out.println("\n=== 模拟点击按钮 btnSave ===");
        mainPanel.handleEvent("click:btnSave");
    }
}

Mermaid 时序图

sequenceDiagram
    participant Window as 窗口系统
    participant MainPanel as mainPanel:PanelComposite
    participant ToolBar as toolBar:PanelComposite
    participant BtnSave as btnSave:ButtonLeaf
    participant Content as content:PanelComposite
    participant LblTitle as lblTitle:LabelLeaf

    Window->>MainPanel: paint()
    activate MainPanel
    MainPanel->>MainPanel: 绘制自身背景
    MainPanel->>ToolBar: paint()
    activate ToolBar
    ToolBar->>BtnSave: paint()
    BtnSave-->>ToolBar: 绘制按钮
    ToolBar->>Content: paint()
    activate Content
    Content->>LblTitle: paint()
    LblTitle-->>Content: 绘制标签
    deactivate Content
    deactivate ToolBar
    deactivate MainPanel

文字说明:该时序图模拟了 GUI 系统中 paint() 消息沿组件树传播的过程。顶层窗口(此处为 mainPanel)接收到绘制请求后,首先绘制自身的背景和边框,然后遍历其子组件列表,依次调用每个子组件的 paint() 方法。对于容器子组件(如 toolBarcontent),绘制流程递归进入其内部,最终所有叶子组件完成自身绘制。AWT/Swing 正是基于此模型运作。事件处理机制与此类似,但通常采用“冒泡”或“捕获”策略:事件从根组件向下传递(或从目标叶子向上冒泡),组合模式为这种递归传递提供了结构基础。与 Web DOM 事件模型相比,两者在树形遍历与委托机制上如出一辙,充分证明了组合模式在 GUI 框架设计中的普适性。


七、面试题精选与专家级解答

1. 透明组合模式与安全组合模式有什么区别?分别在什么场景下使用?

回答要点:透明组合将管理子节点的方法(add/remove)定义在 Component 接口中,使客户端可以一致地对待叶子和容器,但叶子必须实现这些方法(通常抛出异常),牺牲了类型安全。安全组合将管理方法下放到 Composite 接口,叶子无需实现,类型安全但客户端管理子节点时必须进行类型转换。场景选择:若业务中频繁混合调用业务方法与管理方法,且希望客户端代码极度简洁,选透明组合;若管理操作与业务操作调用者明确分离,或项目强制要求编译期类型检查,则选安全组合。此外,现代 Java 可通过默认方法(default)在接口中提供空实现,一定程度上缓解了透明组合的异常问题。

2. JDK 中哪些地方使用了组合模式?请至少列举三个并分析其实现细节。

回答要点

  • java.awt.ComponentContainerComponent 为抽象根类,定义 paintgetPreferredSize 等方法;Container 继承 Component 并维护 List<Component>,提供 add/removeJButtonJPanel 等 Swing 组件均以此为基础。属于安全组合模式。
  • java.util.Map 嵌套Map<String, Object>Object 可为基本类型或另一个 Map,形成树形结构,常用于表示 JSON 或配置。虽未显式定义抽象组件,但递归遍历处理方式与组合模式思想一致。
  • java.io.FileFile 既表示文件(叶子)也表示目录(容器)。方法如 listFiles() 对文件返回 null,对目录返回数组;length() 统一获取大小。虽然缺少统一抽象接口,但 API 设计体现了组合模式精神。

3. 组合模式如何与访问者模式配合使用?请以文件系统遍历为例说明。

回答要点:组合模式提供树形数据结构,访问者模式定义对树中不同类型节点的操作。在文件系统场景中,定义 FileVisitor 接口,包含 visit(FileLeaf)visit(DirectoryComposite) 方法。在 FileComponent 中添加 accept(FileVisitor v) 抽象方法。FileLeaf 实现为 v.visit(this)DirectoryComposite 实现为先调用 v.visit(this),再遍历子节点调用 child.accept(v)。客户端只需实现具体的访问者(如计算总大小、统计文件数量),并调用 root.accept(visitor),即可完成对整棵树的操作。该组合实现了数据结构与算法的分离,当需要新增操作时无需修改节点类,符合开闭原则。

4. 组合模式中的叶子节点和容器节点是否应该暴露相同的接口?请从单一职责原则角度分析。

回答要点:从单一职责原则(SRP)看,叶子节点的职责是执行具体的业务逻辑,不应承担管理子节点的职责。透明组合模式强制叶子实现管理方法,这明显违反了 SRP 和接口隔离原则(ISP)。然而,设计模式常常是在多种原则间权衡的结果。透明组合牺牲了部分 SRP 以换取客户端的简洁性和一致性。在实际开发中,可以通过在抽象类中提供默认空实现(Java 8+)或文档明确说明来缓解违规影响。若项目对代码纯净度要求极高,应选择安全组合模式,让接口职责更加清晰。

5. Spring 中的 CompositePropertySource 是如何体现组合模式的?它的设计解决了什么问题?

回答要点CompositePropertySource 继承自 EnumerablePropertySource,内部持有 Set<PropertySource<?>> 集合。它重写了 getProperty(String name) 方法,遍历所有子 PropertySource 并返回第一个非空值。这体现了组合模式的核心思想:客户端可以将 CompositePropertySource 当作一个普通的 PropertySource 使用,而无需关心其内部聚合了多少个配置源。它解决了多配置源优先级合并的问题,例如 Spring Boot 中 application.properties、环境变量、命令行参数等统一合并为一个 CompositePropertySource,简化了配置读取逻辑。

6. MyBatis 的动态 SQL 是如何利用组合模式构建 SqlNode 树的?请分析 MixedSqlNode 的实现。

回答要点:MyBatis 的 SqlNode 接口代表动态 SQL 解析树中的节点,定义 boolean apply(DynamicContext context) 方法。MixedSqlNode 是实现组合模式的容器节点,内部持有 List<SqlNode>。其 apply 方法遍历所有子节点并调用它们的 apply 方法,从而将各个子节点生成的 SQL 片段拼接到 DynamicContext 中。叶子节点包括 TextSqlNode(纯文本)、IfSqlNode(条件判断)、ForEachSqlNode(循环)等,它们各自实现特定的逻辑。这种设计使得 MyBatis 可以灵活地解析任意嵌套的动态 SQL 标签,且新增标签只需添加新的 SqlNode 实现,无需修改框架核心逻辑。

7. 在分布式系统中,如何利用组合模式设计一个支持多级缓存的查询框架?

回答要点:定义 CacheComponent 接口,包含 Object get(String key) 方法。LocalCacheLeaf 实现本地缓存(如 Caffeine),RedisCacheLeaf 实现分布式缓存。CacheComposite 容器内部持有 List<CacheComponent>,其 get 方法按顺序遍历子缓存:先查本地,命中则返回;未命中则查 Redis,若命中则回填本地缓存后返回;仍未命中则穿透到数据库,并将结果回填至各级缓存。客户端仅需与根 CacheComposite 交互,无需关心多级缓存的具体结构。通过组合模式,可以动态调整缓存层级(如增加 Memcached 层),符合开闭原则,且缓存回填逻辑被统一封装在容器内部。

8. 组合模式与装饰器模式在结构上都是递归包装,如何从意图和子节点数量上区分?

回答要点意图区别:组合模式旨在构建“部分-整体”的树形层次,强调对象聚合;装饰器模式旨在动态地为对象添加额外功能,强调功能增强。子节点数量:组合模式中的容器通常持有多个子节点(List),形成多叉树;装饰器模式中的装饰器通常只持有一个被装饰对象的引用,形成单链结构。使用场景:组合模式用于文件系统、GUI 组件;装饰器模式用于 Java I/O 流、功能增强包装。尽管结构上都是递归包含,但通过子节点数量(一 vs 多)和设计意图可明确区分。

9. 如何实现组合模式的懒加载?即子节点在首次访问时才加载。

回答要点:在 Composite 类中引入一个 boolean loaded = false 标志,以及一个 loadChildren() 方法。getChildoperation 等方法在访问子节点前,先检查 loaded 标志,若未加载则调用 loadChildren() 从数据库或远程服务加载子节点列表并缓存。loadChildren() 可设计为抽象方法,由具体子类实现加载逻辑。同时,需要注意线程安全问题,可使用双重检查锁或 volatile 配合 synchronized 保证可见性。懒加载可显著降低大规模树形结构初始化的内存与时间开销。

10. 组合模式在处理大规模树形结构时可能遇到性能瓶颈,如何优化?

回答要点

  • 缓存计算结果:如目录大小、权限校验结果等,在子节点变动时标记脏数据,避免每次重复递归。
  • 批量操作:提供批量添加/删除子节点的方法,减少重复遍历。
  • 并行遍历:对于计算密集型聚合操作(如薪资汇总),可使用 parallelStream() 并发处理子节点,但需注意线程安全。
  • 迭代器与流式处理:使用内部迭代器或 Stream API 避免显式递归造成的栈溢出风险(深层树可转为迭代 + 栈模拟)。
  • 节点索引:对于频繁搜索的场景,在容器内部维护 Map<String, Component> 索引加速查找。
  • 剪枝优化:在遍历过程中根据条件提前终止递归(如权限校验找到匹配即返回)。

八、结语

组合模式以其简洁而强大的抽象能力,成为处理树形结构的首选设计模式。从单机应用的文件系统、GUI 组件,到分布式系统中的权限树、配置树、任务编排 DAG,组合模式的身影无处不在。掌握组合模式不仅意味着理解其类图结构与代码实现,更在于领悟“统一对待部分与整体”的设计哲学,以及如何在类型安全与接口一致性之间做出恰当权衡。

本文通过从原始代码到两种变体的演进、框架源码的深度剖析、分布式场景的拓展应用、五大典型场景的独立 Demo,以及十余道专家级面试题的解析,力求为读者构建一个立体而完整的组合模式知识体系。希望读者在阅读后,不仅能熟练运用组合模式解决实际问题,更能在系统架构设计中灵活变通,将这一经典模式发扬光大。