Java 23 种设计模式:从踩坑到精通 | 组合模式 —— 树形结构处理,部分与整体一视同仁

摘要:业务代码里充斥着 if-else 来判断是“文件”还是“文件夹”?每次新增一种节点类型都要修改十几处逻辑?在 2026 年的微服务架构中,这种“面条代码”往往是性能下降的隐形杀手。本文带你用组合模式彻底消灭这些重复判断——通过透明式安全式的深度剖析,结合 Spring Security 源码实录AI 辅助编程演示,让你彻底掌握树形结构处理的终极奥义。

📖 《Java 23 种设计模式:从踩坑到精通》
开篇:系列介绍与目录 | 上一篇:桥接模式 | 当前:组合模式 | 下一篇:装饰器模式
🔗 返回系列总目录


1. 你还在写“面条代码”吗?

假设你正在开发一个 2026 年的低代码平台,需要处理复杂的页面组件树:

if (component.isContainer()) {
    for (Component child : component.getChildren()) {
        // 递归处理
    }
} else {
    // 渲染组件
}

当产品说“我们要增加一种循环容器”时,你必须去改所有写过 if-else 的地方。这不仅违背了开闭原则,更让代码变得像面条一样纠缠不清。在微服务架构中,复杂的树形菜单或权限结构处理往往成为服务性能的隐形杀手。

组合模式(Composite Pattern)正是为解决这种“部分-整体层次结构”的访问一致性而生的:它将对象组合成树形结构以表示“部分-整体”的层次关系,使得客户端对单个对象和组合对象的使用具有一致性。

💡 核心逻辑:组合模式 = 树形结构 + 一致接口。

1.1 你的场景该不该用组合模式?

判断标准 是 → 用组合模式 否 → 用其他方式
对象之间存在明显的“整体-部分”层次关系
需要统一处理单个对象和组合对象
希望新增节点类型不影响客户端代码
结构扁平,无需递归处理 直接集合遍历即可

2. 模式定义与 UML 结构

组合模式 将对象组合成树形结构以表示“部分-整体”的层次关系,使得用户对单个对象和组合对象的使用具有一致性。它属于 结构型设计模式

标准组合模式的 UML 类图:

组合模式

三个角色

  • Component(抽象构件):定义叶子构件和容器构件的公共接口,可以是抽象类或接口;
  • Leaf(叶子构件):表示树中的叶子节点,没有子节点;
  • Composite(容器构件):包含子节点(可以是叶子或其他容器),提供管理子节点的方法和统一的 operation()

💡 根据管理子节点方法(add/remove)的位置,可分为透明式组合安全式组合两种。


3. 透明组合与安全组合

3.1 透明组合

将所有管理方法(add/remove)定义在抽象 Component 中,让 LeafComposite 都实现它们。Leaf 的方法抛出异常或不处理。这样客户端完全一致,但运行时可能抛出异常。

3.2 安全组合

只在 Composite 中定义管理方法,Component 中只定义业务方法。这样更安全,不会在 Leaf 上调用无意义的方法,但客户端必须区分 LeafComposite,牺牲了部分透明性。

⚠️ 避坑指南:在实际项目中,除非你有极强的统一接口需求,否则强烈建议使用安全组合。否则后期维护时,Leaf 节点上不断抛出的 UnsupportedOperationException 会让你头疼不已。安全组合更符合单一职责原则


4. 代码实现:文件系统(透明组合)

4.1 抽象构件

public abstract class FileSystemNode {
    protected String name;

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

    public abstract void show(int depth);

    // 管理子节点方法(透明组合)
    public void add(FileSystemNode node) {
        throw new UnsupportedOperationException();
    }

    public void remove(FileSystemNode node) {
        throw new UnsupportedOperationException();
    }
}

4.2 叶子构件:文件

public class FileNode extends FileSystemNode {
    public FileNode(String name) {
        super(name);
    }

    @Override
    public void show(int depth) {
        // 建议在此处打断点,观察递归过程中 depth 的变化
        System.out.println("  ".repeat(depth) + "📄 文件:" + name);
    }
}

4.3 容器构件:文件夹

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

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

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

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

    @Override
    public void show(int depth) {
        // 建议在 show() 处打断点,观察 children 的递归压栈过程
        System.out.println("  ".repeat(depth) + "📁 文件夹:" + name);
        for (FileSystemNode child : children) {
            child.show(depth + 1);  // 递归调用,深度+1
        }
    }
}

4.4 客户端调用

public class Client {
    public static void main(String[] args) {
        FileSystemNode root = new FolderNode("root");
        FileSystemNode home = new FolderNode("home");
        FileSystemNode docs = new FolderNode("docs");
        FileSystemNode file1 = new FileNode("readme.txt");
        FileSystemNode file2 = new FileNode("image.png");

        root.add(home);
        root.add(docs);
        docs.add(file1);
        docs.add(file2);

        root.show(0);
    }
}

