组合模式:公司组织架构树——把对象揉成树,管起来就简单了

说实话,组合模式是我在项目中用得最顺手的一个结构型模式。它解决了一个很朴素的问题:如何用一致的方式处理单个对象和对象组合

你想想看,公司里一个部门经理和普通员工,操作上有什么不同?经理下面可以挂人,员工不行。但如果你要统计所有人的工资,或者发全员邮件,你希望遍历的时候还要区分“这是叶子节点还是树枝节点”吗?

嗯,组合模式就是干这个的。

什么是组合模式?

组合模式允许你将对象组合成树形结构,来表示“部分-整体”的层次结构。它让客户端可以统一对待单个对象和组合对象

说白了,就是不管你是文件夹还是文件,我都能对你执行“双击打开”这个操作。文件夹打开后显示内容,文件打开后启动编辑器——但调用者不需要关心这些细节。

核心思想:把“部分”和“整体”抽象成同一个接口,让客户端无差别调用。

业务场景:公司组织架构树

我在做一家中型企业的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点开全员大会

组合模式的结构图

下面这张图展示了组合模式的核心结构,以及它如何映射到公司组织架构:

组合模式结构图 —— 公司组织架构 «abstract» OrgUnit Leaf: Employee - salary: double + getPersonCount(): int Composite: Department - children: List<OrgUnit> + add(OrgUnit): void 包含 实例:公司组织架构树 总公司 技术部 人事部 张三 李四

避坑指南

我曾经踩过的一个坑:在组合节点里直接暴露了 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解析等。

组合模式的优缺点

优点 缺点
客户端代码简单——统一对待所有节点 可能违反接口隔离原则(叶子节点继承不需要的方法)
容易新增节点类型——开闭原则 树结构可能过于通用,难以限制
天然支持递归遍历 调试时递归调用栈可能很深

嗯,组合模式就是这么回事。它不复杂,但用好了能让代码结构清晰很多。下次你遇到树形结构的数据,不妨试试这个模式。

公众号:蓝海资料掘金营,微信deep3321