组合模式 (Composite)
📖 通俗理解
想象你电脑里的文件系统:
- 文件夹里可以放文件
- 文件夹里也可以放文件夹
- 打开一个文件夹,里面的结构可能和外面一样
这就是一个树形结构!无论是文件还是文件夹,你都可以对它们进行相同的操作(复制、删除、移动)。
组合模式就是:把对象组合成树形结构,让客户端对单个对象和组合对象的使用具有一致性。
🎯 解决什么问题?
场景:你有一个树形结构的数据,希望能够统一处理叶子节点和容器节点。
例子:
- 文件系统(文件和文件夹)
- 公司组织架构(员工和部门)
- 菜单系统(菜单项和子菜单)
🌰 生活中的例子
- 文件夹结构:文件夹可以包含文件和子文件夹
- 公司组织:部门可以包含员工和子部门
- 电商分类:分类可以包含商品和子分类
- XML/HTML:标签可以包含文本和子标签
💻 Java 代码实现
场景:文件系统
第一步:定义抽象组件
/**
* 抽象组件:文件系统节点
*/
public abstract class FileSystemNode {
protected String name;
public FileSystemNode(String name) {
this.name = name;
}
public String getName() {
return name;
}
// 显示节点信息(抽象方法)
public abstract void display(int depth);
// 获取大小(抽象方法)
public abstract long getSize();
// 以下方法对于叶子节点不适用,提供默认实现
public void add(FileSystemNode node) {
throw new UnsupportedOperationException("不支持添加操作");
}
public void remove(FileSystemNode node) {
throw new UnsupportedOperationException("不支持删除操作");
}
public FileSystemNode getChild(int index) {
throw new UnsupportedOperationException("不支持获取子节点操作");
}
}
第二步:实现叶子节点(文件)
/**
* 叶子节点:文件
*/
public class File extends FileSystemNode {
private long size;
public File(String name, long size) {
super(name);
this.size = size;
}
@Override
public void display(int depth) {
// 根据深度打印缩进
String indent = " ".repeat(depth);
System.out.println(indent + "📄 " + name + " (" + size + " bytes)");
}
@Override
public long getSize() {
return size;
}
}
第三步:实现容器节点(文件夹)
import java.util.ArrayList;
import java.util.List;
/**
* 容器节点:文件夹
*/
public class Folder extends FileSystemNode {
private List<FileSystemNode> children = new ArrayList<>();
public Folder(String name) {
super(name);
}
@Override
public void add(FileSystemNode node) {
children.add(node);
}
@Override
public void remove(FileSystemNode node) {
children.remove(node);
}
@Override
public FileSystemNode getChild(int index) {
return children.get(index);
}
@Override
public void display(int depth) {
String indent = " ".repeat(depth);
System.out.println(indent + "📁 " + name + "/");
// 递归显示所有子节点
for (FileSystemNode child : children) {
child.display(depth + 1);
}
}
@Override
public long getSize() {
long totalSize = 0;
// 递归计算所有子节点的大小
for (FileSystemNode child : children) {
totalSize += child.getSize();
}
return totalSize;
}
}
第四步:使用组合模式
public class Client {
public static void main(String[] args) {
// 创建根目录
Folder root = new Folder("root");
// 创建子目录
Folder documents = new Folder("documents");
Folder pictures = new Folder("pictures");
Folder music = new Folder("music");
// 添加文件到 documents
documents.add(new File("简历.docx", 25600));
documents.add(new File("报告.pdf", 102400));
// 在 documents 下创建子目录
Folder work = new Folder("work");
work.add(new File("项目计划.xlsx", 51200));
work.add(new File("会议纪要.docx", 12800));
documents.add(work);
// 添加文件到 pictures
pictures.add(new File("风景.jpg", 2048000));
pictures.add(new File("头像.png", 512000));
// 添加文件到 music
music.add(new File("歌曲1.mp3", 5120000));
music.add(new File("歌曲2.mp3", 4800000));
// 组装目录结构
root.add(documents);
root.add(pictures);
root.add(music);
root.add(new File("readme.txt", 1024));
// 显示整个目录结构
System.out.println("=== 目录结构 ===");
root.display(0);
// 计算总大小
System.out.println("\n=== 目录大小 ===");
System.out.println("root 总大小: " + root.getSize() + " bytes");
System.out.println("documents 大小: " + documents.getSize() + " bytes");
System.out.println("pictures 大小: " + pictures.getSize() + " bytes");
}
}
输出:
=== 目录结构 ===
📁 root/
📁 documents/
📄 简历.docx (25600 bytes)
📄 报告.pdf (102400 bytes)
📁 work/
📄 项目计划.xlsx (51200 bytes)
📄 会议纪要.docx (12800 bytes)
📁 pictures/
📄 风景.jpg (2048000 bytes)
📄 头像.png (512000 bytes)
📁 music/
📄 歌曲1.mp3 (5120000 bytes)
📄 歌曲2.mp3 (4800000 bytes)
📄 readme.txt (1024 bytes)
=== 目录大小 ===
root 总大小: 12673024 bytes
documents 大小: 192000 bytes
pictures 大小: 2560000 bytes
🔥 实战案例:公司组织架构
/**
* 抽象组件:组织节点
*/
public abstract class OrganizationNode {
protected String name;
public OrganizationNode(String name) {
this.name = name;
}
// 计算薪资
public abstract double getSalary();
// 显示组织结构
public abstract void display(int depth);
public void add(OrganizationNode node) {
throw new UnsupportedOperationException();
}
public void remove(OrganizationNode node) {
throw new UnsupportedOperationException();
}
}
/**
* 叶子节点:员工
*/
public class Employee extends OrganizationNode {
private String position;
private double salary;
public Employee(String name, String position, double salary) {
super(name);
this.position = position;
this.salary = salary;
}
@Override
public double getSalary() {
return salary;
}
@Override
public void display(int depth) {
String indent = " ".repeat(depth);
System.out.println(indent + "👤 " + name + " - " + position +
" (¥" + salary + ")");
}
}
/**
* 容器节点:部门
*/
public class Department extends OrganizationNode {
private List<OrganizationNode> members = new ArrayList<>();
public Department(String name) {
super(name);
}
@Override
public void add(OrganizationNode node) {
members.add(node);
}
@Override
public void remove(OrganizationNode node) {
members.remove(node);
}
@Override
public double getSalary() {
double total = 0;
for (OrganizationNode member : members) {
total += member.getSalary();
}
return total;
}
@Override
public void display(int depth) {
String indent = " ".repeat(depth);
System.out.println(indent + "🏢 " + name + " (总薪资: ¥" + getSalary() + ")");
for (OrganizationNode member : members) {
member.display(depth + 1);
}
}
}
使用方式:
public class CompanyDemo {
public static void main(String[] args) {
// 创建公司
Department company = new Department("ABC科技有限公司");
// 技术部
Department techDept = new Department("技术部");
techDept.add(new Employee("张三", "技术总监", 50000));
// 技术部下的研发组
Department devTeam = new Department("研发组");
devTeam.add(new Employee("李四", "高级工程师", 30000));
devTeam.add(new Employee("王五", "中级工程师", 20000));
devTeam.add(new Employee("赵六", "初级工程师", 12000));
techDept.add(devTeam);
// 技术部下的测试组
Department testTeam = new Department("测试组");
testTeam.add(new Employee("钱七", "测试经理", 25000));
testTeam.add(new Employee("孙八", "测试工程师", 15000));
techDept.add(testTeam);
// 人事部
Department hrDept = new Department("人事部");
hrDept.add(new Employee("周九", "人事经理", 25000));
hrDept.add(new Employee("吴十", "招聘专员", 10000));
// 组装公司
company.add(new Employee("老板", "CEO", 100000));
company.add(techDept);
company.add(hrDept);
// 显示组织架构
System.out.println("=== 公司组织架构 ===");
company.display(0);
}
}
输出:
=== 公司组织架构 ===
🏢 ABC科技有限公司 (总薪资: ¥287000.0)
👤 老板 - CEO (¥100000.0)
🏢 技术部 (总薪资: ¥152000.0)
👤 张三 - 技术总监 (¥50000.0)
🏢 研发组 (总薪资: ¥62000.0)
👤 李四 - 高级工程师 (¥30000.0)
👤 王五 - 中级工程师 (¥20000.0)
👤 赵六 - 初级工程师 (¥12000.0)
🏢 测试组 (总薪资: ¥40000.0)
👤 钱七 - 测试经理 (¥25000.0)
👤 孙八 - 测试工程师 (¥15000.0)
🏢 人事部 (总薪资: ¥35000.0)
👤 周九 - 人事经理 (¥25000.0)
👤 吴十 - 招聘专员 (¥10000.0)
📊 类图结构
┌────────────────┐
│ Component │ ◄─────────────┐
│ + operation() │ │
│ + add() │ │
│ + remove() │ │
└───────▲────────┘ │
│ │
┌────────────┴────────────┐ │
│ │ │
┌────┴────┐ ┌─────┴─────┐ │
│ Leaf │ │ Composite │ │ 包含
│+operation│ │+operation │ ────┘
└─────────┘ │+add() │
│+remove() │
└───────────┘
✅ 适用场景
- 表示树形结构:如文件系统、组织架构、菜单
- 统一处理:希望统一处理叶子和容器对象
- 递归结构:整体与部分具有相同的操作
⚠️ 透明模式 vs 安全模式
| 模式 | 特点 | 优点 | 缺点 |
|---|---|---|---|
| 透明模式 | 所有方法都在抽象类中声明 | 叶子和容器完全一致 | 叶子节点调用 add/remove 会出错 |
| 安全模式 | add/remove 只在容器中声明 | 不会误操作 | 需要区分叶子和容器 |
上面的例子使用的是透明模式(在抽象类中声明了 add/remove)。
小结
组合模式的核心:将对象组合成树形结构,统一叶子和容器的操作。
适合场景:任何"整体-部分"的层次结构,如文件系统、组织架构、菜单等。
👉 下一篇:装饰器模式
