【无标题】

本文是【GoF设计模式】系列第11篇

前言

为什么需要组合模式?

假设在做一个文件管理系统,需要统计某个文件夹的总大小。文件夹里有文件,也有子文件夹,子文件夹里还有文件和文件夹------这是一棵递归的树。

如果文件和文件夹是两个完全不同的类,代码会变成这样:

java 复制代码
if (node instanceof File) {
    total += ((File) node).getSize();
} else if (node instanceof Folder) {
    for (Object child : ((Folder) node).getChildren()) {
        // 又要判断 child 是文件还是文件夹......
    }
}

每多一层嵌套,就多一层 instanceof。更麻烦的是,如果将来新增一种"快捷方式"节点,所有遍历逻辑都要改。

组合模式的核心思路:让文件和文件夹实现同一个接口,不管是文件还是文件夹,都用 getSize() 一行调用搞定。文件返回自身大小,文件夹递归汇总子节点大小------调用者完全不需要区分。

概念

组合模式(Composite Pattern)是一种结构型设计模式 ,核心思想是将对象组织成树状结构,让单个对象和组合对象实现相同的接口,客户端无需区分就能统一处理

拆解三个关键点:

  • 树状结构:对象之间是"部分-整体"的层次关系,比如文件夹包含文件、部门包含员工
  • 统一接口:叶子节点和组合节点实现同一套接口,调用方式完全一致
  • 递归处理:组合节点的操作会递归委托给子节点,最终汇聚结果

组合模式涉及三个角色:

  • 组件(Component):定义所有节点的公共接口,是叶子和组合的统一抽象
  • 叶子(Leaf):树的末端节点,不再包含子节点,直接实现业务逻辑
  • 组合(Composite):容器节点,持有子节点列表,操作时递归委托给子节点

