跳转到内容

组合 (Composite)

Ken 码农
  • 设计模式
  • 结构型模式

组合 (Composite) 是一种结构型设计模式,用来把对象组织成树形结构,让客户端可以用同一套接口去操作“单个对象”和“由很多对象组成的整体”。

直观一点的理解就是:不管是叶子节点,还是一整棵子树,在客户端眼里都是同一种“组件”

  • 业务里存在典型的“部分-整体”层次结构:比如学院下面有专业,专业下面有班级,班级下面有学生。
  • 如果客户端每次都要判断“这是学院还是班级还是学生”,写一堆 if/else 来分别处理,代码会越来越乱、越来越难扩展。
  • 希望可以用统一的抽象来描述“任何层级的节点”,让客户端直接对这类抽象进行遍历、统计等操作,而不用在意这是叶子还是中间节点。
  • 抽象出一个统一的组件 (Component):描述“树里的一个节点”,无论是叶子还是中间节点,都要实现它。
  • 叶子节点 (Leaf):没有子节点,只关注自身的业务语义,比如“某个学生”“某个课程”。
  • 组合节点 (Composite):内部维护子组件列表,实现业务时通常是“遍历子组件并委托给它们”,同时提供 add / remove 等管理子节点的方法。
  • 客户端只需要面向 Component 编程,可以对整棵树做统一处理,而不必关心当前拿到的是哪一层。
classDiagram
    class Component {
        <<abstract>>
        +operation()* void
        +add(Component c) void
        +remove(Component c) void
    }
    class Leaf {
        +operation() void
    }
    class Composite {
        -children List~Component~
        +operation() void
        +add(Component c) void
        +remove(Component c) void
    }
    Component <|-- Leaf
    Component <|-- Composite
    Composite o-- Component : children

示例:学校组织架构的组合模型

Section titled “示例:学校组织架构的组合模型”

在一个高校信息系统里,经常会有类似这样的层级:

  • 学校
    • 学院
      • 专业
        • 班级
          • 学生

很多后台服务都需要在这棵树上做操作,比如:

  • 统计某个学院下所有学生数量。
  • 列出某个专业下所有班级和学生。
  • 统一打印整个组织架构树。

如果在客户端层面写一堆“如果是学院就遍历专业,如果是班级就遍历学生”,会非常杂乱。用组合模式可以把这些节点统一抽象为 OrgComponent


/**
* 组件抽象:组织树中的一个节点
* 可以是学校、学院、专业、班级、学生等
*/
public abstract class OrgComponent {
protected String name;
public OrgComponent(String name) {
this.name = name;
}
/**
* 展示当前节点信息(以及可能的子节点)
*/
public abstract void print(int level);
/**
* 统计当前节点下的学生数量
*/
public abstract int countStudents();
/**
* 组合节点会重写,叶子节点默认不支持
*/
public void add(OrgComponent child) {
throw new UnsupportedOperationException();
}
public void remove(OrgComponent child) {
throw new UnsupportedOperationException();
}
}

/**
* 叶子节点:学生
*/
public class StudentLeaf extends OrgComponent {
private final String studentId;
public StudentLeaf(String name, String studentId) {
super(name);
this.studentId = studentId;
}
@Override
public void print(int level) {
String indent = " ".repeat(level);
System.out.println(indent + "学生: " + name + "(学号 " + studentId + "");
}
@Override
public int countStudents() {
return 1;
}
}

3. 组合节点:学院 / 专业 / 班级

Section titled “3. 组合节点:学院 / 专业 / 班级”
import java.util.ArrayList;
import java.util.List;
/**
* 组合节点:可以包含子组织或学生
*/
public class OrgComposite extends OrgComponent {
private final List<OrgComponent> children = new ArrayList<>();
private final String type; // 学校 / 学院 / 专业 / 班级
public OrgComposite(String name, String type) {
super(name);
this.type = type;
}
@Override
public void print(int level) {
String indent = " ".repeat(level);
System.out.println(indent + type + ": " + name);
for (OrgComponent child : children) {
child.print(level + 1);
}
}
@Override
public int countStudents() {
int total = 0;
for (OrgComponent child : children) {
total += child.countStudents();
}
return total;
}
@Override
public void add(OrgComponent child) {
children.add(child);
}
@Override
public void remove(OrgComponent child) {
children.remove(child);
}
}

4. 客户端:统一处理整棵组织树

Section titled “4. 客户端:统一处理整棵组织树”
/**
* 客户端:只依赖 OrgComponent 抽象
*/
public class OrgClient {
public static void main(String[] args) {
// 学校
OrgComposite university = new OrgComposite("示例大学", "学校");
// 学院
OrgComposite csCollege = new OrgComposite("计算机学院", "学院");
OrgComposite mathCollege = new OrgComposite("数学学院", "学院");
// 专业
OrgComposite csMajor = new OrgComposite("软件工程", "专业");
OrgComposite aiMajor = new OrgComposite("人工智能", "专业");
// 班级
OrgComposite seClass1 = new OrgComposite("软工 2301 班", "班级");
// 学生
seClass1.add(new StudentLeaf("张三", "20230001"));
seClass1.add(new StudentLeaf("李四", "20230002"));
csMajor.add(seClass1);
csCollege.add(csMajor);
csCollege.add(aiMajor);
university.add(csCollege);
university.add(mathCollege);
// 统一打印组织树
university.print(0);
// 统计整个学校的学生数量
int totalStudents = university.countStudents();
System.out.println("全校学生总数: " + totalStudents);
}
}

在这个示例中:

  • OrgComponent 抽象了树中的节点,不关心具体层级。
  • OrgComposite 代表学校、学院、专业、班级等可以继续往下挂子节点的组织单元。
  • StudentLeaf 是叶子节点,表示具体学生。
  • 客户端只依赖 OrgComponent,就可以统一地打印整棵树、统计学生数量,而不用写很多层级判断。

  • 学校组织结构、课程目录、权限菜单等天然是树形结构的场景。
  • 需要对“整体”和“部分”做统一操作,例如批量授权、批量统计、统一导出等。
  • 希望避免在客户端代码里出现大量“如果是叶子就这样,如果是组合就那样”的分支判断。

优点

  • 统一抽象:客户端只依赖一个组件抽象即可处理单个对象和组合对象。
  • 易于扩展:新增一种叶子或组合节点类型,只需实现组件抽象接口,不影响已有客户端逻辑。
  • 天然适配树结构:对于学校这种多层级组织、课程分类等树形数据,模型自然、表达清晰。

注意事项

  • 组合根节点的职责要控制好,避免把过多业务逻辑都塞进组合节点导致膨胀。
  • 不是所有结构都要强行抽象成组合模式,简单结构直接用列表也许更直观。

  • 组合模式解决的是“部分-整体”场景下,如何用统一方式处理叶子和组合节点的问题。
  • 通过统一的 Component 抽象和树形结构,客户端可以在不知道具体层级的情况下,对整棵树执行遍历、统计等操作。
  • 在学校的组织架构、课程目录、权限菜单等场景中,组合模式都非常适合用来建模和实现后台服务逻辑。