设计模式之组合模式
组合模式是一种结构型设计模式,它允许你将对象组合成树形结构来表示“部分-整体”的层次关系,使得客户端可以统一处理单个对象和组合对象,而无需关心具体的对象类型。
解析这个定义:
- “结构型”&“层次关系”,说明此模式针对的对象结构十分典型,对象间的关系有明显的结构特征。
- “树形结构”,说明此模式就是针对树形结构的对象关系。
- “部分-整体”,说明组合对象包含了单个对象。
- “客户端可以统一处理”,客户端可以不关心对象是属于单个对象还是组合对象,表明单个对象和组合对象有相同的超类,而且实现了相同的接口。

在设计业务场景中,有部分整体关系,成树形结构的,有许多,比如:
- 文件 vs 文件夹
- 员工 vs 部门
- 评论 vs 回复
从对定义的解析,到树形结构的实现,可以推导出组合模式的结构图。

但,组合模式的关键点,不在于结构图,而在于真正理解这个模式解决的问题,已经为什么要如此设计。
组合模式的核心本质之一,是将递归逻辑从客户端转移到“组合对象”内部。
我们从实际的例子,来一步步看组合模式是如何演进的。
初始实现,无模式,硬编码递归,最初的代码可能是这样的:
import java.util.ArrayList;
import java.util.List;
// 文件类
class File {
private String name;
public File(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
// 目录类
class Directory {
private List<Object> children = new ArrayList<>(); // 目录可以包含 File 和 Directory
public void add(Object obj) { // 允许存 File 和 Directory
children.add(obj);
}
public List<Object> getChildren() {
return children;
}
}
// Client 代码
public class Client {
public static void printFileNames(Directory dir) {
for (Object obj : dir.getChildren()) {
if (obj instanceof File) {
System.out.println(((File) obj).getName()); // 直接打印文件名
} else if (obj instanceof Directory) {
printFileNames((Directory) obj); // 递归遍历子目录
}
}
}
public static void main(String[] args) {
Directory root = new Directory();
Directory subDir = new Directory();
File file1 = new File("file1.txt");
File file2 = new File("file2.txt");
root.add(file1);
root.add(subDir);
subDir.add(file2);
printFileNames(root); // 输出:file1.txt, file2.txt
}
}
Client 端必须自己处理递归逻辑,printFileNames()
需要 Client
端手动判断 File
和 Directory
,涉及到了具体类细节。如果在多个地方都需要递归遍历,就会有大量重复代码。
如果以后增加新的文件系统元素(如 SymbolicLink
),必须修改 printFileNames()
的 if-else
代码。违反“对扩展开放,对修改封闭”原则(OCP 原则)。
如何解决,还记得组合模式的核心吗,将递归逻辑从客户端转移到“组合对象”内部。因此这个递归逻辑应该封装到组合对象内部,而不应该由client来实现,不然多个client都需要编写复杂麻烦的递归逻辑。
对于共性抽取,文件与文件夹之间存在着行为共性,这也是可以提取出相同超类的前提。来看看组合模式的实现。
import java.util.ArrayList;
import java.util.List;
// 统一接口
interface Component {
void printFileNames();
void add(Component component); // 让所有组件都能被 add
}
// 文件类
class File implements Component {
private String name;
public File(String name) { this.name = name; }
@Override
public void printFileNames() {
System.out.println(name);
}
@Override
public void add(Component component) {
throw new UnsupportedOperationException("Cannot add to a file.");
}
}
// 目录类
class Directory implements Component {
private List<Component> children = new ArrayList<>();
@Override
public void add(Component component) {
children.add(component);
}
@Override
public void printFileNames() {
for (Component child : children) {
child.printFileNames(); // 递归调用
}
}
}
// Client 代码
public class Client {
public static void main(String[] args) {
Component root = new Directory(); // 现在 root 只用 Component,不用 Directory
Component subDir = new Directory();
Component file1 = new File("file1.txt");
Component file2 = new File("file2.txt");
root.add(file1); // ✅ 不需要强制类型转换
root.add(subDir);
subDir.add(file2);
root.printFileNames(); // Client 只调用 Component 的方法
}
}
组合模式在设计模式中,属于较难学的一个模式,最大的难点在于理解单个对象与组合对象为何要抽取出一个共性超类,本质是为了客户端能够统一对待。另一方面,组合模式的应用场景十分典型,能够快速识别,即有树形层次结构的业务场景,识别到这种场景,应第一反应想到应用组合模式。