【从零入门23种设计模式08】结构型之组合模式(含电商业务场景)

一、组合模式核心定义

组合模式是结构型设计模式 的一种,核心目的是:将对象组合成树形结构,以表示 "部分 - 整体" 的层次结构,让客户端能够统一地处理单个对象和对象组合

简单来说,就是让 "单个元素" 和 "元素集合" 拥有一致的接口,客户端无需区分它们,只需按统一方式调用 ------ 比如操作 "一个文件" 和 "一个文件夹(包含多个文件)" 时,使用相同的方法。

生活类比(最易理解)
  • 场景 :电脑的文件系统
    • 单个元素:文件(如 笔记.txt);
    • 组合元素:文件夹(如 文档 文件夹,里面可包含文件、子文件夹);
    • 操作需求:无论是文件还是文件夹,都需要支持 "查看大小""删除""重命名" 等操作;
  • 组合模式解决 :给文件和文件夹定义统一的接口(如 FileSystemComponent),客户端调用 component.getSize() 时,文件返回自身大小,文件夹递归计算所有子元素的大小,客户端无需区分是文件还是文件夹。

二、组合模式核心角色

角色 作用
抽象组件(Component) 定义单个对象和组合对象的统一接口,声明所有公共方法(如获取大小、删除)
叶子节点(Leaf) 树形结构的最小单元(单个对象),实现 Component 接口,无子节点(如文件)
复合节点(Composite) 树形结构的分支节点(组合对象),实现 Component 接口,包含子节点(如文件夹),可添加 / 删除子节点

三、实战案例 1:文件系统(Java)

这是组合模式最经典的应用,完美体现 "部分 - 整体" 的层次结构。

1. 定义抽象组件(统一接口)
复制代码
/**
 * 抽象组件:文件系统组件(统一文件和文件夹的接口)
 */
public abstract class FileSystemComponent {
    protected String name; // 名称(文件名/文件夹名)

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

    // 公共方法:获取大小(文件返回自身大小,文件夹返回所有子元素大小之和)
    public abstract long getSize();

    // 公共方法:显示信息(含层级,方便打印树形结构)
    public abstract void showInfo(int level);

    // 复合节点需要的方法(叶子节点默认抛出异常)
    public void add(FileSystemComponent component) {
        throw new UnsupportedOperationException("叶子节点不支持添加子元素");
    }

    public void remove(FileSystemComponent component) {
        throw new UnsupportedOperationException("叶子节点不支持删除子元素");
    }
}
2. 定义叶子节点(文件)
复制代码
/**
 * 叶子节点:文件(最小单元,无子女点)
 */
public class File extends FileSystemComponent {
    private long size; // 文件大小(字节)

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

    @Override
    public long getSize() {
        // 文件直接返回自身大小
        return size;
    }

    @Override
    public void showInfo(int level) {
        // 打印层级(用空格表示)+ 文件名 + 大小
        StringBuilder prefix = new StringBuilder();
        for (int i = 0; i < level; i++) {
            prefix.append("  ");
        }
        System.out.printf("%s文件:%s | 大小:%d字节%n", prefix, name, size);
    }
}
3. 定义复合节点(文件夹)
复制代码
import java.util.ArrayList;
import java.util.List;

/**
 * 复合节点:文件夹(包含子节点,可添加/删除文件/子文件夹)
 */
public class Folder extends FileSystemComponent {
    // 存储子节点(文件/子文件夹)
    private List<FileSystemComponent> children = new ArrayList<>();

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

    @Override
    public long getSize() {
        // 文件夹大小 = 所有子元素大小之和(递归计算)
        long totalSize = 0;
        for (FileSystemComponent child : children) {
            totalSize += child.getSize();
        }
        return totalSize;
    }

    @Override
    public void showInfo(int level) {
        // 打印文件夹信息
        StringBuilder prefix = new StringBuilder();
        for (int i = 0; i < level; i++) {
            prefix.append("  ");
        }
        System.out.printf("%s文件夹:%s | 总大小:%d字节%n", prefix, name, getSize());
        // 递归打印所有子元素
        for (FileSystemComponent child : children) {
            child.showInfo(level + 1);
        }
    }

