【设计模式】组合模式——树形结构的统一处理

一、从一个问题开始

假设你要开发一个文件系统,文件和文件夹需要统一管理:

java

arduino 复制代码
// 文件
public class File {
    private String name;
    private int size;
    
    public File(String name, int size) {
        this.name = name;
        this.size = size;
    }
    
    public void display() {
        System.out.println("文件:" + name + " (" + size + "KB)");
    }
}

// 文件夹
public class Folder {
    private String name;
    private List<File> files = new ArrayList<>();
    private List<Folder> subFolders = new ArrayList<>();
    
    public Folder(String name) {
        this.name = name;
    }
    
    public void addFile(File file) {
        files.add(file);
    }
    
    public void addFolder(Folder folder) {
        subFolders.add(folder);
    }
    
    public void display() {
        System.out.println("文件夹:" + name);
        for (File file : files) {
            file.display();
        }
        for (Folder folder : subFolders) {
            folder.display();
        }
    }
}

使用代码

java

ini 复制代码
public class Client {
    public static void main(String[] args) {
        // 创建文件
        File file1 = new File("a.txt", 10);
        File file2 = new File("b.txt", 20);
        File file3 = new File("c.txt", 30);
        
        // 创建文件夹
        Folder folder1 = new Folder("文档");
        folder1.addFile(file1);
        folder1.addFile(file2);
        
        Folder root = new Folder("根目录");
        root.addFile(file3);
        root.addFolder(folder1);
        
        // 显示
        root.display();
    }
}

问题:客户端需要区分文件和文件夹,用不同的方式处理。

问题 说明
处理方式不统一 文件和文件夹有不同的操作方式
递归操作复杂 每次都要写递归遍历代码
扩展困难 增加新类型的节点(如快捷方式),所有地方都要改

组合模式的解决方案 :让文件和文件夹实现同一接口,客户端可以统一处理。

二、组合模式是什么?

一句话定义 :将对象组合成树形结构 以表示"部分-整体"的层次结构。组合模式使得用户对单个对象和组合对象的使用具有一致性

通俗理解

  • 就像文件系统:文件和文件夹都可以"显示"、"重命名"、"删除"
  • 就像公司组织架构:员工和部门都可以"发工资"、"汇报工作"
  • 就像菜单系统:菜单项和子菜单都可以"显示"

角色结构

text

scss 复制代码
┌─────────────────────────────────────────────────────────┐
│                   Component(抽象组件)                   │
│              + operation()                              │
│              + add(Component)  // 可选                   │
│              + remove(Component) // 可选                 │
│              + getChild(int)  // 可选                    │
└─────────────────────────────────────────────────────────┘
                              △
              ┌───────────────┴───────────────┐
              │                               │
┌─────────────────────────┐     ┌─────────────────────────┐
│    Leaf(叶子节点)       │     │   Composite(容器节点)   │
│    - 没有子节点          │     │   - 有子节点             │
│    + operation()        │     │   + operation()         │
└─────────────────────────┘     │   + add(Component)      │
                                │   + remove(Component)   │
                                │   + getChild(int)       │
                                └─────────────────────────┘

三、两种实现方式

3.1 透明组合模式(安全组合模式)

在抽象组件中声明所有方法,叶子节点和容器节点实现同一接口。

java

csharp 复制代码
/**
 * 抽象组件:文件系统节点
 */
public abstract class FileSystemNode {
    protected String name;
    
    public FileSystemNode(String name) {
        this.name = name;
    }
    
    // 公共方法
    public abstract void display(int indent);
    public abstract int getSize();
    
    // 容器特有方法(叶子节点抛出异常或空实现)
    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 File extends FileSystemNode {
    private int size;
    
    public File(String name, int size) {
        super(name);
        this.size = size;
    }
    
    @Override
    public void display(int indent) {
        System.out.println("  ".repeat(indent) + "📄 " + name + " (" + size + "KB)");
    }
    
    @Override
    public int getSize() {
        return size;
    }
}

/**
 * 容器节点:文件夹
 */
public class Directory extends FileSystemNode {
    private List<FileSystemNode> children = new ArrayList<>();
    
    public Directory(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 children;
    }
    
    @Override
    public void display(int indent) {
        System.out.println("  ".repeat(indent) + "📁 " + name);
        for (FileSystemNode child : children) {
            child.display(indent + 1);
        }
    }
    
