如何手写一个组合模式?Java 代码怎么实现?
简化版
手写组合模式可以先定义统一接口 Component,再实现叶子类 File 和组合类 Directory。组合类内部维护 List<Component>,对外实现同样的方法,内部递归调用子节点。
详细版
以文件系统为例:
interface FileNode {
long size();
void print(String indent);
}
文件是叶子:
class FileLeaf implements FileNode {
public long size() { return size; }
}
目录是组合:
class Directory implements FileNode {
private List<FileNode> children = new ArrayList<>();
public long size() {
return children.stream().mapToLong(FileNode::size).sum();
}
}
客户端调用:
root.size();
root.print("");
不需要关心 root 下面有多少层目录和文件。
完整版教学
一、定义公共接口
interface FileNode {
String name();
long size();
void print(String indent);
}
这里定义的是文件和目录都具备的能力。
文件有名称和大小,目录也有名称和总大小。
二、实现叶子节点 File
class FileLeaf implements FileNode {
private final String name;
private final long size;
FileLeaf(String name, long size) {
this.name = name;
this.size = size;
}
public String name() {
return name;
}
public long size() {
return size;
}
public void print(String indent) {
System.out.println(indent + "- " + name + " (" + size + ")");
}
}
叶子节点没有子节点,所以 size() 直接返回自身大小。
三、实现组合节点 Directory
class Directory implements FileNode {
private final String name;
private final List<FileNode> children = new ArrayList<>();
Directory(String name) {
this.name = name;
}
void add(FileNode node) {
children.add(node);
}
public String name() {
return name;
}
public long size() {
long total = 0;
for (FileNode child : children) {
total += child.size();
}
return total;
}
public void print(String indent) {
System.out.println(indent + "+ " + name);
for (FileNode child : children) {
child.print(indent + " ");
}
}
}
目录的大小来自子节点大小之和。
如果子节点也是目录,它会继续递归计算。
四、客户端统一使用
Directory root = new Directory("root");
root.add(new FileLeaf("a.txt", 10));
Directory images = new Directory("images");
images.add(new FileLeaf("logo.png", 200));
root.add(images);
root.print("");
System.out.println(root.size());
客户端统一操作 FileNode,不需要写一堆类型判断。
五、工程里要注意访问控制
上面为了演示简单,把 add() 放在 Directory 上。
真实项目中,要考虑:
- 是否允许外部随便添加子节点;
- 是否需要删除子节点;
- 是否需要父节点引用;
- 是否防止形成环;
- 是否需要缓存计算结果;
- 是否线程安全。
组合模式只是结构,树的维护规则仍然要设计清楚。
六、常见误区与追问
实现验收要同时检查统一操作和结构约束。构造1个目录并加入2个文件后,调用 size() 应递归得到两者之和;尝试把目录自身加入子节点必须被拒绝,否则遍历会无限循环。子节点集合还应封装为只读视图,避免客户端绕过校验直接修改。
| 检查维度 | 判定依据 |
|---|---|
| Component | 声明叶子与组合共有操作 |
| Composite | 维护子节点并递归聚合结果 |
易错点:统一业务操作不代表必须把 add/remove 也塞进所有叶子。
- 误区:Component 必须是抽象类。 接口或抽象类都可以,取决于是否需要共享状态和默认实现。
- 追问:子节点集合为什么不能直接暴露? 外部修改会绕过环检测、父指针和权限等结构不变量。
- 误区:递归方法只要能跑就正确。 还要处理环、深度、异常节点和大树性能。
- 追问:父指针是否必须? 不是;需要向上导航或快速移除时才增加,同时要维护双向一致性。
- 追问:如何测试新增节点类型? 把它作为 Component 放进现有树,验证客户端和 Composite 无需类型分支。
七、加强记忆
手写组合模式抓住三个点:公共接口表示统一能力,叶子节点直接执行,组合节点保存 List<Component> 并递归调用子节点。文件系统是最经典例子:文件是叶子,目录是组合。