    @Override
    public void add(FileSystemComponent component) {
        children.add(component);
    }

    @Override
    public void remove(FileSystemComponent component) {
        children.remove(component);
    }
}
4. 客户端调用
复制代码
public class FileSystemClient {
    public static void main(String[] args) {
        // 1. 创建叶子节点(文件)
        FileSystemComponent file1 = new File("笔记.txt", 1024); // 1KB
        FileSystemComponent file2 = new File("图片.jpg", 204800); // 200KB
        FileSystemComponent file3 = new File("视频.mp4", 10485760); // 10MB

        // 2. 创建复合节点(文件夹)
        FileSystemComponent docFolder = new Folder("文档");
        docFolder.add(file1); // 文件夹添加文件

        FileSystemComponent mediaFolder = new Folder("媒体");
        mediaFolder.add(file2);
        mediaFolder.add(file3);

        FileSystemComponent rootFolder = new Folder("根目录");
        rootFolder.add(docFolder); // 文件夹添加子文件夹
        rootFolder.add(mediaFolder);

        // 3. 统一操作:显示所有元素信息(客户端无需区分文件/文件夹)
        rootFolder.showInfo(0);

        // 4. 统一操作:获取根目录总大小
        System.out.printf("%n根目录总大小:%d字节%n", rootFolder.getSize());
    }
}
输出结果
复制代码
文件夹:根目录 | 总大小:10690048字节
  文件夹:文档 | 总大小:1024字节
    文件:笔记.txt | 大小:1024字节
  文件夹:媒体 | 总大小:10689024字节
    文件:图片.jpg | 大小:204800字节
    文件:视频.mp4 | 大小:10485760字节

根目录总大小:10690048字节

四、实战案例 2:菜单系统(前端 / 后端通用)

组合模式也是菜单系统的核心设计思路,比如:

  • 叶子节点:菜单项(如 "首页""个人中心");
  • 复合节点:菜单组(如 "系统管理",包含 "用户管理""角色管理" 子菜单);
  • 客户端统一渲染所有菜单,无需区分是菜单项还是菜单组。
核心代码(简化版)
复制代码
/**
 * 抽象组件:菜单组件
 */
public abstract class MenuComponent {
    protected String name;
    protected String url; // 菜单项的跳转地址(菜单组为空)

    public MenuComponent(String name, String url) {
        this.name = name;
        this.url = url;
    }

    // 渲染菜单(统一方法)
    public abstract void render(int level);

    // 添加子菜单(叶子节点抛异常)
    public void add(MenuComponent component) {
        throw new UnsupportedOperationException();
    }
}

// 叶子节点:菜单项
public class MenuItem extends MenuComponent {
    public MenuItem(String name, String url) {
        super(name, url);
    }

    @Override
    public void render(int level) {
        String prefix = "  ".repeat(level);
        System.out.printf("%s<a href='%s'>%s</a>%n", prefix, url, name);
    }
}

// 复合节点:菜单组
public class MenuGroup extends MenuComponent {
    private List<MenuComponent> children = new ArrayList<>();

    public MenuGroup(String name) {
        super(name, ""); // 菜单组无跳转地址
    }

