组合模式:公司组织架构树——把对象揉成树,管起来就简单了
说实话,组合模式是我在项目中用得最顺手的一个结构型模式。它解决了一个很朴素的问题:如何用一致的方式处理单个对象和对象组合。
你想想看,公司里一个部门经理和普通员工,操作上有什么不同?经理下面可以挂人,员工不行。但如果你要统计所有人的工资,或者发全员邮件,你希望遍历的时候还要区分“这是叶子节点还是树枝节点”吗?
嗯,组合模式就是干这个的。
什么是组合模式?
组合模式允许你将对象组合成树形结构,来表示“部分-整体”的层次结构。它让客户端可以统一对待单个对象和组合对象。
说白了,就是不管你是文件夹还是文件,我都能对你执行“双击打开”这个操作。文件夹打开后显示内容,文件打开后启动编辑器——但调用者不需要关心这些细节。
核心思想:把“部分”和“整体”抽象成同一个接口,让客户端无差别调用。
业务场景:公司组织架构树
我在做一家中型企业的HR系统时,遇到了一个典型场景:
- 公司有多个部门,部门下有子部门
- 每个部门有部门经理和普通员工
- 需要统计整个公司的总人数、总薪资
- 需要支持“一键发送通知”到所有人
如果不用组合模式,你可能会写出这样的代码:
// 糟糕的设计——到处是if-else
void sendNotification(Object obj) {
if (obj instanceof Department) {
Department dept = (Department) obj;
for (Employee emp : dept.getEmployees()) {
emp.sendEmail();
}
for (Department sub : dept.getSubDepartments()) {
sendNotification(sub); // 递归
}
} else if (obj instanceof Employee) {
Employee emp = (Employee) obj;
emp.sendEmail();
}
}
这种代码我见过太多次了。每次新增一种节点类型,就要改这个递归方法。维护起来简直想骂人。
组合模式的核心结构
组合模式有三个角色:
| 角色 | 说明 | 公司例子 |
|---|---|---|
| Component(抽象构件) | 定义叶子节点和组合节点的公共接口 | OrgUnit(组织单元) |
| Leaf(叶子节点) | 没有子节点的对象 | Employee(员工) |
| Composite(组合节点) | 有子节点的对象,实现子节点管理方法 | Department(部门) |
我的习惯:在抽象构件中,我会把 add/remove 方法写成抛出 UnsupportedOperationException 的默认实现。这样叶子节点就不用被迫实现这些方法了。虽然违反了接口隔离原则,但实际用起来很顺手。
代码实现:公司组织架构
先定义抽象构件:
// 抽象构件
public abstract class OrgUnit {
protected String name;
public OrgUnit(String name) {
this.name = name;
}
// 公共方法
public abstract int getPersonCount();
public abstract double getTotalSalary();
public abstract void sendNotification(String message);
// 子节点管理——默认不支持
public void add(OrgUnit unit) {
throw new UnsupportedOperationException("不支持添加子节点");
}
public void remove(OrgUnit unit) {
throw new UnsupportedOperationException("不支持移除子节点");
}
}
叶子节点——员工:
// 叶子节点
public class Employee extends OrgUnit {
private double salary;
public Employee(String name, double salary) {
super(name);
this.salary = salary;
}
@Override
public int getPersonCount() {
return 1; // 一个人
}
@Override
public double getTotalSalary() {
return salary;
}
@Override
public void sendNotification(String message) {
System.out.println("发送给员工 " + name + ":" + message);
}
}
组合节点——部门:
// 组合节点
public class Department extends OrgUnit {
private List<OrgUnit> children = new ArrayList<>();
public Department(String name) {
super(name);
}
@Override
public void add(OrgUnit unit) {
children.add(unit);
}
@Override
public void remove(OrgUnit unit) {
children.remove(unit);
}
@Override
public int getPersonCount() {
int count = 0;
for (OrgUnit unit : children) {
count += unit.getPersonCount();
}
return count;
}
@Override
public double getTotalSalary() {
double total = 0;
for (OrgUnit unit : children) {
total += unit.getTotalSalary();
}
return total;
}
@Override
public void sendNotification(String message) {
System.out.println("通知部门 " + name + ":");
for (OrgUnit unit : children) {
unit.sendNotification(message);
}
}
}
客户端使用:
// 构建组织架构树
Department company = new Department("总公司");
Department techDept = new Department("技术部");
techDept.add(new Employee("张三", 20000));
techDept.add(new Employee("李四", 18000));
Department subDept = new Department("前端组");
subDept.add(new Employee("王五", 15000));
subDept.add(new Employee("赵六", 14000));
techDept.add(subDept);
Department hrDept = new Department("人事部");
hrDept.add(new Employee("钱七", 12000));
company.add(techDept);
company.add(hrDept);
// 统一调用——完全不用关心是部门还是员工
System.out.println("公司总人数:" + company.getPersonCount());
System.out.println("公司总薪资:" + company.getTotalSalary());
company.sendNotification("下午3点开全员大会");
输出结果:
公司总人数:5
公司总薪资:79000.0
通知部门 总公司:
通知部门 技术部:
发送给员工 张三:下午3点开全员大会
发送给员工 李四:下午3点开全员大会
通知部门 前端组:
发送给员工 王五:下午3点开全员大会
发送给员工 赵六:下午3点开全员大会
通知部门 人事部:
发送给员工 钱七:下午3点开全员大会
组合模式的结构图
下面这张图展示了组合模式的核心结构,以及它如何映射到公司组织架构:
避坑指南
我曾经踩过的一个坑:在组合节点里直接暴露了 children 列表的 getter 方法。结果外部代码直接修改了列表,导致树结构被破坏。
正确做法:永远通过 add/remove 方法来操作子节点。如果一定要遍历,返回一个不可修改的副本:
public List<OrgUnit> getChildren() {
return Collections.unmodifiableList(children);
}
另外,组合模式有一个经典问题:透明性 vs 安全性。
- 透明方式:在抽象构件中声明所有方法(包括 add/remove),叶子节点继承但抛出异常。客户端可以统一调用,但运行时可能出错。
- 安全方式:只在组合节点中声明 add/remove,客户端需要先判断类型。安全但不够优雅。
我个人习惯用透明方式。因为组合模式的核心价值就是“统一对待”,如果还要客户端去判断类型,那还不如不用这个模式。运行时异常虽然不好看,但比到处写 instanceof 强多了。
文件系统的类比
其实你每天都在用组合模式——文件系统。文件夹和文件都支持“双击打开”、“重命名”、“删除”这些操作。文件夹打开后显示内容,文件打开后启动编辑器。这就是组合模式的经典应用。
Linux 的 VFS(虚拟文件系统)也是这个思路。不管是 ext4、NTFS 还是 NFS,都统一成文件、目录、链接这些抽象。上层应用根本不用关心底层是什么文件系统。
我的建议:当你发现代码里到处都是 if-else 来判断“是叶子还是树枝”时,就该考虑组合模式了。它特别适合树形结构的场景,比如组织架构、菜单系统、UI组件树、XML/JSON解析等。
组合模式的优缺点
| 优点 | 缺点 |
|---|---|
| 客户端代码简单——统一对待所有节点 | 可能违反接口隔离原则(叶子节点继承不需要的方法) |
| 容易新增节点类型——开闭原则 | 树结构可能过于通用,难以限制 |
| 天然支持递归遍历 | 调试时递归调用栈可能很深 |
嗯,组合模式就是这么回事。它不复杂,但用好了能让代码结构清晰很多。下次你遇到树形结构的数据,不妨试试这个模式。