#mermaid-svg-ueWIqY1ZGtm5TMtp{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-ueWIqY1ZGtm5TMtp .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-ueWIqY1ZGtm5TMtp .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-ueWIqY1ZGtm5TMtp .error-icon{fill:#552222;}#mermaid-svg-ueWIqY1ZGtm5TMtp .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-ueWIqY1ZGtm5TMtp .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-ueWIqY1ZGtm5TMtp .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-ueWIqY1ZGtm5TMtp .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-ueWIqY1ZGtm5TMtp .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-ueWIqY1ZGtm5TMtp .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-ueWIqY1ZGtm5TMtp .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-ueWIqY1ZGtm5TMtp .marker{fill:#333333;stroke:#333333;}#mermaid-svg-ueWIqY1ZGtm5TMtp .marker.cross{stroke:#333333;}#mermaid-svg-ueWIqY1ZGtm5TMtp svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-ueWIqY1ZGtm5TMtp p{margin:0;}#mermaid-svg-ueWIqY1ZGtm5TMtp g.classGroup text{fill:#9370DB;stroke:none;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:10px;}#mermaid-svg-ueWIqY1ZGtm5TMtp g.classGroup text .title{font-weight:bolder;}#mermaid-svg-ueWIqY1ZGtm5TMtp .cluster-label text{fill:#333;}#mermaid-svg-ueWIqY1ZGtm5TMtp .cluster-label span{color:#333;}#mermaid-svg-ueWIqY1ZGtm5TMtp .cluster-label span p{background-color:transparent;}#mermaid-svg-ueWIqY1ZGtm5TMtp .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-ueWIqY1ZGtm5TMtp .cluster text{fill:#333;}#mermaid-svg-ueWIqY1ZGtm5TMtp .cluster span{color:#333;}#mermaid-svg-ueWIqY1ZGtm5TMtp .nodeLabel,#mermaid-svg-ueWIqY1ZGtm5TMtp .edgeLabel{color:#131300;}#mermaid-svg-ueWIqY1ZGtm5TMtp .edgeLabel .label rect{fill:#ECECFF;}#mermaid-svg-ueWIqY1ZGtm5TMtp .label text{fill:#131300;}#mermaid-svg-ueWIqY1ZGtm5TMtp .labelBkg{background:#ECECFF;}#mermaid-svg-ueWIqY1ZGtm5TMtp .edgeLabel .label span{background:#ECECFF;}#mermaid-svg-ueWIqY1ZGtm5TMtp .classTitle{font-weight:bolder;}#mermaid-svg-ueWIqY1ZGtm5TMtp .node rect,#mermaid-svg-ueWIqY1ZGtm5TMtp .node circle,#mermaid-svg-ueWIqY1ZGtm5TMtp .node ellipse,#mermaid-svg-ueWIqY1ZGtm5TMtp .node polygon,#mermaid-svg-ueWIqY1ZGtm5TMtp .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-ueWIqY1ZGtm5TMtp .divider{stroke:#9370DB;stroke-width:1;}#mermaid-svg-ueWIqY1ZGtm5TMtp g.clickable{cursor:pointer;}#mermaid-svg-ueWIqY1ZGtm5TMtp g.classGroup rect{fill:#ECECFF;stroke:#9370DB;}#mermaid-svg-ueWIqY1ZGtm5TMtp g.classGroup line{stroke:#9370DB;stroke-width:1;}#mermaid-svg-ueWIqY1ZGtm5TMtp .classLabel .box{stroke:none;stroke-width:0;fill:#ECECFF;opacity:0.5;}#mermaid-svg-ueWIqY1ZGtm5TMtp .classLabel .label{fill:#9370DB;font-size:10px;}#mermaid-svg-ueWIqY1ZGtm5TMtp .relation{stroke:#333333;stroke-width:1;fill:none;}#mermaid-svg-ueWIqY1ZGtm5TMtp .dashed-line{stroke-dasharray:3;}#mermaid-svg-ueWIqY1ZGtm5TMtp .dotted-line{stroke-dasharray:1 2;}#mermaid-svg-ueWIqY1ZGtm5TMtp #compositionStart,#mermaid-svg-ueWIqY1ZGtm5TMtp .composition{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-ueWIqY1ZGtm5TMtp #compositionEnd,#mermaid-svg-ueWIqY1ZGtm5TMtp .composition{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-ueWIqY1ZGtm5TMtp #dependencyStart,#mermaid-svg-ueWIqY1ZGtm5TMtp .dependency{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-ueWIqY1ZGtm5TMtp #dependencyStart,#mermaid-svg-ueWIqY1ZGtm5TMtp .dependency{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-ueWIqY1ZGtm5TMtp #extensionStart,#mermaid-svg-ueWIqY1ZGtm5TMtp .extension{fill:transparent!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-ueWIqY1ZGtm5TMtp #extensionEnd,#mermaid-svg-ueWIqY1ZGtm5TMtp .extension{fill:transparent!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-ueWIqY1ZGtm5TMtp #aggregationStart,#mermaid-svg-ueWIqY1ZGtm5TMtp .aggregation{fill:transparent!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-ueWIqY1ZGtm5TMtp #aggregationEnd,#mermaid-svg-ueWIqY1ZGtm5TMtp .aggregation{fill:transparent!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-ueWIqY1ZGtm5TMtp #lollipopStart,#mermaid-svg-ueWIqY1ZGtm5TMtp .lollipop{fill:#ECECFF!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-ueWIqY1ZGtm5TMtp #lollipopEnd,#mermaid-svg-ueWIqY1ZGtm5TMtp .lollipop{fill:#ECECFF!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-ueWIqY1ZGtm5TMtp .edgeTerminals{font-size:11px;line-height:initial;}#mermaid-svg-ueWIqY1ZGtm5TMtp .classTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-ueWIqY1ZGtm5TMtp .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-ueWIqY1ZGtm5TMtp .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-ueWIqY1ZGtm5TMtp :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 实现
实现
聚合
<<interface>>
Component
+operation()
Leaf
+operation()
Composite
-children: List<Component>
+operation()
+add(Component)
+remove(Component)

Component 定义统一接口,LeafComposite 都实现它。Composite 内部持有一个 List<Component>,可以存放 Leaf 也可以嵌套其他 Composite。调用 Composite 的操作时,它会遍历子节点逐个调用------这就是递归的核心。

实现

透明方式(GoF 原版基础实现)

组合模式的实现分为以下几个步骤:

  1. 定义 Component 接口,声明公共操作及子节点管理方法
  2. 定义 Leaf 实现 Component,叶子节点不支持子节点管理,调用时抛出异常
  3. 定义 Composite 实现 Component,持有子节点列表,操作时递归遍历子节点
java 复制代码
// Component:统一接口,包含子节点管理方法
interface Component {
    public void operation();
    public void add(Component component);
    public void remove(Component component);
    public Component getChild(int index);
}