    @Override
    public void render(int level) {
        String prefix = "  ".repeat(level);
        System.out.printf("%s<div class='menu-group'>%s</div>%n", prefix, name);
        for (MenuComponent child : children) {
            child.render(level + 1);
        }
    }

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

// 客户端调用
public class MenuClient {
    public static void main(String[] args) {
        // 菜单项(叶子)
        MenuComponent home = new MenuItem("首页", "/home");
        MenuComponent user = new MenuItem("用户管理", "/user");
        MenuComponent role = new MenuItem("角色管理", "/role");

        // 菜单组(复合)
        MenuComponent systemGroup = new MenuGroup("系统管理");
        systemGroup.add(user);
        systemGroup.add(role);

        // 根菜单
        MenuComponent rootMenu = new MenuGroup("根菜单");
        rootMenu.add(home);
        rootMenu.add(systemGroup);

        // 统一渲染
        rootMenu.render(0);
    }
}

五、组合模式的典型实战场景

1. 树形结构数据处理(核心场景)
  • 文件系统 :如上述案例,处理文件 / 文件夹的层级结构;
  • 组织机构 :公司部门(总部分公司→部门→小组),统一获取部门人数、打印组织架构;
  • 权限系统 :权限节点(菜单权限、按钮权限、权限组),统一校验权限、分配权限。
2. UI 组件库
  • 前端组件 :Vue/React 的组件树(如 Form 组件包含 Input、Button 子组件),统一渲染、销毁组件;
  • 桌面应用 UI :窗口(包含按钮、文本框、子窗口),统一处理事件、布局。
3. 电商 / 商品分类
  • 商品类目 :一级类目(数码)→二级类目(手机)→三级类目(智能手机)→商品,统一查询类目下商品数量、遍历所有商品。
4. 图形绘制(计算机图形学)
  • 图形组合 :基本图形(直线、圆)→复合图形(矩形、多边形)→复杂图形(流程图、思维导图),统一绘制、移动、缩放图形。

六、组合模式的关键注意事项

  1. 透明式 vs 安全式

    • 透明式:抽象组件声明所有方法(包括 add/remove),叶子节点抛异常(如上述案例),优点是客户端完全统一操作,缺点是叶子节点有无关方法;
    • 安全式:抽象组件只声明公共方法,add/remove 仅在复合节点声明,优点是类型安全,缺点是客户端需区分叶子 / 复合节点;
    • 实战中透明式更常用 (符合 "单一接口" 原则)。
  2. 递归操作 :复合节点的核心是递归处理子节点,需注意递归终止条件(避免栈溢出)。

  3. 与装饰器模式的区别

    • 组合模式:聚焦 "部分 - 整体" 的树形结构,让客户端统一处理单个 / 组合对象;
    • 装饰器模式:聚焦 "增强功能",给单个对象动态添加功能,无树形结构。

总结

  1. 组合模式的核心是统一单个对象和组合对象的接口 ,让客户端无需区分 "部分" 和 "整体",按相同方式操作;
  2. 关键是识别出树形层级结构 (如文件 / 文件夹、菜单 / 菜单项),将节点分为叶子(最小单元)和复合(包含子节点);
  3. 实战中主要用于处理层级结构数据,核心价值是简化客户端操作、提高扩展性 (新增节点类型无需修改客户端代码)。

扩展(电商业务场景):

一、电商商品分类业务场景拆解

核心需求

电商平台的商品分类是典型的树形层级结构 ,需支持:

  1. 多级类目:一级类目(数码)→ 二级类目(手机)→ 三级类目(智能手机)→ 四级类目(安卓手机);
  2. 类目操作:添加子类目、关联商品、移除类目 / 商品;
  3. 数据统计:统计某个类目下的商品总数(递归包含所有子类目);
  4. 树形展示:按层级打印类目结构及商品信息;
  5. 统一操作:客户端无需区分 "类目(组合节点)" 和 "商品(叶子节点)",用相同接口操作。
设计思路(组合模式)
组合模式角色 电商场景对应实现
抽象组件(Component) ProductComponent:统一类目和商品的接口
叶子节点(Leaf) Product:商品(最小单元,无子节点)
复合节点(Composite) Category:商品类目(可包含子类目 / 商品)

二、完整代码实现(Java)

1. 依赖准备(可选,用于 JSON 格式化)
复制代码
<!-- 用于树形展示时格式化JSON(可选) -->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>2.0.43</version>
</dependency>
2. 抽象组件:统一类目和商品的接口
复制代码
/**
 * 抽象组件:商品/类目统一接口(组合模式核心)
 */
public abstract class ProductComponent {
    // 组件名称(类目名/商品名)
    protected String name;
    // 组件ID(类目ID/商品SKU)
    protected String id;

