【设计模式】 组合模式(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组件:按钮和面板都可以刷新显示

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

相关推荐
Yeniden4 小时前
【设计模式】# 外观模式(Facade)大白话讲解!
java·设计模式·外观模式
脚踏实地的大梦想家4 小时前
【Go】P17 Go语言并发编程核心:深入理解 Goroutine (从入门到实战)
java·开发语言·golang
初学小白...4 小时前
线程同步机制及三大不安全案例
java·开发语言·jvm
CS Beginner5 小时前
【搭建】个人博客网站的搭建
java·前端·学习·servlet·log4j·mybatis
JavaTree20175 小时前
【Spring Boot】Spring Boot解决循环依赖
java·spring boot·后端
lang201509286 小时前
Maven 五分钟入门
java·maven
cj6341181506 小时前
SpringBoot配置Redis
java·后端
用坏多个鼠标6 小时前
Nacos和Nginx集群,项目启动失败问题
java·开发语言
TangKenny6 小时前
基于EasyExcel的动态列映射读取方案
java·easyexcel