运行结果:

📁 文件夹:root
  📁 文件夹:home
  📁 文件夹:docs
    📄 文件:readme.txt
    📄 文件:image.png

客户端完全不用区分文件和文件夹,统一调用 show() 即可递归展示整棵树。

🔧 调试技巧:在 FolderNode.show() 处打断点,观察 children 的递归压栈过程,你会对组合模式的执行流程有更直观的理解。


5. 代码实现:组织架构(安全组合,推荐)

5.1 抽象构件

public interface Employee {
    void showDetail();
}

5.2 叶子构件:员工

public class IndividualEmployee implements Employee {
    private String name;
    private String position;

    public IndividualEmployee(String name, String position) {
        this.name = name;
        this.position = position;
    }

    @Override
    public void showDetail() {
        System.out.println("  员工:" + name + ",职位:" + position);
    }
}

5.3 容器构件:部门

public class Department implements Employee {
    private String name;
    private List<Employee> members = new ArrayList<>();

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

    // 子节点管理方法只在 Composite 中定义(安全组合)
    public void add(Employee e) {
        members.add(e);
    }

    public void remove(Employee e) {
        members.remove(e);
    }

    @Override
    public void showDetail() {
        System.out.println("部门:" + name);
        for (Employee member : members) {
            member.showDetail();
        }
    }
}

5.4 客户端调用

Department company = new Department("总公司");
Department devDept = new Department("研发部");
Employee alice = new IndividualEmployee("Alice", "高级工程师");
Employee bob = new IndividualEmployee("Bob", "实习生");

devDept.add(alice);
devDept.add(bob);
company.add(devDept);

company.showDetail();

输出:

部门:总公司
部门:研发部
  员工:Alice,职位:高级工程师
  员工:Bob,职位:实习生

如果新增一个“子公司”部门,它同样是 Employee 类型,只需将子部门加入总公司,无需修改现有代码。


6. 优缺点一览

优点 缺点
一致性:客户端统一处理叶子与容器,简化代码 设计复杂度增加:需要引入抽象层,类数量增多
符合开闭原则:新增叶子或容器类型无需修改现有代码 不易限制类型:若需强制只允许特定子类型,需额外约束
清晰的层次结构:形成树形结构,符合自然认知 透明组合下,叶子节点存在无用方法,违背接口隔离原则
递归操作天然适合:如遍历、统计、查找

⚠️ 避坑:如果树深度过大或节点过多,递归遍历可能导致栈溢出或性能瓶颈。此时可考虑懒加载(只加载当前层,展开时再加载子节点)或迭代器模式替代递归。


7. 进阶:Spring Security 源码中的组合模式

Spring Security 的配置构建器是组合模式的经典应用。HttpSecurity 作为容器,SecurityConfigurer 的各种子类作为叶子或容器,通过组合形成安全配置树。

核心源码AbstractConfiguredSecurityBuilder 简化版):

public abstract class AbstractConfiguredSecurityBuilder<O, B extends SecurityBuilder<O>> 
        extends AbstractSecurityBuilder<O> {

    // 配置器列表(组合模式的 children)
    private final LinkedHashMap<Class<? extends SecurityConfigurer<O, B>>, 
                                List<SecurityConfigurer<O, B>>> configurers = new LinkedHashMap<>();

    // 添加配置器(组合模式的 add)
    public <C extends SecurityConfigurer<O, B>> C apply(C configurer) throws Exception {
        configurer.init(this);       // 初始化
        configurer.configure(this);  // 配置
        return configurer;
    }

    // 构建时遍历所有配置器(组合模式的递归处理)
    @Override
    protected final O doBuild() throws Exception {
        for (SecurityConfigurer<O, B> configurer : getConfigurers()) {
            configurer.configure(this);  // 统一调用
        }
        return performBuild();
    }
}

对应关系

组合模式角色 Spring Security 中的对应
Component SecurityConfigurer<O, B>
Leaf CorsConfigurerCsrfConfigurer(只配置单一功能)
Composite HttpSecurity(包含多个子配置器)
operation() configure(HttpSecurity)
add() apply(C configurer)

客户端调用

http
    .cors().and()      // 添加 CorsConfigurer
    .csrf().disable()  // 修改 CsrfConfigurer
    .authorizeRequests()
        .anyRequest().authenticated();

💡 HttpSecurity 通过 and() 方法返回父级容器,形成配置链。最终调用 build() 时,递归遍历所有子配置器,统一应用安全规则——这正是组合模式的“统一处理”思想的体现。


8. 组合模式 vs 解释器模式(面试连环炮)

面试官:“组合模式和解释器模式都用到了树,它们有什么关系?”

:虽然都用树,但解释器模式通常建立在组合模式的基础上(如 Spring Expression Language 解析表达式时,先构建组合树,再递归求值)。组合模式关注 “结构”(统一处理部分与整体),解释器关注 “计算”(递归求值语法树)。