    public ProductComponent(String id, String name) {
        this.id = id;
        this.name = name;
    }

    /**
     * 统计当前组件下的商品总数
     *  - 商品:返回1
     *  - 类目:递归统计所有子节点的商品数
     */
    public abstract int countProducts();

    /**
     * 按层级展示组件信息(树形打印)
     * @param level 层级(根节点为0,子节点+1)
     */
    public abstract void showInfo(int level);

    /**
     * 添加子组件(仅类目支持,商品抛异常)
     */
    public void add(ProductComponent component) {
        throw new UnsupportedOperationException("当前组件不支持添加子元素");
    }

    /**
     * 移除子组件(仅类目支持,商品抛异常)
     */
    public void remove(ProductComponent component) {
        throw new UnsupportedOperationException("当前组件不支持移除子元素");
    }

    // Getter
    public String getId() { return id; }
    public String getName() { return name; }
}
3. 叶子节点:商品(最小单元)
复制代码
import java.math.BigDecimal;

/**
 * 叶子节点:商品(无子女点,组合模式Leaf)
 */
public class Product extends ProductComponent {
    // 商品扩展属性
    private BigDecimal price; // 价格
    private String brand;     // 品牌
    private int stock;        // 库存

    public Product(String sku, String name, BigDecimal price, String brand, int stock) {
        super(sku, name);
        this.price = price;
        this.brand = brand;
        this.stock = stock;
    }

    /**
     * 商品的商品数=1
     */
    @Override
    public int countProducts() {
        return 1;
    }

    /**
     * 展示商品信息(带层级缩进)
     */
    @Override
    public void showInfo(int level) {
        // 层级缩进(每级2个空格)
        String prefix = "  ".repeat(level);
        System.out.printf(
            "%s【商品】SKU:%s | 名称:%s | 品牌:%s | 价格:¥%s | 库存:%d%n",
            prefix, id, name, brand, price.setScale(2), stock
        );
    }

    // Getter & Setter
    public BigDecimal getPrice() { return price; }
    public String getBrand() { return brand; }
    public int getStock() { return stock; }
}
4. 复合节点:商品类目(可包含子类目 / 商品)
复制代码
import java.util.ArrayList;
import java.util.List;

/**
 * 复合节点:商品类目(可包含子类目/商品,组合模式Composite)
 */
public class Category extends ProductComponent {
    // 子组件列表(子类目/商品)
    private List<ProductComponent> children = new ArrayList<>();
    // 类目层级(1级/2级/3级)
    private int categoryLevel;

    public Category(String categoryId, String name, int categoryLevel) {
        super(categoryId, name);
        this.categoryLevel = categoryLevel;
    }

    /**
     * 递归统计类目下所有商品数(包含子类目)
     */
    @Override
    public int countProducts() {
        int total = 0;
        for (ProductComponent child : children) {
            total += child.countProducts();
        }
        return total;
    }

    /**
     * 展示类目信息(递归展示所有子组件)
     */
    @Override
    public void showInfo(int level) {
        String prefix = "  ".repeat(level);
        // 打印类目基本信息
        System.out.printf(
            "%s【%d级类目】ID:%s | 名称:%s | 商品总数:%d%n",
            prefix, categoryLevel, id, name, this.countProducts()
        );
        // 递归打印所有子组件
        for (ProductComponent child : children) {
            child.showInfo(level + 1);
        }
    }

    /**
     * 添加子组件(类目/商品)
     */
    @Override
    public void add(ProductComponent component) {
        // 校验:避免重复添加
        if (children.stream().anyMatch(c -> c.getId().equals(component.getId()))) {
            throw new IllegalArgumentException("子组件ID已存在:" + component.getId());
        }
        children.add(component);
    }

    /**
     * 移除子组件(类目/商品)
     */
    @Override
    public void remove(ProductComponent component) {
        children.removeIf(c -> c.getId().equals(component.getId()));
    }

