【设计模式】 组合模式(Composite)大白话讲解

组合模式(Composite)大白话讲解

一句话概括

就像文件夹和文件的关系:文件夹可以包含文件,也可以包含其他文件夹,但你对它们的操作是统一的


现实生活比喻

场景1:公司组织架构

  • 员工:普通员工(叶子节点)
  • 经理:可以管理多个员工(组合节点)
  • CEO:可以管理多个经理(组合节点)
  • 操作:计算总工资、发布通知等操作对所有人都一样

场景2:菜单系统

  • 菜单项:具体的菜(叶子节点)
  • 子菜单:包含多个菜单项(组合节点)
  • 主菜单:包含多个子菜单(组合节点)
  • 操作:显示菜单、计算总价等操作对各级都一样

完整代码示例

场景:文件系统

java 复制代码
/**
 * 组合模式 - 文件系统示例
 */
public class Main {
    public static void main(String[] args) {
        System.out.println("=== 构建文件系统 ===");
        
        // 创建文件(叶子节点)
        FileSystemComponent file1 = new File("简历.doc", 200);
        FileSystemComponent file2 = new File("照片.jpg", 1500);
        FileSystemComponent file3 = new File("报告.pdf", 500);
        FileSystemComponent file4 = new File("代码.java", 100);
        FileSystemComponent file5 = new File("数据.txt", 50);
        
        // 创建文件夹(组合节点)
        FileSystemComponent workFolder = new Folder("工作资料");
        FileSystemComponent personalFolder = new Folder("个人文件");
        FileSystemComponent projectFolder = new Folder("项目文档");
        FileSystemComponent rootFolder = new Folder("我的电脑");
        
        // 构建树形结构
        workFolder.add(file1);
        workFolder.add(file3);
        
        personalFolder.add(file2);
        personalFolder.add(file4);
        
        projectFolder.add(file5);
        workFolder.add(projectFolder);  // 文件夹中可以包含文件夹
        
        rootFolder.add(workFolder);
        rootFolder.add(personalFolder);
        
        // 统一操作 - 显示整个文件系统
        System.out.println("\n=== 文件系统结构 ===");
        rootFolder.display();
        
        // 统一操作 - 计算总大小
        System.out.println("\n=== 统计信息 ===");
        System.out.println("工作资料大小: " + workFolder.getSize() + "KB");
        System.out.println("个人文件大小: " + personalFolder.getSize() + "KB");
        System.out.println("整个电脑大小: " + rootFolder.getSize() + "KB");
        
        // 统一操作 - 搜索文件
        System.out.println("\n=== 搜索文件 ===");
        searchFile(rootFolder, "代码.java");
        searchFile(rootFolder, "不存在的文件.xyz");
    }
    
    // 统一的搜索方法,对文件和文件夹都适用
    public static void searchFile(FileSystemComponent component, String fileName) {
        if (component.search(fileName)) {
            System.out.println("找到文件: " + fileName);
        } else {
            System.out.println("未找到文件: " + fileName);
        }
    }
}

/**
 * 抽象组件 - 定义文件和文件夹的共同接口
 */
interface FileSystemComponent {
    void display();          // 显示
    int getSize();           // 获取大小
    boolean search(String fileName);  // 搜索文件
    void add(FileSystemComponent component);      // 添加(对文件无效)
    void remove(FileSystemComponent component);   // 删除(对文件无效)
}

/**
 * 叶子节点 - 文件
 */
class File implements FileSystemComponent {
    private String name;
    private int size;  // 文件大小,单位KB
    
    public File(String name, int size) {
        this.name = name;
        this.size = size;
    }
    
    @Override
    public void display() {
        System.out.println("文件: " + name + " (" + size + "KB)");
    }
    
    @Override
    public int getSize() {
        return size;
    }
    
    @Override
    public boolean search(String fileName) {
        return this.name.equals(fileName);
    }
    
    @Override
    public void add(FileSystemComponent component) {
        // 文件不能添加子组件,抛出异常或忽略
        throw new UnsupportedOperationException("文件不支持添加操作");
    }
    
    @Override
    public void remove(FileSystemComponent component) {
        // 文件不能删除子组件,抛出异常或忽略
        throw new UnsupportedOperationException("文件不支持删除操作");
    }
}

/**
 * 组合节点 - 文件夹
 */
class Folder implements FileSystemComponent {
    private String name;
    private List<FileSystemComponent> children = new ArrayList<>();
    
    public Folder(String name) {
        this.name = name;
    }
    
