一、为什么每个Java开发者都要学组合模式?🤔
场景痛点:
当你需要处理像公司组织架构、电脑文件系统、电商商品分类这样的树形结构数据时,是不是经常遇到这样的困扰?
-
代码中充斥着
if-else
判断层级关系 -
添加新类型的节点要修改大量代码
-
遍历不同层级的对象非常麻烦
组合模式的价值:
✨ 统一处理:文件和文件夹用同一套API操作
✨ 无限嵌套:轻松实现"文件夹里套文件夹"的树形结构
✨ 扩展自由:新增节点类型无需修改已有代码
二、3分钟快速理解组合模式 🚀
生活案例:公司组织架构
-
叶子节点:普通员工(没有下属)
-
组合节点:部门经理(管理多个员工/小组)
-
统一接口:都支持查看人员信息
UML类图解析
三、手把手实现文件系统案例 📂
步骤1:定义组件接口
public interface FileSystemComponent { // 公共操作方法 void display(int depth); // 默认实现(叶子节点不需要) default void add(FileSystemComponent component) { throw new UnsupportedOperationException(); } default void remove(FileSystemComponent component) { throw new UnsupportedOperationException(); } default FileSystemComponent getChild(int index) { throw new UnsupportedOperationException(); } }
步骤2:实现叶子节点(文件)
public class File implements FileSystemComponent { private String name; public File(String name) { this.name = name; } @Override public void display(int depth) { System.out.println("-".repeat(depth) + "📄 " + name); } }
步骤3:实现组合节点(文件夹)
public class Folder implements FileSystemComponent { private String name; private List<FileSystemComponent> children = new ArrayList<>(); public Folder(String name) { this.name = name; } @Override public void display(int depth) { System.out.println("-".repeat(depth) + "📁 " + name); for (FileSystemComponent child : children) { child.display(depth + 2); } } @Override public void add(FileSystemComponent component) { children.add(component); } @Override public void remove(FileSystemComponent component) { children.remove(component); } @Override public FileSystemComponent getChild(int index) { return children.get(index); } }
步骤4:客户端使用
public class Client { public static void main(String[] args) { // 创建文件 FileSystemComponent file1 = new File("简历.pdf"); FileSystemComponent file2 = new File("照片.jpg"); // 创建子文件夹 Folder subFolder = new Folder("工作资料"); subFolder.add(new File("项目计划.doc")); subFolder.add(new File("会议记录.txt")); // 创建根文件夹 Folder root = new Folder("我的电脑"); root.add(file1); root.add(file2); root.add(subFolder); // 展示所有文件 root.display(0); } }
运行结果:
📁 我的电脑 📄 简历.pdf 📄 照片.jpg 📁 工作资料 📄 项目计划.doc 📄 会议记录.txt
四、组合模式在哪些场景最吃香?💼
场景 | 典型应用 | 优势体现 |
---|---|---|
文件系统 | 管理文件和文件夹 | 统一操作接口,支持无限嵌套 |
GUI组件 | 窗口包含面板/按钮等组件 | 批量设置样式,递归渲染 |
电商分类 | 商品类目层级管理 | 动态添加删除类目,灵活展示 |
组织架构 | 公司部门与员工管理 | 统计部门人数,快速生成架构图 |
游戏场景 | 地图中的区域与子区域 | 统一处理碰撞检测,递归遍历 |
五、组合模式的三大坑点与避雷指南 ⚡
坑点1:叶子节点不应该有add/remove方法
错误示范:
File file = new File("test.txt"); file.add(new File("error.txt")); // 抛出异常!
正确做法:
// 接口中提供默认实现抛出异常 default void add(Component component) { throw new UnsupportedOperationException(); }
坑点2:忘记实现迭代方法
错误现象:
文件夹只显示自己,不显示子文件
正确实现:
public void display() { // 显示自己 showSelf(); // 递归显示子组件 for (Component child : children) { child.display(); } }
坑点3:循环引用检测
危险场景:
文件夹A包含文件夹B,文件夹B又包含文件夹A
解决方案:
public void add(Component component) { if (isCyclic(this, component)) { throw new IllegalArgumentException("检测到循环引用!"); } children.add(component); } private boolean isCyclic(Component parent, Component child) { // 实现循环引用检测算法 }
六、组合模式 PK 其他设计模式 🥊
对比维度 | 组合模式 | 装饰器模式 | 享元模式 |
---|---|---|---|
目的 | 处理树形结构 | 动态添加职责 | 共享细粒度对象 |
结构 | 树状层级 | 链式包装 | 对象池 |
关系 | 整体-部分关系 | 增强功能 | 共享状态 |
典型应用 | 文件系统 | IO流包装 | 字符缓存池 |
七、组合模式面试必考题 💯
-
Q:组合模式如何保证叶子节点不能添加子节点?
A:通过在Component接口中为add/remove方法提供默认实现,抛出UnsupportedOperationException -
Q:组合模式在JDK哪些地方有应用?
A:java.awt.Container的add()方法、XML解析中的DOM树结构 -
Q:如何处理组合对象的循环引用问题?
A:在添加子节点时检查父节点链,可以使用深度优先搜索检测环路
八、实战升级:Spring风格实现 🌟
场景:电商商品类目管理
// 定义商品组件接口 public interface ProductComponent { void display(); double getPrice(); } // 实现叶子节点(具体商品) @Component @Scope("prototype") public class Product implements ProductComponent { private String name; private double price; // 省略getter/setter @Override public void display() { System.out.println(name + " ¥" + price); } } // 实现组合节点(商品分类) @Component @Scope("prototype") public class ProductCategory implements ProductComponent { private String name; @Autowired private List<ProductComponent> items = new ArrayList<>(); // 省略其他方法 @Override public double getPrice() { return items.stream() .mapToDouble(ProductComponent::getPrice) .sum(); } } // 使用示例 @RestController public class ProductController { @Autowired private ProductCategory electronics; @GetMapping("/products") public String showProducts() { electronics.display(); return "总价值:" + electronics.getPrice(); }