一、组合模式核心定义
组合模式是结构型设计模式 的一种,核心目的是:将对象组合成树形结构,以表示 "部分 - 整体" 的层次结构,让客户端能够统一地处理单个对象和对象组合。
简单来说,就是让 "单个元素" 和 "元素集合" 拥有一致的接口,客户端无需区分它们,只需按统一方式调用 ------ 比如操作 "一个文件" 和 "一个文件夹(包含多个文件)" 时,使用相同的方法。
生活类比(最易理解)
- 场景 :电脑的文件系统
- 单个元素:文件(如 笔记.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. 图形绘制(计算机图形学)
- 图形组合 :基本图形(直线、圆)→复合图形(矩形、多边形)→复杂图形(流程图、思维导图),统一绘制、移动、缩放图形。
六、组合模式的关键注意事项
-
透明式 vs 安全式:
- 透明式:抽象组件声明所有方法(包括 add/remove),叶子节点抛异常(如上述案例),优点是客户端完全统一操作,缺点是叶子节点有无关方法;
- 安全式:抽象组件只声明公共方法,add/remove 仅在复合节点声明,优点是类型安全,缺点是客户端需区分叶子 / 复合节点;
- 实战中透明式更常用 (符合 "单一接口" 原则)。
-
递归操作 :复合节点的核心是递归处理子节点,需注意递归终止条件(避免栈溢出)。
-
与装饰器模式的区别:
- 组合模式:聚焦 "部分 - 整体" 的树形结构,让客户端统一处理单个 / 组合对象;
- 装饰器模式:聚焦 "增强功能",给单个对象动态添加功能,无树形结构。
总结
- 组合模式的核心是统一单个对象和组合对象的接口 ,让客户端无需区分 "部分" 和 "整体",按相同方式操作;
- 关键是识别出树形层级结构 (如文件 / 文件夹、菜单 / 菜单项),将节点分为叶子(最小单元)和复合(包含子节点);
- 实战中主要用于处理层级结构数据,核心价值是简化客户端操作、提高扩展性 (新增节点类型无需修改客户端代码)。
扩展(电商业务场景):
一、电商商品分类业务场景拆解
核心需求
电商平台的商品分类是典型的树形层级结构 ,需支持:
- 多级类目:一级类目(数码)→ 二级类目(手机)→ 三级类目(智能手机)→ 四级类目(安卓手机);
- 类目操作:添加子类目、关联商品、移除类目 / 商品;
- 数据统计:统计某个类目下的商品总数(递归包含所有子类目);
- 树形展示:按层级打印类目结构及商品信息;
- 统一操作:客户端无需区分 "类目(组合节点)" 和 "商品(叶子节点)",用相同接口操作。
设计思路(组合模式)
| 组合模式角色 | 电商场景对应实现 |
|---|---|
| 抽象组件(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());
五、实战价值总结
- 统一接口,简化开发 :客户端无需区分 "类目" 和 "商品",用countProducts()、showInfo()等统一方法操作,降低代码复杂度;
- 灵活扩展,符合开闭原则 :新增类目 / 商品只需新增子类,无需修改原有代码(如新增 "平板电脑" 类目,仅需创建Category对象并添加);
- 递归处理层级结构 :完美适配电商分类的树形层级,递归统计、展示数据,避免大量嵌套判断;
- 贴近真实业务 :包含 SKU、价格、库存、品牌等电商核心属性,可直接复用至商品管理系统。
关键点回顾
- 组合模式在电商分类中,核心是将「类目(复合节点)」和「商品(叶子节点)」抽象为统一接口,实现 "部分 - 整体" 的统一操作;
- 复合节点(类目)通过递归处理子节点,实现层级数据统计和展示;
- 扩展时仅需新增组件类,符合开闭原则,是电商树形结构数据处理的最优设计思路之一。
