组合模式:把树形结构玩明白
组合模式,说白了就是处理「部分-整体」关系的一种设计模式。你想想看,在软件开发中,我们经常遇到树形结构——文件夹里有文件,文件夹里还有文件夹;公司有部门,部门下有小组,小组里有员工。这种结构怎么统一处理?组合模式就是干这个的。
我个人习惯把组合模式叫做「树形结构的统一接口」。它让客户端可以用一致的方式对待单个对象和组合对象。嗯,这里要注意,不是所有树形结构都适合用组合模式,后面我会讲到什么时候该用。
定义与核心思想
组合模式的定义其实很简单:将对象组合成树形结构以表示「部分-整体」的层次结构,使得用户对单个对象和组合对象的使用具有一致性。
核心思想就一句话:让叶子节点和容器节点实现相同的接口。这样客户端就不用关心你操作的是单个对象还是组合对象,直接调用统一的方法就行。
关键点:组合模式不是用来「组合」对象的,而是用来「统一」对单个对象和组合对象的操作方式。
结构组成
组合模式的结构其实很清晰,就三个角色:
| 角色 | 名称 | 职责 |
|---|---|---|
| Component | 抽象构件 | 定义叶子节点和容器节点的公共接口,包含业务方法和子节点管理方法 |
| Leaf | 叶子构件 | 没有子节点的对象,实现业务方法 |
| Composite | 容器构件 | 有子节点的对象,实现业务方法,同时管理子节点 |
我记得第一次看这个结构时,觉得挺简单的。但真正用起来,才发现坑不少。咱们先看代码,再聊坑。
两种实现方式
组合模式有两种实现风格:透明式和安全式。这两种方式我都在项目里用过,各有各的适用场景。
透明式组合模式
透明式,就是把所有方法都定义在抽象构件里。叶子节点和容器节点都实现同样的接口。这样做的好处是客户端完全透明,但坏处是叶子节点要处理一些它不需要的方法。
// 抽象构件
abstract class Component {
protected String name;
public Component(String name) {
this.name = name;
}
// 业务方法
public abstract void operation();
// 子节点管理方法
public void add(Component c) {
throw new UnsupportedOperationException("不支持添加操作");
}
public void remove(Component c) {
throw new UnsupportedOperationException("不支持移除操作");
}
public Component getChild(int i) {
throw new UnsupportedOperationException("不支持获取子节点操作");
}
}
// 叶子构件
class Leaf extends Component {
public Leaf(String name) {
super(name);
}
@Override
public void operation() {
System.out.println("叶子节点:" + name + " 执行操作");
}
// add、remove、getChild 继承默认实现,抛出异常
}
// 容器构件
class Composite extends Component {
private List<Component> children = new ArrayList<>();
public Composite(String name) {
super(name);
}
@Override
public void operation() {
System.out.println("容器节点:" + name + " 开始执行操作");
for (Component child : children) {
child.operation();
}
}
@Override
public void add(Component c) {
children.add(c);
}
@Override
public void remove(Component c) {
children.remove(c);
}
@Override
public Component getChild(int i) {
return children.get(i);
}
}
我曾经踩过的坑:透明式看起来优雅,但叶子节点调用 add() 方法时会抛出运行时异常。如果客户端代码没有做好防御,运行时直接崩掉。我有个项目就是因为这个 bug 在线上出了事故,从那以后我对透明式就格外小心。
安全式组合模式
安全式,就是把子节点管理方法从抽象构件中剥离,只在容器节点中定义。这样叶子节点就不会有那些无意义的方法了。
// 抽象构件
abstract class Component {
protected String name;
public Component(String name) {
this.name = name;
}
// 只定义业务方法
public abstract void operation();
}
// 叶子构件
class Leaf extends Component {
public Leaf(String name) {
super(name);
}
@Override
public void operation() {
System.out.println("叶子节点:" + name + " 执行操作");
}
}
// 容器构件
class Composite extends Component {
private List<Component> children = new ArrayList<>();
public Composite(String name) {
super(name);
}
@Override
public void operation() {
System.out.println("容器节点:" + name + " 开始执行操作");
for (Component child : children) {
child.operation();
}
}
// 子节点管理方法只在容器中定义
public void add(Component c) {
children.add(c);
}
public void remove(Component c) {
children.remove(c);
}
public Component getChild(int i) {
return children.get(i);
}
}
我的建议:安全式更符合接口隔离原则。如果你能确保客户端代码知道自己在操作什么类型,安全式是更好的选择。我个人在大多数项目中都倾向安全式。
核心逻辑图
下面这张图展示了组合模式的核心结构和工作流程:
优缺点分析
任何模式都有两面性,组合模式也不例外。咱们客观地看看:
| 优点 | 缺点 |
|---|---|
|
|
我的经验:组合模式最大的价值在于「统一性」。如果你发现代码里到处都是 if-else 判断「是叶子还是容器」,那就该考虑组合模式了。但如果你只有两层结构,用组合模式反而有点杀鸡用牛刀。
应用场景
组合模式在现实项目中的应用其实很广泛。我总结了几类典型场景:
- 文件系统:文件和文件夹的统一操作。这是最经典的例子,每个操作系统都在用。
- UI 组件树:窗口、面板、按钮的嵌套组合。Java Swing 和 Android 的 View 体系都是典型。
- 组织结构管理:公司、部门、小组、员工的层级管理。
- XML/JSON 解析:节点和子节点的统一处理。
- 菜单系统:菜单项和子菜单的嵌套结构。
我记得在做一个企业级报表系统时,就用组合模式来处理报表模板的嵌套结构。一个报表模板可以包含多个子模板,子模板又可以继续嵌套。用组合模式后,生成报表的代码从原来的两百行缩减到四十行,而且扩展性极好。
避坑指南:我曾经在一个项目中过度使用组合模式,把只有两层的数据结构也套上了组合模式。结果代码变得很绕,维护起来特别痛苦。后来我总结了一个原则:当树形结构深度超过3层,或者未来可能扩展时,才考虑组合模式。
总结
组合模式的核心价值在于「统一接口,递归处理」。它让树形结构的操作变得优雅而简洁。但记住,不是所有层级结构都需要组合模式。你想想看,如果你的业务逻辑中,叶子节点和容器节点的行为差异很大,那强行用组合模式反而会适得其反。
选择透明式还是安全式?我的建议是:优先考虑安全式。除非你的客户端代码完全不需要关心类型,而且你能确保所有调用都不会触发异常,否则安全式更稳妥。
好了,组合模式就聊到这里。记住一句话:组合模式不是用来组合对象的,而是用来统一操作方式的。理解了这个,你就真正掌握了组合模式的精髓。
公众号:蓝海资料掘金营,微信deep3321