Skip to content

组合模式 (Composite Pattern)

一、定义

一句话概括:将对象组合成树形结构以表示"部分-整体"的层次结构,使客户端对单个对象和组合对象的使用具有一致性。

官方定义(GoF):Compose objects into tree structures to represent part-whole hierarchies. Composite lets clients treat individual objects and compositions of objects uniformly.

组合模式的核心思想是:用统一的方式处理叶子节点(单个对象)和组合节点(容器对象),客户端无需区分它们,可以一致地操作整个树形结构。


二、解决的问题

2.1 什么场景下需要组合模式?

  • 树形结构:组织架构(公司→部门→员工)、文件系统(文件夹→子文件夹→文件)、菜单系统(菜单→子菜单→菜单项)。
  • 统一操作:需要对整体和部分执行相同操作,如统计大小、遍历、复制、删除等。
  • 递归嵌套:结构本身是递归的,一个容器可以包含其他容器或叶子节点。

2.2 不用组合模式会有什么问题?

java
// 假设我们要处理一个文件系统
class File {
    String name;
    int size;
}

class Folder {
    String name;
    List<Folder> subFolders;  // 只能放文件夹
    List<File> files;          // 只能放文件
}

// 客户端代码必须区分处理:
void calculateSize(Object obj) {
    if (obj instanceof File) {
        return ((File) obj).size;
    } else if (obj instanceof Folder) {
        int total = 0;
        for (File f : ((Folder) obj).files) total += f.size;
        for (Folder f : ((Folder) obj).subFolders) total += calculateSize(f);
        return total;
    }
}
问题1:客户端必须区分处理叶子和容器,if-else 泛滥
问题2:无法统一递归遍历,每种节点类型需要单独处理
问题3:新增节点类型需要修改所有客户端代码,违反开闭原则
问题4:代码重复,每个操作都需要类型判断

三、结构

3.1 文字描述

组合模式包含以下角色:

  • Component(抽象构件):定义叶子和容器的公共接口。
  • Leaf(叶子节点):树形结构中的末端节点,没有子节点。
  • Composite(容器节点):包含子节点(Component 类型),实现子节点的管理方法。

3.2 透明组合模式

┌─────────────────────────────────────┐
│      <<interface>> Component        │
├─────────────────────────────────────┤
│ + add(Component)                    │
│ + remove(Component)                 │
│ + getChild(int)                     │
│ + operation()                       │
└────────────┬────────────────────────┘

    ┌────────┴────────┐
    │                 │
┌───┴──────────┐  ┌──┴──────────────────┐
│    Leaf       │  │    Composite         │
├──────────────┤  ├──────────────────────┤
│ + operation()│  │ - children: List<Comp>│
│              │  │ + add(Component)      │
│              │  │ + remove(Component)   │
│              │  │ + getChild(int)       │
│              │  │ + operation()         │
│              │  │   for child: op()     │
└──────────────┘  └──────────────────────┘

3.3 安全组合模式

┌─────────────────────────────────────┐
│      <<interface>> Component        │
├─────────────────────────────────────┤
│ + operation()                       │
└────────────┬────────────────────────┘

    ┌────────┴────────┐
    │                 │
┌───┴──────────┐  ┌──┴──────────────────┐
│    Leaf       │  │    Composite         │
├──────────────┤  ├──────────────────────┤
│ + operation()│  │ - children: List<Comp>│
└──────────────┘  │ + add(Component)      │
                  │ + remove(Component)   │
                  │ + getChild(int)       │
                  │ + operation()         │
                  └──────────────────────┘

四、代码实现

4.1 基础实现

场景:文件系统

java
// ============ Component:抽象构件 ============
abstract class FileSystemNode {
    protected String name;

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

    public abstract void display(int depth);
    public abstract int getSize();

    // 默认实现:叶子节点不支持
    public void add(FileSystemNode node) {
        throw new UnsupportedOperationException();
    }

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

// ============ Leaf:叶子节点(文件)============
class File extends FileSystemNode {
    private int size;

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

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

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

// ============ Composite:容器节点(文件夹)============
class Folder extends FileSystemNode {
    private List<FileSystemNode> children = new ArrayList<>();

