📌 PDF:AI人工智能 — AI Agent智能体实战项目
大白话说Java设计模式-20-组合模式(业务实战篇):大白商城商品类目树的"枝叶结构"
📌 一句话本质:组合模式就是"树形结构",让单个对象和组合对象用同一套 API。
🏷️ 标签:组合模式 / Java 设计模式 / 树形结构 / 大白商城 / 类目树 🎯 适合:初中级后端 / 经常处理树形结构的工程师
目录
- 一、业务场景引入:商品类目树怎么设计?
- 二、反面教材:树形结构的"灾难现场"
- 三、模式原理:组合的"枝叶同形 + 一张图"
- 四、实战代码:大白商城商品类目树完整实现
- 五、组合 vs 继承 vs 装饰器
- 六、JDK 的 File 类是组合模式吗?
- 七、工程决策 Checklist
- 八、与其他模式协作
- 九、本篇小结 + 下篇预告
一、业务场景引入:商品类目树怎么设计?
大白商城 2024 年做了一次大改造,运营经理提了一个看似简单、实则"动全身"的需求:
“商品类目要支持无限层级。比如:电子 → 手机 → 苹果 → iPhone 15。我要能查任意节点的子节点、父节点、整棵树。”
我打开老代码一看,根本没法看:
/**
* 大白商城老的类目代码
*/
public class CategoryService {
public void printAllCategories() {
// ❌ 硬编码 3 层
List<Category> level1 = categoryDao.findByLevel(1);
for (Category c1 : level1) {
System.out.println(c1.getName());
List<Category> level2 = categoryDao.findByParentId(c1.getId());
for (Category c2 : level2) {
System.out.println(" " + c2.getName());
List<Category> level3 = categoryDao.findByParentId(c2.getId());
for (Category c3 : level3) {
System.out.println(" " + c3.getName());
// ❌ 还要再加 level4?level5?
}
}
}
}
}
这种代码的痛点:
| 序号 | 问题 | 后果 |
|---|---|---|
| ① | 硬编码 3 层 | 超过 3 层崩 |
| ② | 嵌套 for 循环 | 加 1 层要改所有 for |
| ③ | 每个层级单独查 | 性能差(N+1 查询) |
| ④ | 业务代码臃肿 | 看不到核心逻辑 |
| ⑤ | 树形操作缺失 | 没法查子树、路径 |
老板要的"无限层级 + 树形操作",怎么搞?
答案就是——组合模式。
1.1 大白话讲透组合
继续打比方:
场景:大白商城商品类目是树形结构:
- 电子(节点)
- 手机(节点)
- 苹果(节点)
- iPhone 15(叶子)
- iPhone 14(叶子)
- 小米(节点)
- 电脑(节点)
每个节点都"看起来一样":
- 节点可以有子节点
- 叶子没有子节点
- 但对客户端来说,"节点"和"叶子"是同一种东西——都能
add / remove / getName。这就是组合模式的核心:让客户端一致地处理"单个对象"和"组合对象"。
组合模式 = 将对象组合成树形结构以表示"部分-整体"的层次结构,让客户端对单个对象和组合对象的使用具有一致性。
1.2 组合模式的 3 个真实场景
大白商城里,组合模式用在:
| 场景 | “树” |
|---|---|
| 商品类目 | 电子 → 手机 → 苹果 → iPhone 15 |
| 权限树 | 菜单 → 子菜单 → 按钮 |
| 组织架构 | 公司 → 部门 → 团队 → 员工 |
| 文件系统 | 根目录 → 子目录 → 文件 |
| 评论回复 | 评论 → 回复 → 回复的回复 |
任何"树形结构"的场景,都用组合模式。
二、反面教材:树形结构的"灾难现场"
我们看 4 个反面教材,看它们是怎么一步步崩的。
2.1 反面教材 v1:硬编码层级
/**
* ❌ 反面教材 v1:硬编码层级
*/
public void printTree(List<Category> level1) {
for (Category c1 : level1) {
System.out.println(c1.getName());
List<Category> level2 = categoryDao.findByParentId(c1.getId());
for (Category c2 : level2) {
System.out.println(" " + c2.getName());
List<Category> level3 = categoryDao.findByParentId(c2.getId());
for (Category c3 : level3) {
// ... 又要加 level4
}
}
}
}
翻车现场:
| 序号 | 问题 | 后果 |
|---|---|---|
| ① | 层数写死 | 加 1 层要改方法 |
| ② | N+1 查询 | 性能差 |
| ③ | 代码臃肿 | 嵌套 4 层循环 |
2.2 反面教材 v2:每个层级一个类
/**
* ❌ 反面教材 v2:每个层级一个类
*/
public class Level1Category { ... }
public class Level2Category extends Level1Category { ... }
public class Level3Category extends Level2Category { ... }
// ... 无限子类
翻车现场:
| 序号 | 问题 | 后果 |
|---|---|---|
| ① | 类数量爆炸 | 层数越多类越多 |
| ② | 代码重复 | 每个层级类相似 |
2.3 反面教材 v3:Map 嵌套
/**
* ❌ 反面教材 v3:Map 嵌套
*/
public void printTree(Map<String, Object> tree) {
System.out.println(tree.get("name"));
List<Map<String, Object>> children = (List<Map<String, Object>>) tree.get("children");
if (children != null) {
for (Map<String, Object> child : children) {
printTree(child); // 递归
}
}
}
翻车现场:
| 序号 | 问题 | 后果 |
|---|---|---|
| ① | 类型不安全 | 编译期不检查 |
| ② | IDE 难重构 | 改字段名不报错 |
| ③ | 业务逻辑散落 | 不集中 |
2.4 反面教材 v4:直接用数据库递归查询
/**
* ❌ 反面教材 v4:每次递归查数据库
*/
public void printTree(Long parentId, int level) {
List<Category> children = categoryDao.findByParentId(parentId); // 每次查
for (Category child : children) {
System.out.println(" ".repeat(level) + child.getName());
printTree(child.getId(), level + 1); // 递归
}
}
翻车现场:
| 序号 | 问题 | 后果 |
|---|---|---|
| ① | N+1 查询 | 100 个节点 = 100 次查库 |
| ② | 性能差 | 树大时崩溃 |
2.5 4 个反面教材的共同病根
| 痛点 | 反模式方案能不能解决? |
|---|---|
| 无限层级 | ❌ 全部写死 |
| 单次查整棵树 | ❌ 全部多次查 |
| 客户端一致处理 | ❌ 全部区分 |
| 树形操作 | ❌ 全部缺失 |
必须上组合模式。
三、模式原理:组合的"枝叶同形 + 一张图"
3.1 组合的 3 个核心角色
| 角色 | 职责 | 例子 |
|---|---|---|
| 抽象组件(Component) | 定义叶子和容器的共同接口 | CategoryComponent |
| 叶子(Leaf) | 没有子节点 | GoodsCategory(具体商品类目) |
| 容器(Composite) | 有子节点,也实现 Component | CategoryNode(中间类目) |
关键点:容器和叶子实现同一接口,客户端无差别使用。
3.2 一张图看懂组合
CategoryComponent(接口)
├── add(Component c)
├── remove(Component c)
├── getName()
└── getChildren()
↑ ↑
│ implements │ implements
│ │
CategoryNode(容器) GoodsCategory(叶子)
├── List<Component> children
├── add(c) { children.add(c) }
├── getName() { return "电子" }
└── getChildren() { return children }
↑ ↑ ↑
│ │ │
CategoryNode CategoryNode GoodsCategory
(手机) (苹果) (iPhone 15)
3.3 组合的"灵魂三问"
Q1:组合 vs 继承,区别是什么?
答:
- 组合:has-a 关系(节点持有子节点)
- 继承:is-a 关系(叶子继承父类)
- 组合不依赖具体类,依赖抽象
- 组合更灵活
Q2:组合 vs 装饰器,区别是什么?
答:
- 组合:整体-部分(树形结构)
- 装饰器:功能叠加(链式包装)
- 组合侧重"结构",装饰器侧重"行为"
- 组合是 is-part-of,装饰器是 has-a
Q3:客户端怎么统一处理节点和叶子?
答:
- 节点和叶子都实现 Component 接口
- 客户端调
component.operation(),不关心是节点还是叶子- 节点递归调子节点的
operation()- 叶子只执行自己的
operation()
3.4 组合的 3 种实现方式
| 方式 | 特点 | 适用 |
|---|---|---|
| 透明式 | 叶子也实现 add/remove(抛异常) | 客户端完全统一 |
| 安全式 | 叶子不实现 add/remove | 客户端要判断类型 |
| 混合式 | 默认实现,子类可覆盖 | 灵活 |
大白商城主推透明式(统一接口)。
四、实战代码:大白商城商品类目树完整实现
下面是大白商城生产环境在用的组合实现,全套代码可直接复制到 IDEA 跑。
4.1 项目环境与依赖
pom.xml:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.0</version>
<relativePath/>
</parent>
<groupId>com.dabai.mall</groupId>
<artifactId>mall-design-pattern-20</artifactId>
<version>1.0.0-SNAPSHOT</version>
<name>mall-design-pattern-20</name>
<description>大白商城 - 设计模式 20 组合模式</description>
<properties>
<java.version>17</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
4.2 抽象组件:CategoryComponent
package com.dabai.mall.category;
import java.util.List;
/**
* ✅ 抽象组件:商品类目组件
* <p>
* 节点和叶子都实现这个接口
*
* @author 大白商城技术团队
*/
public interface CategoryComponent {
/**
* 获取类目 ID
*/
Long getId();
/**
* 获取类目名称
*/
String getName();
/**
* ✅ 树形操作
*/
void add(CategoryComponent component);
/**
* ✅ 树形操作
*/
void remove(CategoryComponent component);
/**
* ✅ 树形操作
*/
List<CategoryComponent> getChildren();
/**
* ✅ 树形操作:打印
*/
void print(int level);
}
4.3 容器:CategoryNode
package com.dabai.mall.category;
import lombok.Getter;
import java.util.ArrayList;
import java.util.List;
/**
* ✅ 容器:类目节点(有子节点)
* <p>
* 关键:实现 CategoryComponent 接口
*/
@Getter
public class CategoryNode implements CategoryComponent {
private final Long id;
private final String name;
private final List<CategoryComponent> children = new ArrayList<>();
public CategoryNode(Long id, String name) {
this.id = id;
this.name = name;
}
@Override
public void add(CategoryComponent component) {
children.add(component);
}
@Override
public void remove(CategoryComponent component) {
children.remove(component);
}
@Override
public List<CategoryComponent> getChildren() {
return children;
}
@Override
public void print(int level) {
// 打印当前节点
System.out.println(" ".repeat(level) + "📁 " + getName());
// ✅ 递归打印子节点
for (CategoryComponent child : children) {
child.print(level + 1);
}
}
}
4.4 叶子:GoodsCategory
package com.dabai.mall.category;
import lombok.Getter;
import java.util.Collections;
import java.util.List;
/**
* ✅ 叶子:商品类目(无子节点)
* <p>
* 关键:实现 CategoryComponent 接口
*/
@Getter
public class GoodsCategory implements CategoryComponent {
private final Long id;
private final String name;
private final Long goodsCount; // 商品数量
public GoodsCategory(Long id, String name, Long goodsCount) {
this.id = id;
this.name = name;
this.goodsCount = goodsCount;
}
@Override
public void add(CategoryComponent component) {
// 叶子不能 add,抛异常
throw new UnsupportedOperationException("叶子节点不能 add");
}
@Override
public void remove(CategoryComponent component) {
throw new UnsupportedOperationException("叶子节点不能 remove");
}
@Override
public List<CategoryComponent> getChildren() {
return Collections.emptyList(); // 叶子无子节点
}
@Override
public void print(int level) {
// 打印叶子节点
System.out.println(" ".repeat(level) + "📄 " + getName() + " (" + goodsCount + " 件)");
}
}
4.5 构建类目树
package com.dabai.mall.category;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.List;
/**
* ✅ 商品类目服务
*
* @author 大白商城技术团队
*/
@Slf4j
@Service
public class CategoryService {
/**
* 构建大白商城的完整类目树
*/
public CategoryComponent buildCategoryTree() {
// 根节点:全部商品
CategoryNode root = new CategoryNode(0L, "全部商品");
// 一级:电子
CategoryNode electronics = new CategoryNode(1L, "电子");
root.add(electronics);
// 二级:手机
CategoryNode phones = new CategoryNode(2L, "手机");
electronics.add(phones);
// 三级:苹果
CategoryNode apple = new CategoryNode(3L, "苹果");
phones.add(apple);
// 四级:iPhone 15 / iPhone 14(叶子)
apple.add(new GoodsCategory(4L, "iPhone 15 Pro", 100L));
apple.add(new GoodsCategory(5L, "iPhone 14", 50L));
// 三级:小米
CategoryNode xiaomi = new CategoryNode(6L, "小米");
phones.add(xiaomi);
xiaomi.add(new GoodsCategory(7L, "小米 14", 80L));
// 二级:电脑
CategoryNode computers = new CategoryNode(8L, "电脑");
electronics.add(computers);
computers.add(new GoodsCategory(9L, "MacBook Pro", 30L));
// 一级:服饰
CategoryNode clothing = new CategoryNode(10L, "服饰");
root.add(clothing);
clothing.add(new GoodsCategory(11L, "T恤", 200L));
return root;
}
/**
* 打印整棵树
*/
public void printTree() {
CategoryComponent root = buildCategoryTree();
root.print(0);
}
/**
* 统计整棵树的商品数
*/
public long countGoods(CategoryComponent component) {
if (component instanceof GoodsCategory) {
return ((GoodsCategory) component).getGoodsCount();
}
long total = 0;
for (CategoryComponent child : component.getChildren()) {
total += countGoods(child);
}
return total;
}
/**
* 查找节点
*/
public CategoryComponent findById(CategoryComponent component, Long id) {
if (component.getId().equals(id)) {
return component;
}
for (CategoryComponent child : component.getChildren()) {
CategoryComponent found = findById(child, id);
if (found != null) {
return found;
}
}
return null;
}
}
4.6 完整类目树结构
全部商品
├── 📁 电子
│ ├── 📁 手机
│ │ ├── 📁 苹果
│ │ │ ├── 📄 iPhone 15 Pro (100 件)
│ │ │ └── 📄 iPhone 14 (50 件)
│ │ └── 📁 小米
│ │ └── 📄 小米 14 (80 件)
│ └── 📁 电脑
│ └── 📄 MacBook Pro (30 件)
└── 📁 服饰
└── 📄 T恤 (200 件)
4.7 单元测试
package com.dabai.mall.category;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
/**
* 组合模式完整单元测试
*/
class CategoryTreeTest {
@Test
void testPrintTree() {
CategoryService service = new CategoryService();
// 打印树(观察输出)
service.printTree();
}
@Test
void testCountGoods() {
CategoryService service = new CategoryService();
CategoryComponent root = service.buildCategoryTree();
// 100 + 50 + 80 + 30 + 200 = 460
assertEquals(460L, service.countGoods(root));
}
@Test
void testFindById() {
CategoryService service = new CategoryService();
CategoryComponent root = service.buildCategoryTree();
// 查找"苹果"节点
CategoryComponent apple = service.findById(root, 3L);
assertNotNull(apple);
assertEquals("苹果", apple.getName());
// 查找"iPhone 15"叶子
CategoryComponent iphone = service.findById(root, 4L);
assertNotNull(iphone);
assertEquals("iPhone 15 Pro", iphone.getName());
// 查找不存在的 ID
CategoryComponent notFound = service.findById(root, 999L);
assertNull(notFound);
}
@Test
void testLeafCannotAdd() {
// 叶子不能 add
GoodsCategory leaf = new GoodsCategory(4L, "iPhone 15 Pro", 100L);
assertThrows(UnsupportedOperationException.class,
() -> leaf.add(new GoodsCategory(5L, "test", 1L)));
}
@Test
void testCompositeAndLeafUnified() {
// ✅ 客户端不区分节点和叶子
CategoryService service = new CategoryService();
CategoryComponent root = service.buildCategoryTree();
// 调用 root.print() 不管里面是节点还是叶子
root.print(0);
// 调用 root.getChildren() 拿到的是 List<CategoryComponent>
List<CategoryComponent> topLevel = root.getChildren();
assertEquals(2, topLevel.size()); // 电子 + 服饰
}
}
五、组合 vs 继承 vs 装饰器
5.1 一张表看清区别
| 模式 | 关注点 | 关系 | 适用场景 |
|---|---|---|---|
| 组合 | 整体-部分 | has-a(树形) | 树形结构 |
| 继承 | 父子关系 | is-a | 类层次 |
| 装饰器 | 功能叠加 | has-a(链式) | 动态增强 |
5.2 一个具体例子区分 3 个模式
大白商城:
| 模式 | 例子 |
|---|---|
| 组合 | 商品类目树:电子 → 手机 → 苹果 |
| 继承 | VipOrder extends Order:VIP 订单 |
| 装饰器 | 价格计算:满减 + 折扣 + 运费叠加 |
5.3 决策树
数据是树形结构?
├── 是 → ✅ 组合模式
└── 否
├── 类之间有 is-a 关系?
│ ├── 是 → ✅ 继承
│ └── 否
│ ├── 动态叠加功能?
│ │ ├── 是 → ✅ 装饰器
│ │ └── 否 → 普通类
5.4 组合 vs 装饰器:易混淆
| 维度 | 组合 | 装饰器 |
|---|---|---|
| 结构 | 树形 | 链式 |
| 数量 | N 个子节点 | 1 个被装饰者 |
| 递归 | 递归遍历子节点 | 链式调用 |
| 类比 | 文件夹 | 包装纸 |
大白商城选型:
| 场景 | 选哪个 | 原因 |
|---|---|---|
| 商品类目 | 组合 | 树形结构 |
| 价格计算 | 装饰器 | 链式叠加 |
| 权限树 | 组合 | 树形结构 |
| 日志装饰 | 装饰器 | 链式叠加 |
六、JDK 的 File 类是组合模式吗?
6.1 java.io.File:文件系统树
源码位置:
- OpenJDK 17
src/java.base/share/classes/java/io/File.java
核心源码(简化版):
/**
* ✅ JDK 组合模式:File 类
* <p>
* File 既可以表示"文件"(叶子),也可以表示"目录"(容器)
*/
public class File implements Comparable<File>, Serializable {
/**
* ✅ 叶子操作
*/
public boolean isFile() { ... }
public long length() { ... }
/**
* ✅ 容器操作
*/
public boolean isDirectory() { ... }
public File[] listFiles() { ... }
public boolean mkdir() { ... }
/**
* ✅ 删除
*/
public boolean delete() { ... }
}
完整树形操作:
/**
* ✅ 递归遍历文件系统
*/
public void printFileTree(File file, int level) {
System.out.println(" ".repeat(level) + (file.isDirectory() ? "📁 " : "📄 ") + file.getName());
if (file.isDirectory()) {
File[] children = file.listFiles();
if (children != null) {
for (File child : children) {
printFileTree(child, level + 1);
}
}
}
}
// 使用
printFileTree(new File("/data"), 0);
关键点:
| 关键点 | 解释 |
|---|---|
isFile() / isDirectory() | 区分叶子/容器 |
listFiles() | 容器的子节点 |
| File 本身 | 同时支持文件(叶子)和目录(容器) |
6.2 树形结构的核心操作
4 大核心操作:
| 操作 | 解释 | 大白商城例子 |
|---|---|---|
| 遍历 | 访问所有节点 | 打印整棵树 |
| 查找 | 按条件找节点 | 查"苹果"节点 |
| 统计 | 汇总所有叶子 | 统计商品数 |
| 过滤 | 留下满足条件的 | 查所有"电子" |
完整实现:
/**
* 树形结构的 4 大操作
*/
public class CategoryOperations {
/**
* 1. 遍历:打印
*/
public void traverse(CategoryComponent node, int level) {
System.out.println(" ".repeat(level) + node.getName());
for (CategoryComponent child : node.getChildren()) {
traverse(child, level + 1);
}
}
/**
* 2. 查找:按 ID
*/
public CategoryComponent findById(CategoryComponent node, Long id) {
if (node.getId().equals(id)) return node;
for (CategoryComponent child : node.getChildren()) {
CategoryComponent found = findById(child, id);
if (found != null) return found;
}
return null;
}
/**
* 3. 统计:叶子数量
*/
public int countLeaves(CategoryComponent node) {
if (node instanceof GoodsCategory) return 1;
int total = 0;
for (CategoryComponent child : node.getChildren()) {
total += countLeaves(child);
}
return total;
}
/**
* 4. 过滤:按名字
*/
public List<CategoryComponent> filterByName(CategoryComponent node, String keyword) {
List<CategoryComponent> result = new ArrayList<>();
if (node.getName().contains(keyword)) {
result.add(node);
}
for (CategoryComponent child : node.getChildren()) {
result.addAll(filterByName(child, keyword));
}
return result;
}
}
七、工程决策 Checklist
7.1 ✅ 这 5 种情况,强烈建议用组合
| 序号 | 场景 | 原因 |
|---|---|---|
| ① | 树形结构数据 | 类目/权限/组织 |
| ② | 无限层级 | 动态层级 |
| ③ | 整体-部分关系 | 文件系统 |
| ④ | 统一处理节点和叶子 | 一致性 |
| ⑤ | 递归操作 | 树形遍历 |
7.2 ❌ 这 5 种情况,绝对不要用组合
| 序号 | 场景 | 原因 |
|---|---|---|
| ① | 数据是扁平的 | 不需要树形 |
| ② | 层级固定 | 用枚举/常量 |
| ③ | 节点和叶子行为差异大 | 拆开两个类 |
| ④ | 性能敏感 | 递归开销大 |
| ⑤ | 树太深(>1000 层) | 栈溢出 |
7.3 ⚠️ 组合的 6 大常见坑
| 序号 | 坑 | 表现 | 解决方案 |
|---|---|---|---|
| ① | 循环引用 | 节点 A 包含 B,B 包含 A | 检查 ID 唯一 |
| ② | 栈溢出 | 树太深递归 | 改用迭代 |
| ③ | 叶子不能 add | 抛 UnsupportedOperationException | 默认实现 |
| ④ | 类型判断 | instanceof GoodsCategory | 用访问者模式 |
| ⑤ | 性能差 | 每次递归遍历 | 缓存结果 |
| ⑥ | 数据库 N+1 | 每个节点一次查询 | 一次查整树 |
7.4 面试官视角:组合高频追问
Q1:组合 vs 装饰器,区别是什么?
答:组合整体-部分(树形),装饰器功能叠加(链式)。组合是 is-part-of,装饰器是 has-a。
Q2:JDK 的 File 是组合模式吗?
答:是的。File 既可以表示"文件"(叶子),也可以表示"目录"(容器)。
isFile()/isDirectory()/listFiles()是关键 API。
Q3:组合模式的"透明式"vs"安全式"?
答:透明式叶子也实现 add/remove(抛异常),客户端完全统一。安全式叶子不实现 add/remove,客户端要判断类型。推荐透明式(统一接口)。
八、与其他模式协作
8.1 组合 + 访问者 = 在树上做各种操作
/**
* 组合 + 访问者:树形数据 + 各种操作
*/
public interface CategoryVisitor {
void visit(GoodsCategory category);
void visit(CategoryNode node);
}
public class CountVisitor implements CategoryVisitor {
private long totalGoods = 0;
@Override
public void visit(GoodsCategory category) {
totalGoods += category.getGoodsCount();
}
@Override
public void visit(CategoryNode node) {
// 节点本身不统计
}
}
8.2 组合 + 迭代器 = 树形遍历
/**
* 组合 + 迭代器:树形遍历
*/
public class CategoryIterator implements Iterator<CategoryComponent> {
private final Queue<CategoryComponent> queue = new LinkedList<>();
public CategoryIterator(CategoryComponent root) {
queue.add(root);
}
@Override
public boolean hasNext() {
return !queue.isEmpty();
}
@Override
public CategoryComponent next() {
CategoryComponent node = queue.poll();
queue.addAll(node.getChildren());
return node;
}
}
8.3 组合 + 享元 = 共享叶子节点
/**
* 组合 + 享元:相同叶子共享
*/
public class CategoryFlyweight {
private static final Map<String, GoodsCategory> CACHE = new ConcurrentHashMap<>();
public static GoodsCategory get(Long id, String name, Long count) {
String key = id + ":" + name;
return CACHE.computeIfAbsent(key, k -> new GoodsCategory(id, name, count));
}
}
8.4 大白商城模式协作全景图
┌──────────────┐
│ 组合 │ ← 本篇
└──────┬───────┘
│
┌───────────┬───────┼───────┬───────────┐
│ │ │ │ │
┌───▼───┐ ┌────▼───┐ ┌▼────┐ ┌▼─────┐ ┌───▼────┐
│访问者│ │迭代器 │ │享元 │ │责任链│ │ 装饰器 │
│(操作) │ │(遍历) │ │(共享)│ │(链) │ │(叠加) │
└───────┘ └────────┘ └──────┘ └──────┘ └───────┘
43 篇 39 篇 23 篇 31 篇 16 篇
九、本篇小结 + 下篇预告
9.1 本篇小结(5 个核心要点)
- 本质:组合 = 树形结构,让客户端一致处理节点和叶子。
- 场景:类目/权限/组织/文件系统,任何树形都用组合。
- 对比:组合 vs 继承(is-part-of vs is-a)/ 组合 vs 装饰器(树形 vs 链式)。
- JDK File:经典组合模式,
isFile/isDirectory/listFiles三大 API。 - 避坑:循环引用 / 栈溢出 / 叶子抛异常 / 数据库 N+1。
9.2 一句话总结
组合不是"换种方式存数据",是"用树形结构 + 统一接口处理"。大白商城的商品类目从硬编码 3 层改成无限层级,订单组织架构从扁平表改成树形——业务和性能都大幅提升。
9.3 知识脑图
组合模式
├── 3 大角色
│ ├── 抽象组件(CategoryComponent)
│ ├── 容器(CategoryNode)
│ └── 叶子(GoodsCategory)
├── 4 大操作
│ ├── 遍历(print)
│ ├── 查找(findById)
│ ├── 统计(count)
│ └── 过滤(filter)
├── 3 种实现
│ ├── 透明式(叶子实现 add/remove 抛异常)✅
│ ├── 安全式(叶子不实现 add/remove)
│ └── 混合式
├── 模式对比
│ ├── vs 继承(is-part-of vs is-a)
│ ├── vs 装饰器(树形 vs 链式)
│ └── vs 享元(独立 vs 共享)
├── JDK File
│ ├── isFile()(叶子判断)
│ ├── isDirectory()(容器判断)
│ └── listFiles()(子节点)
└── 模式协作
├── + 访问者(在树上操作)
├── + 迭代器(树形遍历)
└── + 享元(共享叶子)
9.4 下篇预告
第 21 篇【组合模式 - 源码剖析篇】:JDK / AWT / MyBatis 中的组合实现
下一篇我们会深入源码,回答三个问题:
- JDK 的
java.io.File怎么用组合表示文件系统? - AWT 的
Container/Component怎么用组合表示 UI 树? - MyBatis 的
SqlNode怎么用组合表示动态 SQL 树?
并附完整的源码解读 + 流程图 + 大白商城的"抄作业"实践。
觉得对您有帮助,麻烦点点关注啦,您的关注是我创作的最大动力~ 🎯
&spm=1001.2101.3001.5002&articleId=163833001&d=1&t=3&u=623660a03994468e898cae2bd356ce8c)
757

被折叠的 条评论
为什么被折叠?