对比维度 组合模式 解释器模式
目的 统一处理部分与整体 解释语言句子,求值
节点行为 operation() 统一,叶子与容器行为相似 interpret() 计算,不同类型节点行为差异大
典型应用 文件系统、组织架构 表达式求值、正则
组合关系 独立使用 通常建立在组合模式之上

9. AI 时代的组合模式

9.1 AI 辅助生成组合模式代码

推荐 Prompt
“我有一个 FileSystemNode 抽象类,它有 FolderNode 容器和 FileNode 叶子两种实现。请帮我新增一个 ShortcutNode 叶子节点,它指向另一个 FileSystemNode,并在 show() 方法中显示 ‘快捷方式 -> 目标文件名’。遵循透明组合模式,保持客户端调用一致。”

AI 会快速生成符合组合模式的类结构,无需手动复制粘贴现有代码。你还可以进一步要求:

“请在这段代码中增加 isLeaf() 方法,方便客户端判断当前节点是否为叶子,并给出使用示例。”

9.2 常见 AI 交互场景

场景 Prompt 示例
新增节点类型 “基于组合模式,新增一个循环容器节点,遍历子节点 N 次”
批量操作 “给所有叶子节点增加权限编码属性,并在打印时显示”
性能优化 “如果组合树深度过大,如何用迭代器替代递归遍历?”

10. 常见误区与面试高频题

❌ 误区1:组合模式就是树结构
组合模式不仅提供树结构,更强调客户端对叶子和容器使用的一致性,这是它区别于普通树的核心。

❌ 误区2:透明组合一定比安全组合好
透明组合牺牲了安全性,增加了运行时错误风险。在多数业务场景中,安全组合更符合单一职责原则。

❌ 误区3:组合模式适用于所有树形场景
如果树深度过大或节点过多,递归遍历可能导致栈溢出。此时应配合懒加载迭代器模式使用。

💡 面试高频追问

  • 组合模式的好处? → 客户端无需区分叶子与容器,统一操作,易于扩展。
  • 透明组合与安全组合的区别? → 管理子节点方法的位置不同,前者放在抽象构件,后者只放在容器构件。
  • 如果只想遍历叶子节点,不想遍历容器,怎么办? → 在 Component 接口中定义 isLeaf() 方法,或使用访问者模式对不同节点类型分别处理。
  • 组合模式会导致大对象产生吗?如何解决? → 会。解决方式是懒加载(只加载当前层,展开时再加载子节点)或分页处理。
  • Java 中哪里用了组合模式? → AWT/Swing 的 ContainerComponent、Spring Security 的 SecurityConfigurer

🎉 恭喜:如果你能立刻画出文件系统的树形结构,说出透明组合与安全组合的区别,并理解 Spring Security 为什么用 HttpSecurity.apply() 统一处理配置,你已经掌握了组合模式的核心。面试中遇到“如何统一处理树形结构”的问题,这将是你的完美答案。


11. 六大设计原则在组合模式中的体现

设计原则 在组合模式中的体现
单一职责原则(SRP) 安全组合中,叶子只负责自身业务,容器负责管理子节点和业务委托
开闭原则(OCP) 新增组件类型(如 ShortcutNode)无需修改现有代码
里氏替换原则(LSP) 任何 Component 子类都可替换父类,不破坏递归逻辑
依赖倒置原则(DIP) 客户端依赖抽象 Component,不依赖具体叶子或容器
接口隔离原则(ISP) 安全组合通过精简的 Component 接口遵守 ISP,透明组合则有所违反
迪米特法则(LoD) 客户端只与 Component 交互,不了解内部层次细节

12. 总结

组合模式是处理树形层次结构的首选方案,它通过统一叶子与容器的接口,让客户端可以一致地处理单个对象和对象集合。在文件系统、菜单、权限树等场景中,组合模式能大幅简化递归操作,提升系统的可扩展性。

最终建议:遇到“部分-整体”层次结构时,优先使用安全组合模式,将管理子节点的方法放在容器中;只有在需要完全透明且能容忍运行时异常时,才采用透明组合。如果树深度过大,配合懒加载或迭代器模式使用,避免栈溢出。


🧭 《Java 23 种设计模式:从踩坑到精通》快速导航

🔔 关注《Java 23 种设计模式:从踩坑到精通》,用 25 篇文章彻底吃透设计模式。
📦 福利预告:全系列代码及 UML 源码将在完结时统一打包开放,点击「关注」「收藏」第一时间获取。
🚀 下一篇:装饰器模式 —— 比继承更灵活的扩展方式,你用过吗?🚧 即将发布,敬请关注!

📌 除了设计模式,我也在深挖智能物流实战(WMS、托盘调度、机器学习落地)。欢迎点击头像,看看专栏 《出版社物流WMS智能调度实战》。技术相通,思路可鉴。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