// Leaf:叶子节点,不支持子节点管理
class Leaf implements Component {
    public void operation() {
        System.out.println("叶子节点的操作");
    }
    public void add(Component component) {
        throw new UnsupportedOperationException("叶子节点不支持添加子节点");
    }
    public void remove(Component component) {
        throw new UnsupportedOperationException("叶子节点不支持移除子节点");
    }
    public Component getChild(int index) {
        throw new UnsupportedOperationException("叶子节点不支持获取子节点");
    }
}

// Composite:组合节点,递归处理子节点
class Composite implements Component {
    private List<Component> children = new ArrayList<>();

    public void operation() {
        System.out.println("组合节点的操作");
        for (Component child : children) {
            child.operation();  // 递归调用
        }
    }
    public void add(Component component) {
        children.add(component);
    }
    public void remove(Component component) {
        children.remove(component);
    }
    public Component getChild(int index) {
        return children.get(index);
    }
}

引入一个例子:「公司组织架构,员工和部门都有"获取薪资"的操作。员工返回自己的工资,部门返回所有下属的薪资之和------不管是员工还是部门,调用 getSalary() 的方式完全一样,调用者不需要区分拿到的是哪种」。

核心思路:把子节点管理方法放在 Component 接口中,客户端完全不需要区分叶子和组合,接口完全统一

java 复制代码
// Component:组织架构统一接口(透明方式:管理方法在接口中)
interface OrganizationComponent {
    public String getName();
    public double getSalary();
    public void printStructure(int depth);
    public void add(OrganizationComponent component);
    public void remove(OrganizationComponent component);
    public OrganizationComponent getChild(int index);
}

// Leaf:员工(被迫实现管理方法,调用时抛异常)
class Employee implements OrganizationComponent {
    private String name;
    private double salary;

    public Employee(String name, double salary) {
        this.name = name;
        this.salary = salary;
    }
    public String getName() {
        return name;
    }
    public double getSalary() {
        return salary;  // 员工返回自身薪资
    }
    public void printStructure(int depth) {
        System.out.println("  ".repeat(depth) + "- " + name + " (¥" + salary + ")");
    }
    public void add(OrganizationComponent component) {
        throw new UnsupportedOperationException("员工节点不支持添加子节点");
    }
    public void remove(OrganizationComponent component) {
        throw new UnsupportedOperationException("员工节点不支持移除子节点");
    }
    public OrganizationComponent getChild(int index) {
        throw new UnsupportedOperationException("员工节点不支持获取子节点");
    }
}

// Composite:部门(真正实现管理方法)
class Department implements OrganizationComponent {
    private String name;
    private List<OrganizationComponent> members = new ArrayList<>();

    public Department(String name) {
        this.name = name;
    }
    public void add(OrganizationComponent component) {
        members.add(component);
    }
    public void remove(OrganizationComponent component) {
        members.remove(component);
    }
    public OrganizationComponent getChild(int index) {
        return members.get(index);
    }
    public String getName() {
        return name;
    }
    public double getSalary() {
        double total = 0;
        for (OrganizationComponent member : members) {
            total += member.getSalary();  // 递归汇总
        }
        return total;
    }
    public void printStructure(int depth) {
        System.out.println("  ".repeat(depth) + "+ " + name);
        for (OrganizationComponent member : members) {
            member.printStructure(depth + 1);  // 统一调用
        }
    }
}

安全方式

安全方式的实现步骤与透明方式类似,区别在于子节点管理方法只放在 Composite 中:

  1. 定义 Component 接口,只声明公共操作
  2. 定义 Leaf 实现 Component,只关注自身业务逻辑
  3. 定义 Composite 实现 Component,额外提供子节点管理方法
java 复制代码
// Component:只定义公共行为
interface Component {
    public void operation();
}

// Leaf:只实现公共行为
class Leaf implements Component {
    public void operation() {
        System.out.println("叶子节点的操作");
    }
}

// Composite:额外提供子节点管理方法
class Composite implements Component {
    private List<Component> children = new ArrayList<>();

    public void add(Component component) {
        children.add(component);
    }
    public void remove(Component component) {
        children.remove(component);
    }
    public void operation() {
        System.out.println("组合节点的操作");
        for (Component child : children) {
            child.operation();  // 递归调用
        }
    }
}