    // Getter
    public List<ProductComponent> getChildren() { return children; }
    public int getCategoryLevel() { return categoryLevel; }
}
5. 客户端:电商分类管理系统(业务层调用)
复制代码
import java.math.BigDecimal;

/**
 * 客户端:电商商品分类管理系统(组合模式使用示例)
 */
public class EcommerceCategorySystem {
    public static void main(String[] args) {
        // ==================== 1. 构建商品分类树 ====================
        // 一级类目:数码
        Category digitalCategory = new Category("C001", "数码", 1);
        // 二级类目:手机
        Category phoneCategory = new Category("C001001", "手机", 2);
        // 三级类目:智能手机
        Category smartPhoneCategory = new Category("C001001001", "智能手机", 3);
        // 四级类目:安卓手机
        Category androidPhoneCategory = new Category("C001001001001", "安卓手机", 4);
        // 四级类目:苹果手机
        Category iphoneCategory = new Category("C001001001002", "苹果手机", 4);

        // ==================== 2. 添加商品 ====================
        // 安卓手机商品
        Product mi14 = new Product("SKU001", "小米14", new BigDecimal("3999"), "小米", 1000);
        Product huaweiP70 = new Product("SKU002", "华为P70", new BigDecimal("4999"), "华为", 800);
        // 苹果手机商品
        Product iphone16 = new Product("SKU003", "iPhone 16", new BigDecimal("5999"), "苹果", 1200);
        // 直接添加到二级类目(手机)的商品(非智能手机,演示灵活度)
        Product nokia1110 = new Product("SKU004", "诺基亚1110", new BigDecimal("199"), "诺基亚", 5000);

        // ==================== 3. 组装分类树 ====================
        // 四级类目添加商品
        androidPhoneCategory.add(mi14);
        androidPhoneCategory.add(huaweiP70);
        iphoneCategory.add(iphone16);
        // 三级类目添加四级类目
        smartPhoneCategory.add(androidPhoneCategory);
        smartPhoneCategory.add(iphoneCategory);
        // 二级类目添加三级类目+商品
        phoneCategory.add(smartPhoneCategory);
        phoneCategory.add(nokia1110);
        // 一级类目添加二级类目
        digitalCategory.add(phoneCategory);

        // ==================== 4. 统一操作:组合模式核心价值 ====================
        System.out.println("======= 1. 树形展示全部分类及商品 =======");
        digitalCategory.showInfo(0);

        System.out.println("\n======= 2. 统计数码类目下总商品数 =======");
        System.out.printf("数码类目商品总数:%d%n", digitalCategory.countProducts());

        System.out.println("\n======= 3. 统计智能手机类目下商品数 =======");
        System.out.printf("智能手机类目商品总数:%d%n", smartPhoneCategory.countProducts());

        System.out.println("\n======= 4. 移除某个商品(诺基亚1110) =======");
        phoneCategory.remove(nokia1110);
        System.out.println("移除后手机类目商品总数:" + phoneCategory.countProducts());
        System.out.println("移除后数码类目商品总数:" + digitalCategory.countProducts());

        System.out.println("\n======= 5. 单独操作商品(叶子节点) =======");
        mi14.showInfo(1);
        System.out.printf("小米14商品数:%d%n", mi14.countProducts());
    }
}

三、输出结果(直观展示业务效果)

复制代码
======= 1. 树形展示全部分类及商品 =======
【1级类目】ID:C001 | 名称:数码 | 商品总数:4
  【2级类目】ID:C001001 | 名称:手机 | 商品总数:4
    【3级类目】ID:C001001001 | 名称:智能手机 | 商品总数:3
      【4级类目】ID:C001001001001 | 名称:安卓手机 | 商品总数:2
        【商品】SKU:SKU001 | 名称:小米14 | 品牌:小米 | 价格:¥3999.00 | 库存:1000
        【商品】SKU:SKU002 | 名称:华为P70 | 品牌:华为 | 价格:¥4999.00 | 库存:800
      【4级类目】ID:C001001001002 | 名称:苹果手机 | 商品总数:1
        【商品】SKU:SKU003 | 名称:iPhone 16 | 品牌:苹果 | 价格:¥5999.00 | 库存:1200
    【商品】SKU:SKU004 | 名称:诺基亚1110 | 品牌:诺基亚 | 价格:¥199.00 | 库存:5000

======= 2. 统计数码类目下总商品数 =======
数码类目商品总数:4

======= 3. 统计智能手机类目下商品数 =======
智能手机类目商品总数:3

======= 4. 移除某个商品(诺基亚1110) =======
移除后手机类目商品总数:3
移除后数码类目商品总数:3

======= 5. 单独操作商品(叶子节点) =======
  【商品】SKU:SKU001 | 名称:小米14 | 品牌:小米 | 价格:¥3999.00 | 库存:1000
小米14商品数:1

四、扩展场景(贴近生产环境)

1. 新增类目 / 商品(符合开闭原则)
复制代码
// 新增三级类目:平板电脑(无需修改原有代码)
Category padCategory = new Category("C001002", "平板电脑", 2);
Product ipadPro = new Product("SKU005", "iPad Pro", new BigDecimal("7999"), "苹果", 500);
padCategory.add(ipadPro);
// 添加到一级类目
digitalCategory.add(padCategory);

// 新增后统计
System.out.println("新增平板后数码类目商品总数:" + digitalCategory.countProducts()); // 输出:4
2. 批量操作(如库存统计)

ProductComponent新增抽象方法,子类实现:

复制代码
// 抽象组件新增方法
public abstract int countStock();

// 商品实现
@Override
public int countStock() {
    return this.stock;
}

// 类目实现
@Override
public int countStock() {
    int totalStock = 0;
    for (ProductComponent child : children) {
        totalStock += child.countStock();
    }
    return totalStock;
}

// 客户端调用
System.out.println("数码类目总库存:" + digitalCategory.countStock());

五、实战价值总结