    @Override
    public int getSize() {
        int total = 0;
        for (FileSystemNode child : children) {
            total += child.getSize();
        }
        return total;
    }
}

3.2 安全组合模式

将容器特有方法单独定义在容器节点中,不在抽象组件中声明。

java

arduino 复制代码
/**
 * 抽象组件:只定义公共方法
 */
public interface FileSystemComponent {
    void display(int indent);
    int getSize();
}

/**
 * 叶子节点:文件
 */
public class SafeFile implements FileSystemComponent {
    private String name;
    private int size;
    
    public SafeFile(String name, int size) {
        this.name = name;
        this.size = size;
    }
    
    @Override
    public void display(int indent) {
        System.out.println("  ".repeat(indent) + "📄 " + name + " (" + size + "KB)");
    }
    
    @Override
    public int getSize() {
        return size;
    }
}

/**
 * 容器节点:文件夹(单独拥有添加/删除方法)
 */
public class SafeDirectory implements FileSystemComponent {
    private String name;
    private List<FileSystemComponent> children = new ArrayList<>();
    
    public SafeDirectory(String name) {
        this.name = name;
    }
    
    // 容器特有方法(不在接口中)
    public void add(FileSystemComponent component) {
        children.add(component);
    }
    
    public void remove(FileSystemComponent component) {
        children.remove(component);
    }
    
    public List<FileSystemComponent> getChildren() {
        return children;
    }
    
    @Override
    public void display(int indent) {
        System.out.println("  ".repeat(indent) + "📁 " + name);
        for (FileSystemComponent child : children) {
            child.display(indent + 1);
        }
    }
    
    @Override
    public int getSize() {
        int total = 0;
        for (FileSystemComponent child : children) {
            total += child.getSize();
        }
        return total;
    }
}

3.3 使用示例

java

ini 复制代码
public class Client {
    public static void main(String[] args) {
        // 创建文件
        FileSystemComponent file1 = new SafeFile("a.txt", 10);
        FileSystemComponent file2 = new SafeFile("b.txt", 20);
        FileSystemComponent file3 = new SafeFile("c.txt", 30);
        FileSystemComponent file4 = new SafeFile("d.txt", 40);
        
        // 创建文件夹
        SafeDirectory folder1 = new SafeDirectory("文档");
        folder1.add(file1);
        folder1.add(file2);
        
        SafeDirectory folder2 = new SafeDirectory("图片");
        folder2.add(file3);
        
        SafeDirectory root = new SafeDirectory("根目录");
        root.add(file4);
        root.add(folder1);
        root.add(folder2);
        
        // 统一显示
        root.display(0);
        
        System.out.println("\n总大小:" + root.getSize() + "KB");
    }
}

输出

text

scss 复制代码
📁 根目录
  📄 d.txt (40KB)
  📁 文档
    📄 a.txt (10KB)
    📄 b.txt (20KB)
  📁 图片
    📄 c.txt (30KB)

总大小:100KB

四、透明组合 vs 安全组合

对比项 透明组合模式 安全组合模式
接口设计 叶子节点和容器节点接口相同 容器节点有额外方法
安全性 叶子节点可能被误调用add/remove 编译期就能避免错误调用
透明性 客户端对叶子/容器完全透明 客户端需要区分叶子/容器
适用场景 叶子节点操作少,简单场景 需要类型安全,复杂场景
推荐程度 ⭐⭐⭐ ⭐⭐⭐⭐

五、实战场景

5.1 公司组织架构

java

csharp 复制代码
/**
 * 组织节点接口
 */
public interface OrganizationComponent {
    void display();
    double getSalary();
}

/**
 * 员工(叶子节点)
 */
public class Employee implements OrganizationComponent {
    private String name;
    private String position;
    private double salary;
    
    public Employee(String name, String position, double salary) {
        this.name = name;
        this.position = position;
        this.salary = salary;
    }
    
    @Override
    public void display() {
        System.out.println("  员工:" + name + "(" + position + ")");
    }
    
    @Override
    public double getSalary() {
        return salary;
    }
}

/**
 * 部门(容器节点)
 */
public 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);
    }
    
    @Override
    public void display() {
        System.out.println("部门:" + name);
        for (OrganizationComponent member : members) {
            member.display();
        }
    }
    
    @Override
    public double getSalary() {
        double total = 0;
        for (OrganizationComponent member : members) {
            total += member.getSalary();
        }
        return total;
    }
}