引入一个例子:「画布上的图形编辑器。矩形、圆形等基本图形只需要 draw() 方法,而"组合图形"还需要 addShape()removeShape() 来管理子图形。把 addShape() 塞给矩形没有意义------只有"组合图形"才需要管理子节点」。

核心思路:子节点管理方法只放在 Composite 中,Component 接口保持干净,编译期就能防止对叶子节点的误操作

java 复制代码
// Component:图形统一接口
interface Shape {
    public void draw();
    public double getArea();
}

// Leaf:基本图形
class Circle implements Shape {
    private double radius;
    public Circle(double radius) {
        this.radius = radius;
    }
    public void draw() {
        System.out.println("绘制圆形,半径: " + radius);
    }
    public double getArea() {
        return Math.PI * radius * radius;
    }
}

class Rectangle implements Shape {
    private double width;
    private double height;
    public Rectangle(double width, double height) {
        this.width = width;
        this.height = height;
    }
    public void draw() {
        System.out.println("绘制矩形: " + width + " x " + height);
    }
    public double getArea() {
        return width * height;
    }
}

// Composite:组合图形,只有它才有 add/remove
class CompositeShape implements Shape {
    private List<Shape> shapes = new ArrayList<>();

    public void add(Shape shape) {
        shapes.add(shape);
    }
    public void remove(Shape shape) {
        shapes.remove(shape);
    }
    public void draw() {
        System.out.println("绘制组合图形:");
        for (Shape shape : shapes) {
            shape.draw();  // 递归调用
        }
    }
    public double getArea() {
        double total = 0;
        for (Shape shape : shapes) {
            total += shape.getArea();  // 递归汇总
        }
        return total;
    }
}

如何选择

维度 透明方式 安全方式
接口统一性 完全统一,客户端无需区分类型 需要区分类型才能调用管理方法
安全性 叶子可能被误调管理方法 编译期就能防止误调
代码简洁度 叶子需要空实现或抛异常 叶子只实现自己需要的方法
推荐场景 树结构构建完成后统一遍历 构建和遍历阶段需要区分节点

简单记忆 :大多数场景用安全方式,只在需要完全透明的统一接口时用透明方式。

总结

组合模式的本质是用统一接口抹平叶子和组合的差异,让递归处理只写一份代码

什么时候用

  • 对象结构是树形的,存在"部分-整体"的层次关系
  • 希望客户端统一处理叶子和组合,不想写 instanceof 判断
  • 需要递归遍历或汇总整棵树的数据

什么时候不用

  • 对象结构不是树形,或者叶子和组合的操作差异很大
  • 需要严格限制"哪些节点能包含哪些子节点",组合模式本身不支持这种约束
  • 业务结构简单,统一接口反而增加了不必要的抽象层

缺点

  • 透明方式破坏类型安全 :叶子节点被迫实现 add()remove() 等管理方法(通常抛异常),编译期无法阻止误调,违反接口隔离原则
  • 难以约束节点组合关系Composite 的子节点列表是 Component 类型,无法限制"只能添加某种叶子",比如菜单系统中无法禁止在子菜单下放按钮

简单记忆

组合模式让"容器"和"物品"共享同一接口,递归处理只写一份。看到树形结构需要统一操作时,就是组合模式的信号。

相似模式区分

组合模式最容易与装饰器模式 混淆------两者结构几乎一样,都是递归组合、共享同一接口。其次与迭代器模式访问者模式在树结构场景下也常被提及。

模式 接口关系 核心意图 典型场景
组合 Component = Leaf = Composite 统一处理树中的叶子和组合 文件系统、组织架构、UI组件树
装饰器 Component = Decorator = ConcreteComponent 动态给对象叠加功能 Java I/O流、日志增强
迭代器 Iterator 独立于集合 顺序遍历,不暴露内部结构 集合遍历、数据库结果集
访问者 Element.accept(Visitor) 不改类的前提下添加新操作 编译器AST、文档多格式导出

简单记忆:组合管"树",装饰管"包",迭代管"遍历",访问管"扩展"。

组合 vs 装饰器

维度 组合模式 装饰器模式
核心意图 统一处理树中的单个对象和组合对象 动态地给对象添加新职责
结构差异 Composite 持有 List<Component>,递归调用多个子节点 Decorator 持有单个 Component 引用,增强后委托
关注点 部分-整体的层次关系 功能的动态增强
典型场景 文件系统、组织架构、UI组件树 Java I/O流包装、日志增强、权限校验

