一、组合模式定义
Composite 模式是一种结构型模式。它通过递归手段来构造树形的对象结构,使得用户对单个对象(叶子)和组合对象(容器)的使用具有一致性——客户端无需关心当前操作的是文件还是文件夹,只管调用统一接口即可访问整棵树。
一句话:当你要处理的对象天然是树形层级(文件系统、菜单、组织架构、UI 控件树)时,用组合模式让"叶子"和"枝干"共用一套接口,客户端统一对待。
二、为什么需要组合模式(动机)
假设没有组合模式,客户端遍历目录树时只能到处写if (node instanceof Folder)判断是否是容器,再决定是否递归——代码和具体类型紧耦合,新增一种节点类型就要改遍所有遍历逻辑。
组合模式的解决思路:让叶子节点和容器节点都实现同一个抽象接口,容器内部维护子节点列表。客户端拿到一个节点,永远只调统一方法(如display()、getChild()),至于它是文件还是文件夹,由对象自己决定行为。树形结构的递归遍历因此变得极其干净。
三、组合模式的结构(角色与职责)
| 角色 | 职责 |
|---|---|
| 抽象构件(Component) | 树形结构的节点抽象。为所有对象定义统一接口(如display),并声明管理子节点的方法(add/remove/getChild),可选提供管理父节点的方法 |
| 叶子节点(Leaf) | Component 的实现子类,代表树的最末端(没有子节点),如File |
| 容器节点(Composite) | Component 的实现子类,代表有子节点的枝干,内部维护子节点集合并递归处理,如Folder |
四、代码实现(透明式:文件系统目录树示例)
本节是透明式写法(Component 声明全部方法,叶子默认空实现),也是最常用的写法。
4.1 抽象构件:IFile
importjava.util.List;/** * 文件节点抽象:是"文件"和"目录"的公共父类 */publicinterfaceIFile{voiddisplay();// 显示自身名称booleanadd(IFilefile);// 添加子节点booleanremove(IFilefile);// 移除子节点List<IFile>getChild();// 返回子节点列表}4.2 叶子节点:File
importjava.util.List;publicclassFileimplementsIFile{privateStringname;publicFile(Stringname){this.name=name;}@Overridepublicvoiddisplay(){System.out.println(name);}// 叶子没有子节点,默认返回 false(透明式的典型处理,见第五节取舍)@Overridepublicbooleanadd(IFilefile){returnfalse;}@Overridepublicbooleanremove(IFilefile){returnfalse;}@OverridepublicList<IFile>getChild(){returnnull;}}4.3 容器节点:Folder
importjava.util.ArrayList;importjava.util.List;publicclassFolderimplementsIFile{privateStringname;privateList<IFile>children;publicFolder(Stringname){this.name=name;children=newArrayList<>();}@Overridepublicvoiddisplay(){System.out.println(name);}@Overridepublicbooleanadd(IFilefile){returnchildren.add(file);}@Overridepublicbooleanremove(IFilefile){returnchildren.remove(file);}@OverridepublicList<IFile>getChild(){returnchildren;}}4.4 客户端:递归遍历整棵树
importjava.util.List;publicclassMainClass{publicstaticvoidmain(String[]args){Folderroot=newFolder("C:");Folderstudy=newFolder("学习资料");Fileresume=newFile("简历.pdf");Folderphotos=newFolder("照片");Filetravel=newFile("旅游.jpg");root.add(study);root.add(resume);study.add(photos);photos.add(travel);// 从根统一遍历,客户端完全不关心节点类型displayTree(root,0);}// 统一接口遍历:叶子 getChild() 返回 null,自动终止递归publicstaticvoiddisplayTree(IFilenode,intdepth){for(inti=0;i<depth;i++){System.out.print(" ");}node.display();List<IFile>children=node.getChild();if(children!=null){for(IFilechild:children){displayTree(child,depth+1);}}}}运行结果(缩进即层级):
C: 学习资料 照片 旅游.jpg 简历.pdf关键点:客户端遍历时没有写任何
instanceof判断——正是因为File.getChild()返回null、统一接口让叶子也能"假装"有子节点列表,递归才能用同一套逻辑对待所有节点。这正是组合模式的价值所在。
五、面试必考:透明式 vs 安全式(含完整代码)
上面的写法里,Component声明了add/remove/getChild,而Leaf的这几个方法只能返回false/null。这就是组合模式最核心的取舍:
| 方案 | 做法 | 优点 | 缺点 |
|---|---|---|---|
| 透明式(第四节) | 在Component中声明全部方法,叶子默认空实现/抛异常 | 客户端可以完全统一地用Component处理所有节点,无需类型判断 | 叶子"假装"能 add 但无效,类型不安全,易误调用 |
| 安全式(本节) | 只在Composite中声明add/remove,Component不声明 | 叶子不会出现无意义的 add 方法,编译期就拦住误用 | 客户端必须区分叶子/容器(要用instanceof或强转),失去统一性 |
5.1 安全式代码
importjava.util.ArrayList;importjava.util.List;// 安全式:Component 只声明公共行为,不声明增删interfaceIFileSafe{voiddisplay();List<IFileSafe>getChild();}// 叶子:根本没有 add/remove 方法classFileSafeimplementsIFileSafe{privateStringname;publicFileSafe(Stringname){this.name=name;}@Overridepublicvoiddisplay(){System.out.println(name);}@OverridepublicList<IFileSafe>getChild(){returnnull;}}// 容器:增删只在 Composite 自己身上声明classFolderSafeimplementsIFileSafe{privateStringname;privateList<IFileSafe>children=newArrayList<>();publicFolderSafe(Stringname){this.name=name;}@Overridepublicvoiddisplay(){System.out.println(name);}@OverridepublicList<IFileSafe>getChild(){returnchildren;}// 独有方法:只有持有 FolderSafe 引用才能调用publicbooleanadd(IFileSafefile){returnchildren.add(file);}publicbooleanremove(IFileSafefile){returnchildren.remove(file);}}客户端差异:
FolderSaferoot=newFolderSafe("C:");root.add(newFileSafe("a.txt"));// ✅ 持有 FolderSafe,可 add// IFileSafe node = root;// node.add(...); // ❌ 编译报错:Component 没有 add —— 这就是"安全"5.2 折中方案(推荐)
实际项目多数选透明式(换调用统一),但为避免"叶子误调 add 静默失败",可让叶子的方法直接抛异常而非默默返回false:
@Overridepublicbooleanadd(IFilefile){thrownewUnsupportedOperationException("叶子节点不支持 add");}这样既保留了透明式的统一调用,又能在误用时立刻报错,兼顾透明与安全。
六、应用场景
- 对象结构呈树形层级:文件系统、菜单/导航树、公司组织架构、XML/JSON 节点、GUI 容器控件树(如 Swing 的
Container)。 - 希望客户端忽略叶子与容器的差异,统一地增删、遍历、执行操作。
- 树形结构可能频繁变动(动态增删节点),组合模式让增删与遍历解耦。
七、真实世界中的组合模式(JDK / 框架)
组合模式不是纸上谈兵,JDK 和主流框架里随处可见:
java.awt.Component/java.awt.Container:Swing 里Container继承Component并持有子Component列表,是最经典的 GOF 组合实现。org.w3c.dom.Node:XML DOM 树中,元素节点和文本节点都实现Node,可统一遍历。javax.swing/ 各类 UI 控件树:窗口→面板→按钮的层级就是组合结构。- 主流框架的配置/路由树(如 Spring Cloud 的复合配置、规则引擎的 AST)底层也是组合思想的变体。
下次看到"一个对象既能是单个元素、又能是一堆元素的容器"的设计,基本就是组合模式。
八、优缺点
优点
- 客户端一致对待叶子与容器,调用方代码简洁(无
instanceof满天飞)。 - 符合开闭原则:新增节点类型只需实现
Component,遍历逻辑不动。 - 递归自然契合树形结构,动态增删节点非常方便。
缺点
- 设计较抽象,难限制"某容器只能放特定类型子节点"(编译期无法约束)。
- 透明式下叶子拥有了不该有的方法,易误用;安全式又牺牲统一性。
- 容器内部常用
ArrayList等非线程安全集合,多线程并发增删需外部同步;树过深时递归遍历还有栈溢出风险。
九、总结
- 组合模式 = 用同一接口统一"叶子"和"容器",通过递归构建并遍历树形对象结构,属于结构型模式。
- 三角色:抽象构件
Component、叶子Leaf、容器Composite。 - 最核心取舍是透明式 vs 安全式:要统一调用选透明式(叶子抛异常更稳),要类型安全选安全式(或叶子抛异常折中)。
- 选型口诀:对象是一棵树、且想统一操作节点时,用组合模式。
一句话总结:当你的数据天然成树,别让客户端满屏
instanceof区分文件还是文件夹——交给组合模式,一视同仁递归到底。