    public Folder(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 display(int depth) {
        System.out.println("  ".repeat(depth) + "📁 " + name + "/");
        for (FileSystemNode child : children) {
            child.display(depth + 1);
        }
    }

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

// ============ 客户端测试 ============
public class CompositeDemo {
    public static void main(String[] args) {
        // 构建文件树
        Folder root = new Folder("root");
        Folder docs = new Folder("docs");
        Folder images = new Folder("images");

        docs.add(new File("readme.md", 10));
        docs.add(new File("design.pdf", 500));

        images.add(new File("logo.png", 200));
        images.add(new File("banner.jpg", 800));

        root.add(docs);
        root.add(images);
        root.add(new File("config.xml", 5));

        // 统一显示
        root.display(0);
        System.out.println("总大小: " + root.getSize() + "KB");
    }
}

4.2 进阶实现

4.2.1 透明组合 vs 安全组合

java
// ====== 透明组合:所有方法在 Component 中定义 ======
// 优点:客户端无需区分叶子/容器,完全透明
// 缺点:叶子节点继承了无用的 add/remove 方法,调用可能抛异常

// ====== 安全组合:管理方法只在 Composite 中定义 ======
interface Component {
    void operation();
}

class Leaf implements Component {
    @Override
    public void operation() {
        System.out.println("Leaf operation");
    }
}

class Composite implements Component {
    private List<Component> children = new ArrayList<>();

    // 管理方法只在 Composite 中定义(安全)
    public void add(Component c) { children.add(c); }
    public void remove(Component c) { children.remove(c); }

    @Override
    public void operation() {
        for (Component child : children) {
            child.operation();
        }
    }
}

// 使用安全组合时,客户端需要知道节点类型才能调用 add/remove
Composite composite = (Composite) node;  // 需要类型转换
composite.add(new Leaf());

4.2.2 递归遍历与迭代器

java
// ============ 深度优先遍历 ============
class FileSystem {
    // 深度优先:先遍历子节点
    public static void dfs(FileSystemNode node) {
        System.out.println(node.name);
        if (node instanceof Folder) {
            for (FileSystemNode child : ((Folder) node).getChildren()) {
                dfs(child);
            }
        }
    }

    // 广度优先:使用队列
    public static void bfs(FileSystemNode root) {
        Queue<FileSystemNode> queue = new LinkedList<>();
        queue.offer(root);
        while (!queue.isEmpty()) {
            FileSystemNode node = queue.poll();
            System.out.println(node.name);
            if (node instanceof Folder) {
                for (FileSystemNode child : ((Folder) node).getChildren()) {
                    queue.offer(child);
                }
            }
        }
    }

    // 过滤:查找所有大于 100KB 的文件
    public static List<File> findLargeFiles(FileSystemNode node, int threshold) {
        List<File> result = new ArrayList<>();
        if (node instanceof File && node.getSize() > threshold) {
            result.add((File) node);
        } else if (node instanceof Folder) {
            for (FileSystemNode child : ((Folder) node).getChildren()) {
                result.addAll(findLargeFiles(child, threshold));
            }
        }
        return result;
    }
}

4.2.3 组合模式实现规则引擎

java
// ============ 规则组合 ============
interface Rule {
    boolean evaluate(Map<String, Object> context);
}

// 叶子规则:具体条件
class AgeRule implements Rule {
    private int minAge;
    public AgeRule(int minAge) { this.minAge = minAge; }

    @Override
    public boolean evaluate(Map<String, Object> context) {
        int age = (int) context.getOrDefault("age", 0);
        return age >= minAge;
    }
}

class VipRule implements Rule {
    @Override
    public boolean evaluate(Map<String, Object> context) {
        return (boolean) context.getOrDefault("isVip", false);
    }
}

// 组合规则:And / Or
class AndRule implements Rule {
    private List<Rule> rules = new ArrayList<>();

    public void add(Rule rule) { rules.add(rule); }

    @Override
    public boolean evaluate(Map<String, Object> context) {
        return rules.stream().allMatch(r -> r.evaluate(context));
    }
}

class OrRule implements Rule {
    private List<Rule> rules = new ArrayList<>();

    public void add(Rule rule) { rules.add(rule); }

    @Override
    public boolean evaluate(Map<String, Object> context) {
        return rules.stream().anyMatch(r -> r.evaluate(context));
    }
}

// 测试
// AndRule rule = new AndRule();
// rule.add(new AgeRule(18));
// rule.add(new VipRule());
// rule.evaluate(Map.of("age", 25, "isVip", true));  // true

4.3 生产级实现

Spring Boot 动态菜单系统

java
// ============ Component:菜单组件 ============
interface MenuComponent {
    Long getId();
    String getName();
    String getIcon();
    String getPath();
    List<MenuComponent> getChildren();
    boolean hasChildren();
}

// ============ Leaf:菜单项(叶子)============
class MenuItem implements MenuComponent {
    private Long id;
    private String name;
    private String icon;
    private String path;
    private String permission;  // 权限标识

    public MenuItem(Long id, String name, String icon, String path, String permission) {
        this.id = id;
        this.name = name;
        this.icon = icon;
        this.path = path;
        this.permission = permission;
    }

    @Override public Long getId() { return id; }
    @Override public String getName() { return name; }
    @Override public String getIcon() { return icon; }
    @Override public String getPath() { return path; }
    @Override public List<MenuComponent> getChildren() { return Collections.emptyList(); }
    @Override public boolean hasChildren() { return false; }
    public String getPermission() { return permission; }
}

// ============ Composite:菜单(容器)============
class Menu implements MenuComponent {
    private Long id;
    private String name;
    private String icon;
    private String path;
    private List<MenuComponent> children = new ArrayList<>();

    public Menu(Long id, String name, String icon, String path) {
        this.id = id;
        this.name = name;
        this.icon = icon;
        this.path = path;
    }

    public void addChild(MenuComponent component) {
        children.add(component);
    }

    @Override public Long getId() { return id; }
    @Override public String getName() { return name; }
    @Override public String getIcon() { return icon; }
    @Override public String getPath() { return path; }
    @Override public List<MenuComponent> getChildren() { return children; }
    @Override public boolean hasChildren() { return !children.isEmpty(); }
}

// ============ 菜单构建服务 ============
@Service
class MenuService {
    @Autowired
    private MenuRepository menuRepository;

    // 构建菜单树
    public List<MenuComponent> buildMenuTree(Long parentId) {
        List<MenuEntity> entities = menuRepository.findByParentId(parentId);
        List<MenuComponent> menus = new ArrayList<>();

        for (MenuEntity entity : entities) {
            if (entity.getType() == MenuType.CATALOG) {
                Menu menu = new Menu(entity.getId(), entity.getName(),
                    entity.getIcon(), entity.getPath());
                // 递归构建子菜单
                List<MenuComponent> children = buildMenuTree(entity.getId());
                for (MenuComponent child : children) {
                    menu.addChild(child);
                }
                menus.add(menu);
            } else {
                menus.add(new MenuItem(entity.getId(), entity.getName(),
                    entity.getIcon(), entity.getPath(), entity.getPermission()));
            }
        }
        return menus;
    }

    // 统一渲染菜单树为前端 JSON
    public List<Map<String, Object>> toTreeJson(List<MenuComponent> components) {
        List<Map<String, Object>> result = new ArrayList<>();
        for (MenuComponent comp : components) {
            Map<String, Object> node = new LinkedHashMap<>();
            node.put("id", comp.getId());
            node.put("name", comp.getName());
            node.put("icon", comp.getIcon());
            node.put("path", comp.getPath());
            if (comp.hasChildren()) {
                node.put("children", toTreeJson(comp.getChildren()));
            }
            result.add(node);
        }
        return result;
    }
}

五、优缺点

优点

优点说明
统一处理客户端无需区分叶子和容器,简化代码
易于扩展新增节点类型只需实现 Component 接口
递归结构天然支持递归操作,如遍历、统计、过滤
符合开闭原则新增组件类型不修改现有代码

缺点

缺点说明
类型限制困难难以限制容器中只能放特定类型的子节点,需要运行时检查
设计复杂需要提取公共接口,如果叶子和容器差异很大,抽象困难
透明性代价透明组合中,叶子节点继承了无用方法,调用时可能抛异常

六、适用场景

  1. 文件系统:文件夹和文件的树形结构。
  2. 组织架构:公司→部门→小组→员工。
  3. 菜单系统:后台管理系统的多级菜单。
  4. UI 组件:容器(Panel/Window)包含子组件(Button/Text)。
  5. 规则引擎:简单条件和复合条件(AND/OR)的组合。
  6. XML/HTML 解析:DOM 树中,元素节点和文本节点。
  7. 分类系统:商品分类的多级目录。

七、JDK / Spring 框架中的实际应用

7.1 JDK 中的组合模式

java
// 1. java.awt.Container 和 java.awt.Component
// Container 继承 Component,可以包含其他 Component
// Button、TextField 是叶子,Panel、Frame 是容器

// 2. java.util.Map 和 java.util.HashMap
// Map.putAll(Map) —— 将另一个 Map 的所有元素加入
// Map 本身可以嵌套 Map

// 3. javax.swing.JComponent 体系
// JPanel 是容器,JButton、JLabel 是叶子

7.2 Spring 中的组合模式

java
// 1. Spring Security 的 GrantedAuthority
// 权限可以嵌套(角色包含多个权限)

// 2. Spring Expression Language (SpEL)
// 表达式可以是简单表达式,也可以是复合表达式(AND/OR/NOT)

// 3. Spring Cloud Gateway 的 Route Predicate
// Route 可以包含多个 Predicate(AND 组合)

八、与其他模式的关系

模式关系
装饰器模式装饰器和组合模式结构相似,但意图不同:装饰器为对象添加职责,组合模式统一处理整体和部分。装饰器只有一个子节点,组合可以有多个。
享元模式组合模式中的叶子节点可以使用享元模式共享,减少内存占用。
迭代器模式常与组合模式配合,遍历树形结构。
访问者模式访问者模式可以对组合模式中的不同节点执行不同操作,解决组合模式中操作分发困难的问题。
责任链模式组合模式中,请求可以从子节点向上传递到父节点。

九、面试常见问题

Q1:透明组合模式和安全组合模式有什么区别?各有什么优缺点?

  • 透明组合:管理子节点的方法(add/remove)定义在 Component 接口中。优点是客户端无需区分叶子和容器,完全透明;缺点是叶子节点继承了无用方法,调用时可能抛出 UnsupportedOperationException
  • 安全组合:管理子节点的方法只定义在 Composite 类中。优点是安全,叶子节点不会有无用方法;缺点是客户端需要知道具体类型才能调用 add/remove,失去了透明性。

Q2:组合模式中如何处理叶子节点的特殊操作?

:有三种方式:

  1. Component 接口中定义所有方法,叶子节点抛出 UnsupportedOperationException(透明组合)。
  2. 只在具体类中定义特殊方法,客户端通过 instanceof 判断后调用(安全组合)。
  3. 使用访问者模式,让访问者根据不同节点类型执行不同操作。

Q3:组合模式和装饰器模式有什么区别?

  • 组合模式关注"整体-部分"关系,一个容器可以有多个子节点,形成树形结构。
  • 装饰器模式关注"功能增强",一个装饰器只包装一个对象,形成链式结构。
  • 组合模式中,客户端统一对待叶子和容器;装饰器模式中,客户端通常不关心装饰链。

Q4:如何在组合模式中实现缓存优化?

:在 Composite 节点中缓存计算结果,当子节点变化时失效缓存。例如,文件夹缓存总大小,当子文件/文件夹变化时重新计算。这可以避免每次遍历整棵树带来的性能开销。

Q5:组合模式中如何限制容器只能放特定类型的子节点?

:在 add 方法中添加类型检查,或在子类中覆盖 add 方法限制类型。也可以使用泛型 <T extends Component> 来限制。但完全限制类型通常需要放弃透明性,因为 Component 接口无法知道子类类型。