组合模式:树形结构、透明式与安全式、实战:菜单系统
组合模式,说白了就是处理「部分-整体」关系的一种设计模式。你想想看,树形结构在代码里太常见了——文件夹套文件、菜单套子菜单、组织架构套部门。如果每次都要区分「叶子节点」和「容器节点」,代码会变得又臭又长。
组合模式的核心思想很简单:让客户端统一对待单个对象和组合对象。嗯,这里要注意,不是所有树形结构都适合用组合模式,得看你的业务场景。
组合模式的结构
组合模式有三个核心角色:
- Component(抽象构件):定义叶子节点和容器节点的公共接口
- Leaf(叶子节点):没有子节点的具体构件
- Composite(容器节点):有子节点的具体构件,负责管理子节点
我在项目中遇到过不少团队把组合模式用歪了——要么把所有方法都塞进抽象类,导致叶子节点不得不实现一堆空方法;要么把管理子节点的方法只放在容器节点里,客户端调用时还得做类型判断。这两种做法,其实对应了组合模式的两种变体:透明式和安全式。
透明式 vs 安全式
| 对比维度 | 透明式 | 安全式 |
|---|---|---|
| 接口定义 | 所有方法都在抽象构件中定义 | 管理子节点的方法只在容器节点中定义 |
| 叶子节点 | 需要实现空方法或抛异常 | 不需要实现管理方法 |
| 客户端调用 | 无需类型判断,统一调用 | 需要判断是叶子还是容器 |
| 安全性 | 运行时可能抛异常 | 编译期就能发现问题 |
| 我个人习惯 | 适合简单场景 | 适合复杂业务 |
为什么会有这两种变体?说白了就是「接口统一性」和「类型安全性」之间的权衡。我建议你根据实际场景来选择——如果树形结构比较简单,叶子节点不会误调用管理方法,透明式更简洁;如果业务复杂,安全式能帮你提前发现错误。
实战:菜单系统
我们拿一个菜单系统来实战。菜单项可以是叶子节点(比如一个具体的菜品),也可以是容器节点(比如一个分类,下面还有子分类和菜品)。
先看抽象构件:
public abstract class MenuComponent {
protected String name;
protected String description;
public MenuComponent(String name, String description) {
this.name = name;
this.description = description;
}
// 公共方法
public String getName() { return name; }
public String getDescription() { return description; }
// 管理子节点的方法——透明式风格
public void add(MenuComponent component) {
throw new UnsupportedOperationException("叶子节点不支持添加操作");
}
public void remove(MenuComponent component) {
throw new UnsupportedOperationException("叶子节点不支持删除操作");
}
public MenuComponent getChild(int i) {
throw new UnsupportedOperationException("叶子节点不支持获取子节点");
}
// 核心业务方法
public abstract void print();
// 获取价格——叶子节点实现,容器节点可以返回子节点价格总和
public double getPrice() {
throw new UnsupportedOperationException("容器节点请重写此方法");
}
}
嗯,这里要注意,我用了透明式的写法——所有管理方法都在抽象类中定义了默认实现,叶子节点不需要关心这些方法,但如果误调用就会抛异常。这种做法在简单场景下很实用。
接下来是叶子节点——具体的菜品:
public class MenuItem extends MenuComponent {
private double price;
private boolean isVegetarian;
public MenuItem(String name, String description,
double price, boolean isVegetarian) {
super(name, description);
this.price = price;
this.isVegetarian = isVegetarian;
}
@Override
public double getPrice() {
return price;
}
@Override
public void print() {
System.out.print(" " + getName());
if (isVegetarian) {
System.out.print("(素)");
}
System.out.println(", " + getPrice() + "元");
System.out.println(" -- " + getDescription());
}
}
然后是容器节点——菜单分类:
import java.util.ArrayList;
import java.util.List;
public class Menu extends MenuComponent {
private List<MenuComponent> menuComponents = new ArrayList<>();
public Menu(String name, String description) {
super(name, description);
}
@Override
public void add(MenuComponent component) {
menuComponents.add(component);
}
@Override
public void remove(MenuComponent component) {
menuComponents.remove(component);
}
@Override
public MenuComponent getChild(int i) {
return menuComponents.get(i);
}
@Override
public double getPrice() {
// 计算所有子节点的价格总和
return menuComponents.stream()
.mapToDouble(MenuComponent::getPrice)
.sum();
}
@Override
public void print() {
System.out.println("\n" + getName());
System.out.println(", " + getDescription());
System.out.println("---------------------");
for (MenuComponent component : menuComponents) {
component.print();
}
}
}
你看,容器节点重写了 add、remove、getChild 方法,而叶子节点不需要关心这些。客户端调用时,完全不需要知道当前操作的是叶子还是容器:
public class MenuClient {
public static void main(String[] args) {
MenuComponent allMenus = new Menu("全部菜单", "所有菜品");
MenuComponent lunchMenu = new Menu("午餐菜单", "午餐供应");
MenuComponent dinnerMenu = new Menu("晚餐菜单", "晚餐供应");
lunchMenu.add(new MenuItem("宫保鸡丁", "经典川菜", 38.0, false));
lunchMenu.add(new MenuItem("番茄炒蛋", "家常美味", 22.0, true));
dinnerMenu.add(new MenuItem("水煮鱼", "麻辣鲜香", 68.0, false));
dinnerMenu.add(new MenuItem("清炒时蔬", "清淡健康", 28.0, true));
allMenus.add(lunchMenu);
allMenus.add(dinnerMenu);
// 统一调用,无需类型判断
allMenus.print();
System.out.println("\n总价: " + allMenus.getPrice() + "元");
}
}
输出结果:
全部菜单
, 所有菜品
---------------------
午餐菜单
, 午餐供应
---------------------
宫保鸡丁, 38.0元
-- 经典川菜
番茄炒蛋, 22.0元
-- 家常美味
晚餐菜单
, 晚餐供应
---------------------
水煮鱼, 68.0元
-- 麻辣鲜香
清炒时蔬, 28.0元
-- 清淡健康
总价: 156.0元
核心要点:组合模式让客户端可以用一致的方式处理单个对象和组合对象。你想想看,如果没有组合模式,我们得写一堆 if-else 来判断当前节点是叶子还是容器,代码会变得非常脆弱。
组合模式的核心逻辑
下面这张图展示了组合模式在菜单系统中的核心逻辑:
避坑指南:我曾经在一个电商项目中,把商品分类和商品本身都塞进了同一个组合树里。结果发现,商品分类有几千个,商品有几十万个,每次遍历整棵树都慢得不行。后来才意识到,组合模式适合层次浅、节点少的场景。如果树很深或者节点很多,建议用迭代器模式或者直接上数据库递归查询。
透明式 vs 安全式——怎么选?
我个人的经验是:
- 选透明式:如果树形结构比较简单,叶子节点和容器节点的行为差异不大,而且你希望客户端代码尽量简洁。比如上面的菜单系统,叶子节点不会误调用 add 方法,透明式完全够用。
- 选安全式:如果业务复杂,叶子节点和容器节点的接口差异很大,或者团队里有新人容易误操作。安全式把管理方法只放在容器节点中,客户端调用时必须先判断类型,虽然代码多几行,但更安全。
你想想看,透明式就像把所有工具都放在一个工具箱里,新手可能拿错;安全式就像把不同工具分开放,虽然拿的时候多走两步,但不会拿错。嗯,没有绝对的好坏,看你的团队和场景。
注意事项:组合模式不是万能的。如果你的树形结构经常变化(比如频繁增删节点),或者需要复杂的遍历逻辑(比如按条件过滤),组合模式会让代码变得难以维护。这时候,我建议你考虑访问者模式或者迭代器模式。
组合模式的优缺点
| 优点 | 缺点 |
|---|---|
| 客户端代码简洁,统一处理叶子节点和容器节点 | 可能违反接口隔离原则(叶子节点需要实现不需要的方法) |
| 新增节点类型方便,符合开闭原则 | 树形结构复杂时,性能可能成为瓶颈 |
| 天然适合树形结构的业务场景 | 不适合层次过深或节点过多的场景 |
组合模式,说白了就是「把树形结构藏起来,让客户端只看到统一的接口」。我在多个项目中都用过它——菜单系统、文件系统、组织架构、权限树……每次用之前我都会问自己一个问题:客户端真的需要区分叶子节点和容器节点吗?如果答案是「不需要」,那组合模式就是你的菜。
公众号:蓝海资料掘金营,微信deep3321