前几天整理博客文件,桌面上散着封面图、UML 图和 Markdown 文件。
一个个文件还好说。碰上文件夹套文件夹,统计大小、打印目录就有点绕了。
组合模式专门处理这种一层套一层的树形结构。

先看一棵目录树
拿“狗哥博客”这个文件夹来说,里面有图片文件夹,也有文章文件。
图片文件夹里还能继续放封面和 UML 图。
这里有两种东西:文件和文件夹。
文件已经到头了。文件夹还能装文件,也能再装文件夹。济南话说,就是一层又一层,怪容易绕。
先看关键 UML 图
组合模式的类不多,关键是三者的关系。
FileSystemNode 是统一接口。
FileItem 表示普通文件,属于叶子节点。Folder 表示文件夹,内部保存一组 FileSystemNode。
所以文件夹里面放文件、放子文件夹,都行。
先把它们叫成同一种节点
文件和文件夹都要能算大小,也都要能显示名称。
public interface FileSystemNode {
String getName();
int getSize();
void show(String indent);
}
接口先保持简单,只有三个操作。
调用方拿到节点以后,不用先判断这是文件还是文件夹。
这就是组合模式最省心的地方:单个对象和一组对象,可以用同一种方式处理。
普通文件很简单
文件的大小在创建时就确定了,直接返回即可。
public class FileItem implements FileSystemNode {
private final String name;
private final int size;
public FileItem(String name, int size) {
this.name = name;
this.size = size;
}
public String getName() {
return name;
}
public int getSize() {
return size;
}
public void show(String indent) {
System.out.println(indent + "- " + name + " (" + size + "MB)");
}
}
FileItem 不能再添加子节点。走到这里就结束了,所以它叫叶子节点。
文件夹负责装孩子
文件夹自己没有固定大小,它的大小来自里面的内容。
import java.util.ArrayList;
import java.util.List;
public class Folder implements FileSystemNode {
private final String name;
private final List<FileSystemNode> children = new ArrayList<>();
public Folder(String name) {
this.name = name;
}
public void add(FileSystemNode node) {
children.add(node);
}
public String getName() {
return name;
}
}
children 的类型是 FileSystemNode,因此文件和文件夹都能放进去。
有些资料会把 Folder 叫“组合节点”或“容器节点”。名字不同,说的是同一个角色。
大小就让孩子们自己报
文件夹统计大小时,挨个询问子节点,再把结果加起来。
public int getSize() {
int total = 0;
for (FileSystemNode child : children) {
total += child.getSize();
}
return total;
}
如果 child 是文件,它直接返回自己的大小。
如果 child 是文件夹,它会继续询问自己的孩子。这样一层层往下走,就是递归。
别被“递归”两个字吓着。这里可以理解成:我不知道总数,就问我下面的人;下面的人也照着做。
把目录打印出来
打印目录也是同一个思路。
public void show(String indent) {
System.out.println(indent + "+ " + name + " (" + getSize() + "MB)");
for (FileSystemNode child : children) {
child.show(indent + " ");
}
}
每进入一层文件夹,就多两个空格。目录层级看着就清楚了。
Folder project = new Folder("狗哥博客");
Folder images = new Folder("图片");
images.add(new FileItem("封面.png", 3));
images.add(new FileItem("UML图.png", 2));
project.add(images);
project.add(new FileItem("组合模式.md", 1));
project.show("");
运行结果:
+ 狗哥博客 (6MB)
+ 图片 (5MB)
- 封面.png (3MB)
- UML图.png (2MB)
- 组合模式.md (1MB)
代码没有写 if (是文件) 或 if (是文件夹)。
因为每个节点都知道自己该怎么算、该怎么显示。青岛话说,这样写挺恣儿,后面加新目录也不费劲。
啥时候适合用组合模式
看到树形结构时,可以想一下组合模式,但别见到列表就硬套。
文件系统、菜单、部门组织、评论回复,都有明显的上下级关系。
如果数据只是平铺的一排商品,没有“里面还能继续放同类对象”的关系,普通列表更省事。
狗哥收个尾
组合模式解决的是:怎么用同一种方式处理单个对象和一组对象。
FileItem 是叶子,Folder 是容器,它们都实现 FileSystemNode。文件夹只管保存子节点,再把操作一层层交下去。
这篇先记住目录树就够了。下一篇继续聊 装饰器模式,看看不改原类,怎么给一杯咖啡慢慢加上牛奶和糖。