逐步区分法

  • 对象结构是树形 的,且需要统一处理叶子和组合 → 组合模式
  • 需要给单个对象 动态叠加功能(增强、装饰) → 装饰器模式
  • 关键看"递归多个子节点 "还是"增强单个对象"

组合 vs 迭代器

维度 组合模式 迭代器模式
核心意图 统一处理树中各节点,递归汇总结果 顺序遍历集合元素,不暴露内部结构
结构差异 节点自身包含递归逻辑 遍历逻辑独立于数据结构
关注点 部分-整体的层次关系 遍历的统一方式
典型场景 递归统计、递归渲染 for-each遍历、数据库结果集

逐步区分法

  • 需要递归处理 树形结构的每个节点 → 组合模式
  • 只需要线性遍历 一个集合 → 迭代器模式

组合 vs 访问者

维度 组合模式 访问者模式
核心意图 统一处理树中各节点 在不修改类的前提下为类族添加新操作
结构差异 节点自身实现操作逻辑 操作逻辑外置到 Visitor 中,通过双分派调用
关注点 部分-整体的层次关系 操作的开放扩展
典型场景 递归统计、递归渲染 编译器AST处理、文档导出多格式

逐步区分法

  • 需要统一处理 树中各节点的相同操作 → 组合模式
  • 需要为同一棵树添加多种不同操作 ,且不想改节点类 → 访问者模式
  • 两者经常配合使用:组合模式构建树结构,访问者模式在树上执行不同操作

练习题目

简易文件系统

题目描述 :使用组合模式实现一个简易文件系统。系统中有两种节点:文件夹(Folder)文件(File)。文件夹可以包含子节点,文件是叶子节点。两者共享统一接口,支持展示目录树和统计大小。

输入描述:第一行是整数 N(1 ≤ N ≤ 100),表示后续 N 行操作。接下来 N 行,每行格式为:

操作 格式 说明
创建文件夹 MF 文件夹名 父节点名 在指定父节点下创建文件夹
创建文件 MK 文件名 大小 父节点名 在指定父节点下创建文件,大小为正整数(KB)
展示结构 LS 节点名 展示该节点下的目录树
查询大小 SZ 节点名 查询该节点的大小(文件返回自身,文件夹返回所有子项之和)

根目录名称为 root,程序启动时自动创建。

输出描述

  • LS 操作:输出目录树,每个层级缩进 2 个空格,文件夹后标注 (DIR),文件后标注大小 (XKB)
  • SZ 操作:输出该节点的大小 节点名: XKB

输入示例

复制代码
10
MF src root
MF doc root
MK Main.java 5 src
MK Utils.java 3 src
MK README.md 2 doc
MF test src
MK Test.java 4 test
MK config.xml 1 root
SZ src
LS root

输出示例

复制代码
src: 12KB
root (DIR)
  src (DIR)
    Main.java (5KB)
    Utils.java (3KB)
    test (DIR)
      Test.java (4KB)
  doc (DIR)
    README.md (2KB)
  config.xml (1KB)

解题思路 :文件和文件夹是典型的"部分-整体"结构,天然适合组合模式。定义抽象类 FileNode 作为统一组件,包含获取大小、展示结构等公共方法。File 是叶子节点,直接返回自身大小;Folder 是组合节点,递归汇总子节点大小、递归展示子节点结构。使用安全方式,addChild() 只在 Folder 中提供。

java 复制代码
import java.util.*;

public class Main {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        Map<String, FileNode> map = new HashMap<>();
        Folder root = new Folder("root");
        map.put("root", root);

        int n = sc.nextInt();
        while (n-- > 0) {
            String op = sc.next();
            String name = sc.next();

            if ("MF".equals(op)) {
                String parentName = sc.next();
                Folder folder = new Folder(name);
                map.get(parentName).addChild(folder);
                map.put(name, folder);
            } else if ("MK".equals(op)) {
                int size = sc.nextInt();
                String parentName = sc.next();
                MyFile file = new MyFile(name, size);
                map.get(parentName).addChild(file);
            } else if ("LS".equals(op)) {
                map.get(name).display(0);
            } else if ("SZ".equals(op)) {
                System.out.println(name + ": " + map.get(name).getSize() + "KB");
            }
        }
    }
}

// Component:统一抽象
abstract class FileNode {
    protected String name;

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

    public String getName() {
        return name;
    }