    @Override
    public void display() {
        System.out.println("文件夹: " + name);
        // 递归显示所有子组件
        for (FileSystemComponent child : children) {
            System.out.print("  ");
            child.display();
        }
    }
    
    @Override
    public int getSize() {
        int totalSize = 0;
        // 递归计算所有子组件的大小
        for (FileSystemComponent child : children) {
            totalSize += child.getSize();
        }
        return totalSize;
    }
    
    @Override
    public boolean search(String fileName) {
        // 先检查当前文件夹是否匹配
        if (this.name.equals(fileName)) {
            return true;
        }
        // 递归搜索所有子组件
        for (FileSystemComponent child : children) {
            if (child.search(fileName)) {
                return true;
            }
        }
        return false;
    }
    
    @Override
    public void add(FileSystemComponent component) {
        children.add(component);
    }
    
    @Override
    public void remove(FileSystemComponent component) {
        children.remove(component);
    }
}

运行结果

复制代码
=== 构建文件系统 ===

=== 文件系统结构 ===
文件夹: 我的电脑
  文件夹: 工作资料
    文件: 简历.doc (200KB)
    文件: 报告.pdf (500KB)
    文件夹: 项目文档
      文件: 数据.txt (50KB)
  文件夹: 个人文件
    文件: 照片.jpg (1500KB)
    文件: 代码.java (100KB)

=== 统计信息 ===
工作资料大小: 750KB
个人文件大小: 1600KB
整个电脑大小: 2350KB

=== 搜索文件 ===
找到文件: 代码.java
未找到文件: 不存在的文件.xyz

更实用的例子:商品分类系统

java 复制代码
/**
 * 电商平台商品分类系统
 */
public class ProductCategoryExample {
    public static void main(String[] args) {
        // 创建商品(叶子节点)
        ProductComponent phone1 = new Product("iPhone 15", 6999);
        ProductComponent phone2 = new Product("小米14", 3999);
        ProductComponent laptop1 = new Product("MacBook Pro", 12999);
        ProductComponent laptop2 = new Product("ThinkPad", 7999);
        ProductComponent tshirt = new Product("T恤", 99);
        ProductComponent jeans = new Product("牛仔裤", 199);
        
        // 创建分类(组合节点)
        ProductComponent electronics = new Category("电子产品");
        ProductComponent clothing = new Category("服装");
        ProductComponent phones = new Category("手机");
        ProductComponent laptops = new Category("笔记本电脑");
        ProductComponent root = new Category("所有商品");
        
        // 构建分类树
        phones.add(phone1);
        phones.add(phone2);
        
        laptops.add(laptop1);
        laptops.add(laptop2);
        
        electronics.add(phones);
        electronics.add(laptops);
        
        clothing.add(tshirt);
        clothing.add(jeans);
        
        root.add(electronics);
        root.add(clothing);
        
        // 统一操作
        System.out.println("=== 商品分类结构 ===");
        root.display();
        
        System.out.println("\n=== 价格统计 ===");
        System.out.println("电子产品总价值: " + electronics.getPrice() + "元");
        System.out.println("手机总价值: " + phones.getPrice() + "元");
        System.out.println("所有商品总价值: " + root.getPrice() + "元");
        
        System.out.println("\n=== 搜索商品 ===");
        searchProduct(root, "iPhone 15");
        searchProduct(root, "MacBook Pro");
    }
    
    public static void searchProduct(ProductComponent component, String productName) {
        if (component.search(productName)) {
            System.out.println("找到商品: " + productName);
        }
    }
}

/**
 * 商品组件接口
 */
interface ProductComponent {
    void display();
    double getPrice();
    boolean search(String productName);
    void add(ProductComponent component);
    void remove(ProductComponent component);
}

/**
 * 具体商品(叶子节点)
 */
class Product implements ProductComponent {
    private String name;
    private double price;
    
    public Product(String name, double price) {
        this.name = name;
        this.price = price;
    }
    
    @Override
    public void display() {
        System.out.println("商品: " + name + " - " + price + "元");
    }
    
    @Override
    public double getPrice() {
        return price;
    }
    
    @Override
    public boolean search(String productName) {
        return this.name.equals(productName);
    }
    
    @Override
    public void add(ProductComponent component) {
        throw new UnsupportedOperationException("商品不能添加子项");
    }
    
    @Override
    public void remove(ProductComponent component) {
        throw new UnsupportedOperationException("商品不能删除子项");
    }
}

/**
 * 商品分类(组合节点)
 */
class Category implements ProductComponent {
    private String name;
    private List<ProductComponent> children = new ArrayList<>();
    
    public Category(String name) {
        this.name = name;
    }
    