  1. 统一接口,简化开发 :客户端无需区分 "类目" 和 "商品",用countProducts()、showInfo()等统一方法操作,降低代码复杂度;
  2. 灵活扩展,符合开闭原则 :新增类目 / 商品只需新增子类,无需修改原有代码(如新增 "平板电脑" 类目,仅需创建Category对象并添加);
  3. 递归处理层级结构 :完美适配电商分类的树形层级,递归统计、展示数据,避免大量嵌套判断;
  4. 贴近真实业务 :包含 SKU、价格、库存、品牌等电商核心属性,可直接复用至商品管理系统。

关键点回顾

  1. 组合模式在电商分类中,核心是将「类目(复合节点)」和「商品(叶子节点)」抽象为统一接口,实现 "部分 - 整体" 的统一操作;
  2. 复合节点(类目)通过递归处理子节点,实现层级数据统计和展示;
  3. 扩展时仅需新增组件类,符合开闭原则,是电商树形结构数据处理的最优设计思路之一。
相关推荐
筱昕~呀1 小时前
冲刺蓝桥杯-DFS板块(第二天)
算法·蓝桥杯·深度优先
问好眼1 小时前
《算法竞赛进阶指南》0x01 位运算-1.a^b
c++·算法·位运算·信息学奥赛
We་ct1 小时前
LeetCode 103. 二叉树的锯齿形层序遍历:解题思路+代码详解
前端·算法·leetcode·typescript·广度优先
Swift社区2 小时前
LeetCode 391 完美矩形 - Swift 题解
算法·leetcode·swift
NGC_66112 小时前
插入排序算法
java·数据结构·算法
zheshiyangyang2 小时前
前端面试基础知识整理【Day-10】
前端·面试·职场和发展
驴儿响叮当20102 小时前
设计模式之状态模式
设计模式·状态模式
bubiyoushang8882 小时前
基于遗传算法的LQR控制器最优设计算法
开发语言·算法·matlab
每天要多喝水2 小时前
图论Day39:孤岛题目
算法·深度优先·图论