    // 默认不支持添加子节点,Leaf 不用管
    public void addChild(FileNode node) {
        throw new UnsupportedOperationException("叶子节点不支持添加子节点");
    }

    // 子类必须实现
    public abstract void display(int depth);
    public abstract int getSize();
}

// Leaf:文件
class MyFile extends FileNode {
    private int size;

    public MyFile(String name, int size) {
        super(name);
        this.size = size;
    }

    public void display(int depth) {
        System.out.println("  ".repeat(depth) + name + " (" + size + "KB)");
    }

    public int getSize() {
        return size;
    }
}

// Composite:文件夹
class Folder extends FileNode {
    private List<FileNode> children = new ArrayList<>();

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

    public void addChild(FileNode node) {
        children.add(node);
    }

    public void display(int depth) {
        System.out.println("  ".repeat(depth) + name + " (DIR)");
        for (FileNode child : children) {
            child.display(depth + 1);  // 递归调用,不管子节点是文件还是文件夹
        }
    }

    public int getSize() {
        int total = 0;
        for (FileNode child : children) {
            total += child.getSize();  // 递归汇总
        }
        return total;
    }
}

扩展:实际项目中的组合模式

JDK 的 java.awt.Component

Java AWT/Swing 是组合模式的经典应用。Component(抽象类,等价于组合模式中的 Component 角色)是所有 UI 组件的基类,Container 是组合节点(可以包含子组件),ButtonLabel 等是叶子节点。调用 Container.repaint() 时会递归重绘所有子组件------客户端不需要知道哪些是容器、哪些是叶子。

java 复制代码
JPanel panel = new JPanel();           // 组合节点
panel.add(new JButton("点击我"));       // 叶子节点
panel.add(new JLabel("Hello"));        // 叶子节点
panel.repaint();                       // 递归重绘整棵组件树

MyBatis 动态 SQL 的 SqlNode

MyBatis 的动态 SQL 构建使用了组合模式。SqlNode 是组件接口,TextSqlNode(纯文本 SQL)是叶子,MixedSqlNode(混合 SQL 片段列表)是组合,IfSqlNodeWhereSqlNode 等是带条件的组合。执行时从根节点递归 apply(),最终拼出完整 SQL------不需要区分哪些是纯文本、哪些是动态片段。

组织架构薪资计算

公司→部门→小组→员工,天然的树形结构。员工返回自身薪资,部门递归汇总所有下属薪资。调用 company.getSalary() 一行代码就能得到整个公司的薪资总额,完全不需要写 instanceof 判断。

JUnit 测试套件

JUnit 的 @Suite 允许将多个测试类组合成一个测试套件,套件还可以嵌套套件,运行时统一执行。@Suite 就是组合节点,测试类就是叶子。CI 系统只需要运行顶层套件,JUnit 会递归执行所有子测试。

Spring Security 的权限体系

GrantedAuthority 可以组合成层次化权限。SimpleGrantedAuthority 是 Spring Security 标准的单个权限实现;聚合多个权限的容器(类似 CompositeGrantedAuthority)可以自行实现,用 contains() 方法递归查找子权限------客户端不需要知道权限树的结构。

现在可能还用不到这些,但等到需要处理树形结构、递归汇总数据的时候,会突然发现:"这不就是组合模式吗?"------那时候就真的懂了。

相关推荐
mqiqe1 小时前
面试题-MyBatis 面试篇
java·面试·mybatis
摇滚侠1 小时前
SpringMVC 入门到实战 @RequestMapping 14-24
java·spring
云烟成雨TD1 小时前
Spring AI Alibaba 1.x 系列【80】可观测集成
java·人工智能·spring
Filwaod2 小时前
MCP 接入模式对比:Agent - Gateway - 业务项目 vs Agent - Adapter - 业务项目
java·agent·mcp
kuonyuma2 小时前
MyBatis入门·注解操作
java·spring boot·mysql·spring·mybatis
码界索隆2 小时前
Python转Java系列:面向对象基础
java·开发语言·python
DIY源码阁2 小时前
JavaSwing酒店管理系统 - MySQL版
java·mysql·eclipse
不恋水的雨2 小时前
easyexcel快速填充大数据量不覆盖后面的行解决方式
java·excel·poi
Rain5092 小时前
1.3. Next.js与Nest.js在AI数据分析中的角色
前端·javascript·人工智能·后端·数据分析·node.js·ai编程