// 使用
public class Company {
    public static void main(String[] args) {
        // 创建员工
        Employee emp1 = new Employee("张三", "开发工程师", 15000);
        Employee emp2 = new Employee("李四", "测试工程师", 12000);
        Employee emp3 = new Employee("王五", "产品经理", 18000);
        Employee emp4 = new Employee("赵六", "HR", 10000);
        
        // 创建部门
        Department techDept = new Department("技术部");
        techDept.add(emp1);
        techDept.add(emp2);
        techDept.add(emp3);
        
        Department hrDept = new Department("人事部");
        hrDept.add(emp4);
        
        Department company = new Department("XX科技有限公司");
        company.add(techDept);
        company.add(hrDept);
        
        // 显示组织架构
        company.display();
        
        System.out.println("\n公司总薪资:" + company.getSalary() + "元");
    }
}

输出

text

复制代码
部门:XX科技有限公司
部门:技术部
  员工:张三(开发工程师)
  员工:李四(测试工程师)
  员工:王五(产品经理)
部门:人事部
  员工:赵六(HR)

公司总薪资:55000.0元

5.2 菜单系统

java

csharp 复制代码
/**
 * 菜单组件
 */
public interface MenuComponent {
    String getName();
    double getPrice();
    void print();
}

/**
 * 菜单项(叶子节点)
 */
public class MenuItem implements MenuComponent {
    private String name;
    private double price;
    private String description;
    
    public MenuItem(String name, double price, String description) {
        this.name = name;
        this.price = price;
        this.description = description;
    }
    
    @Override
    public String getName() { return name; }
    
    @Override
    public double getPrice() { return price; }
    
    @Override
    public void print() {
        System.out.println("  " + name + " - ¥" + price + "(" + description + ")");
    }
}

/**
 * 子菜单(容器节点)
 */
public class SubMenu implements MenuComponent {
    private String name;
    private List<MenuComponent> items = new ArrayList<>();
    
    public SubMenu(String name) {
        this.name = name;
    }
    
    public void add(MenuComponent item) {
        items.add(item);
    }
    
    public void remove(MenuComponent item) {
        items.remove(item);
    }
    
    @Override
    public String getName() { return name; }
    
    @Override
    public double getPrice() {
        return items.stream().mapToDouble(MenuComponent::getPrice).sum();
    }
    
    @Override
    public void print() {
        System.out.println(name + ":");
        for (MenuComponent item : items) {
            item.print();
        }
    }
}

// 使用
SubMenu drinkMenu = new SubMenu("饮品");
drinkMenu.add(new MenuItem("咖啡", 28, "现磨咖啡"));
drinkMenu.add(new MenuItem("奶茶", 18, "珍珠奶茶"));
drinkMenu.add(new MenuItem("果汁", 22, "鲜榨果汁"));

SubMenu foodMenu = new SubMenu("主食");
foodMenu.add(new MenuItem("汉堡", 35, "牛肉汉堡"));
foodMenu.add(new MenuItem("意面", 42, "番茄肉酱意面"));

SubMenu dessertMenu = new SubMenu("甜品");
dessertMenu.add(new MenuItem("蛋糕", 28, "提拉米苏"));
dessertMenu.add(new MenuItem("冰淇淋", 15, "香草冰淇淋"));

SubMenu fullMenu = new SubMenu("=== 完整菜单 ===");
fullMenu.add(drinkMenu);
fullMenu.add(foodMenu);
fullMenu.add(dessertMenu);

fullMenu.print();

5.3 XML/JSON 解析

java

typescript 复制代码
/**
 * JSON 节点接口
 */
public interface JsonNode {
    String toString();
    Object getValue();
}

/**
 * 基本类型节点(叶子)
 */
public class ValueNode implements JsonNode {
    private Object value;
    
    public ValueNode(Object value) {
        this.value = value;
    }
    
    @Override
    public Object getValue() {
        return value;
    }
    
    @Override
    public String toString() {
        if (value instanceof String) {
            return """ + value + """;
        }
        return String.valueOf(value);
    }
}

/**
 * 对象节点(容器)
 */