    @Override
    public void display() {
        System.out.println("分类: " + name);
        for (ProductComponent child : children) {
            System.out.print("  ");
            child.display();
        }
    }
    
    @Override
    public double getPrice() {
        double total = 0;
        for (ProductComponent child : children) {
            total += child.getPrice();
        }
        return total;
    }
    
    @Override
    public boolean search(String productName) {
        for (ProductComponent child : children) {
            if (child.search(productName)) {
                return true;
            }
        }
        return false;
    }
    
    @Override
    public void add(ProductComponent component) {
        children.add(component);
    }
    
    @Override
    public void remove(ProductComponent component) {
        children.remove(component);
    }
}

组合模式的核心结构

复制代码
        Component(抽象组件)
         /    \
        /      \
Leaf(叶子)  Composite(组合)
                |
                |
          包含多个Component

关键特征:

  • 统一接口:叶子和组合实现相同的接口
  • 递归结构:组合可以包含其他组合,形成树形结构
  • 透明性:客户端无需区分叶子和组合

透明式 vs 安全式

透明式(推荐)

java 复制代码
// 在抽象接口中定义所有方法(包括add/remove)
interface Component {
    void operation();
    void add(Component c);      // 叶子节点抛出异常
    void remove(Component c);   // 叶子节点抛出异常
}

优点 :客户端统一对待所有组件
缺点:叶子节点需要实现不需要的方法

安全式

java 复制代码
// 只在组合接口中定义管理子组件的方法
interface Component {
    void operation();
}

interface Composite extends Component {
    void add(Component c);
    void remove(Component c);
}

优点 :避免叶子节点实现不需要的方法
缺点:客户端需要判断组件类型


适用场景

适合用组合模式的场景:

  1. 树形结构数据

    java 复制代码
    // 组织架构、文件系统、菜单系统
    // 任何需要表示"部分-整体"层次结构的场景
  2. 统一处理需求

    java 复制代码
    // 需要对整个树执行相同操作
    root.calculate();     // 计算整个树
    branch.calculate();   // 计算分支
    leaf.calculate();     // 计算叶子
  3. 递归操作

    java 复制代码
    // 深度优先搜索、递归统计等
    int total = root.getTotal();

不适合的场景:

  1. 组件差异很大:如果叶子和组合的行为完全不同
  2. 性能敏感:递归操作可能影响性能
  3. 简单结构:如果结构很简单,不需要这么复杂的设计

优缺点

优点:

  • 简化客户端代码:客户端可以一致地处理叶子和组合
  • 易于扩展:新增组件类型很容易
  • 符合开闭原则:可以灵活添加新的组件

缺点:

  • 设计复杂:需要仔细设计接口
  • 类型安全问题:透明式需要在运行时检测操作是否支持
  • 性能考虑:递归操作可能成为性能瓶颈

总结

组合模式就是:

  • 树形结构:表示部分-整体的层次关系
  • 统一操作:对叶子节点和组合节点的操作方式一致
  • 递归处理:组合节点委托子节点完成操作

核心口诀:

部分整体层次树,

统一操作好维护。

叶子组合一个样,

递归处理真舒服!

就像现实中的:

  • 🏢 组织架构:员工和部门都可以计算工资总额
  • 📁 文件系统:文件和文件夹都可以计算大小
  • 🎮 游戏场景:简单物体和复杂场景都可以渲染
  • 📊 UI组件:按钮和面板都可以刷新显示

记住:当你需要处理树形结构,并且希望对单个对象和组合对象使用统一的操作时,使用组合模式!

相关推荐
Jtti2 分钟前
PHP项目缓存占用硬盘过大?目录清理与优化
java·缓存·php
未若君雅裁30 分钟前
JVM基础总结
java·jvm·java-ee
星释36 分钟前
Rust 练习册 66:密码方块与文本加密
java·前端·rust
q***318942 分钟前
Spring Boot 实战篇(四):实现用户登录与注册功能
java·spring boot·后端
专注于大数据技术栈1 小时前
java学习--==和equals
java·python·学习
鲸沉梦落1 小时前
JVM类加载
java·jvm
carry杰1 小时前
esayExcel导出图片
java·easyexcel 图片
路人甲ing..1 小时前
Android Studio 快速的制作一个可以在 手机上跑的app
android·java·linux·智能手机·android studio
心灵宝贝2 小时前
Mac 安装 JDK 8u281(JDK-8u281-1.dmg)详细步骤(附安装包)
java·macos·intellij-idea
記億揺晃着的那天2 小时前
从单体到微服务:如何拆分
java·微服务·ddd·devops·系统拆分