12、组合模式:树形结构、透明式与安全式组合模式
组合模式,说白了就是处理「树形结构」的一把利器。我刚开始接触这个模式时,觉得它不就是个递归嘛,有什么好讲的?直到我在一个权限管理系统中,被菜单树的遍历搞得焦头烂额,才真正体会到组合模式的妙处。
你想想看,现实世界中到处都是树形结构:公司的组织架构、文件系统的目录、GUI 里的容器和控件……这些结构都有一个共同特点——部分和整体需要被一致对待。
什么是组合模式?
组合模式允许你将对象组合成树形结构,来表示「部分-整体」的层次结构。它让客户端可以统一地处理单个对象和组合对象。
嗯,这里要注意一个核心思想:叶子节点和容器节点对外暴露相同的方法。这样调用方就不用关心你操作的是单个文件还是一个文件夹,直接调用同一个方法就行。
- Component(抽象构件):定义叶子节点和容器节点的公共接口
- Leaf(叶子节点):没有子节点的对象
- Composite(容器节点):有子节点的对象,实现子节点管理
一个真实的例子:文件系统
我在做一个文档管理系统时,需要处理文件夹和文件的统一操作。文件夹里可以包含文件,也可以包含子文件夹。这不就是典型的组合模式吗?
先看抽象构件:
public abstract class FileSystemNode {
protected String name;
public FileSystemNode(String name) {
this.name = name;
}
public abstract void display(int depth);
public abstract long getSize();
public abstract void add(FileSystemNode node);
public abstract void remove(FileSystemNode node);
public abstract FileSystemNode getChild(int index);
}
叶子节点——文件:
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) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < depth; i++) {
sb.append("-");
}
System.out.println(sb.toString() + name + " (" + size + " bytes)");
}
@Override
public long getSize() {
return size;
}
// 叶子节点不支持添加/删除子节点
@Override
public void add(FileSystemNode node) {
throw new UnsupportedOperationException("文件不支持添加子节点");
}
@Override
public void remove(FileSystemNode node) {
throw new UnsupportedOperationException("文件不支持删除子节点");
}
@Override
public FileSystemNode getChild(int index) {
throw new UnsupportedOperationException("文件没有子节点");
}
}
容器节点——文件夹:
public class Directory extends FileSystemNode {
private List<FileSystemNode> children = new ArrayList<>();
public Directory(String name) {
super(name);
}
@Override
public void display(int depth) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < depth; i++) {
sb.append("-");
}
System.out.println(sb.toString() + name + "/");
for (FileSystemNode child : children) {
child.display(depth + 2);
}
}
@Override
public long getSize() {
long totalSize = 0;
for (FileSystemNode child : children) {
totalSize += child.getSize();
}
return totalSize;
}
@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);
}
}
客户端调用:
public class Client {
public static void main(String[] args) {
Directory root = new Directory("根目录");
Directory docs = new Directory("文档");
Directory pics = new Directory("图片");
File readme = new File("readme.txt", 1024);
File photo = new File("photo.jpg", 2048000);
File logo = new File("logo.png", 512000);
docs.add(readme);
pics.add(photo);
pics.add(logo);
root.add(docs);
root.add(pics);
root.display(0);
System.out.println("总大小: " + root.getSize() + " bytes");
}
}
透明式 vs 安全式组合模式
这里有个坑,我当年踩过。组合模式有两种实现方式,各有优劣。
透明式组合模式
刚才的代码就是透明式。所有节点(包括叶子节点)都暴露同样的方法。好处是客户端完全不用区分节点类型,统一调用。坏处是叶子节点不得不实现一些无意义的方法,比如 add()、remove(),只能抛异常。
安全式组合模式
安全式把管理子节点的方法放到容器节点中,抽象构件只定义公共方法。
public abstract class FileSystemNode {
protected String name;
public FileSystemNode(String name) {
this.name = name;
}
public abstract void display(int depth);
public abstract long getSize();
}
public class File extends FileSystemNode {
// 只有 display 和 getSize,没有 add/remove
}
public class Directory extends FileSystemNode {
private List<FileSystemNode> children = new ArrayList<>();
public void add(FileSystemNode node) {
children.add(node);
}
public void remove(FileSystemNode node) {
children.remove(node);
}
public FileSystemNode getChild(int index) {
return children.get(index);
}
}
安全式的好处是接口职责清晰,叶子节点不会有不相关的方法。但客户端需要做类型判断:
if (node instanceof Directory) {
((Directory) node).add(new File("test.txt", 100));
}
说实话,我个人更倾向于透明式。虽然叶子节点抛异常有点丑,但客户端代码的简洁性更重要。你想想看,如果树形结构嵌套很深,到处都要做 instanceof 判断,那代码得多难看。
组合模式的核心逻辑
下面这张图展示了组合模式的核心结构:
什么时候用组合模式?
我总结了几条经验:
- 需要表示树形结构:比如菜单、目录、组织架构
- 希望客户端统一处理:不管单个对象还是组合对象,调用方式一样
- 递归操作频繁:比如遍历、统计、搜索
组合模式的优缺点
| 优点 | 缺点 |
|---|---|
| 简化客户端代码,统一处理单个和组合对象 | 透明式设计违反接口隔离原则(叶子节点有不必要的方法) |
| 新增节点类型方便,符合开闭原则 | 树形结构过深时,递归性能可能成为瓶颈 |
| 天然支持递归操作,代码简洁 | 类型安全方面需要额外注意(尤其是安全式) |
说白了,组合模式就是让你用一棵树来组织对象,然后像操作单个对象一样操作整棵树。它不是什么高深的技术,但用好了能让代码结构清晰很多。下次遇到树形结构,不妨试试这个模式。