public class ObjectNode implements JsonNode {
    private Map<String, JsonNode> properties = new LinkedHashMap<>();
    
    public void put(String key, JsonNode value) {
        properties.put(key, value);
    }
    
    @Override
    public Object getValue() {
        Map<String, Object> result = new LinkedHashMap<>();
        properties.forEach((k, v) -> result.put(k, v.getValue()));
        return result;
    }
    
    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder("{");
        properties.forEach((k, v) -> {
            sb.append(""").append(k).append("":").append(v).append(",");
        });
        if (sb.length() > 1) sb.deleteCharAt(sb.length() - 1);
        sb.append("}");
        return sb.toString();
    }
}

/**
 * 数组节点(容器)
 */
public class ArrayNode implements JsonNode {
    private List<JsonNode> elements = new ArrayList<>();
    
    public void add(JsonNode element) {
        elements.add(element);
    }
    
    @Override
    public Object getValue() {
        return elements.stream().map(JsonNode::getValue).collect(Collectors.toList());
    }
    
    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder("[");
        for (JsonNode e : elements) {
            sb.append(e).append(",");
        }
        if (sb.length() > 1) sb.deleteCharAt(sb.length() - 1);
        sb.append("]");
        return sb.toString();
    }
}

六、常见面试题

Q1:组合模式的优缺点?

优点

优点 说明
统一处理 叶子节点和容器节点使用方式一致
易于扩展 增加新类型的节点不影响现有代码
符合开闭原则 客户端不依赖具体组件类
简化客户端代码 不需要写条件判断区分叶子/容器

缺点

缺点 说明
设计复杂 叶子节点和容器节点差异大时不好处理
类型安全 透明模式可能误调用叶子节点不支持的方法
遍历开销 深层树形结构遍历可能影响性能

Q2:什么时候用组合模式?

适合的场景

  • 需要表示树形结构(文件系统、组织架构、菜单)
  • 需要统一处理部分和整体
  • 可以对组合对象进行递归操作

不适合的场景

  • 树形结构很简单
  • 叶子节点和容器节点差异太大

Q3:组合模式和其他模式的关系?

模式 关系
迭代器模式 可以用迭代器遍历组合结构的子节点
访问者模式 可以对组合结构的每个节点执行操作
责任链模式 可以组合成树形责任链

七、总结与速记卡

核心要点

text

复制代码
组合模式 = 抽象组件 + 叶子节点 + 容器节点 + 树形结构

代码模板

java

csharp 复制代码
// 1. 抽象组件
public abstract class Component {
    public abstract void operation();
    public void add(Component c) { /* 空实现或抛异常 */ }
    public void remove(Component c) { /* 空实现或抛异常 */ }
}

// 2. 叶子节点
public class Leaf extends Component {
    public void operation() { /* 具体实现 */ }
}

// 3. 容器节点
public class Composite extends Component {
    private List<Component> children = new ArrayList<>();
    
    public void add(Component c) { children.add(c); }
    public void remove(Component c) { children.remove(c); }
    
    public void operation() {
        for (Component child : children) {
            child.operation();
        }
    }
}

一句话记忆

场景 方案
树形结构,需要统一处理 组合模式 ✅
文件和文件夹 组合模式 ✅
组织架构 组合模式 ✅
菜单系统 组合模式 ✅
相关推荐
乐观的山里娃8 小时前
【设计模式 12】原型:复制成功
设计模式
傻啦嘿哟8 小时前
办公Agent与人工审核的“握手协议”:关键操作二次确认的设计模式
设计模式
hssfscv9 小时前
软件设计师2021上、下上午题错题解析+2022上、下下午题训练5道 练习真题训练16
笔记·设计模式·uml
乐观的山里娃10 小时前
【设计模式 13】命令:覆水能收
设计模式
乐观的山里娃11 小时前
【设计模式 11】建造者:配置像天书
设计模式
看山是山_Lau1 天前
建造者模式:复杂对象如何一步步构建
设计模式·建造者模式
霸道流氓气质1 天前
业务链路追踪日志设计模式 — 从原理到实践
设计模式
nnsix2 天前
设计模式 - 建造者模式 笔记
笔记·设计模式·建造者模式
cui17875682 天前
矩阵拼团 + 复购拼团:新零售最稳的复购模式,规则简单
大数据·人工智能·设计模式·零售