Appearance
组合模式 (Composite Pattern)
一、定义
一句话概括:将对象组合成树形结构以表示"部分-整体"的层次结构,使客户端对单个对象和组合对象的使用具有一致性。
官方定义(GoF):Compose objects into tree structures to represent part-whole hierarchies. Composite lets clients treat individual objects and compositions of objects uniformly.
组合模式的核心思想是:用统一的方式处理叶子节点(单个对象)和组合节点(容器对象),客户端无需区分它们,可以一致地操作整个树形结构。
二、解决的问题
2.1 什么场景下需要组合模式?
- 树形结构:组织架构(公司→部门→员工)、文件系统(文件夹→子文件夹→文件)、菜单系统(菜单→子菜单→菜单项)。
- 统一操作:需要对整体和部分执行相同操作,如统计大小、遍历、复制、删除等。
- 递归嵌套:结构本身是递归的,一个容器可以包含其他容器或叶子节点。
2.2 不用组合模式会有什么问题?
java
// 假设我们要处理一个文件系统
class File {
String name;
int size;
}
class Folder {
String name;
List<Folder> subFolders; // 只能放文件夹
List<File> files; // 只能放文件
}
// 客户端代码必须区分处理:
void calculateSize(Object obj) {
if (obj instanceof File) {
return ((File) obj).size;
} else if (obj instanceof Folder) {
int total = 0;
for (File f : ((Folder) obj).files) total += f.size;
for (Folder f : ((Folder) obj).subFolders) total += calculateSize(f);
return total;
}
}问题1:客户端必须区分处理叶子和容器,if-else 泛滥
问题2:无法统一递归遍历,每种节点类型需要单独处理
问题3:新增节点类型需要修改所有客户端代码,违反开闭原则
问题4:代码重复,每个操作都需要类型判断三、结构
3.1 文字描述
组合模式包含以下角色:
- Component(抽象构件):定义叶子和容器的公共接口。
- Leaf(叶子节点):树形结构中的末端节点,没有子节点。
- Composite(容器节点):包含子节点(Component 类型),实现子节点的管理方法。
3.2 透明组合模式
┌─────────────────────────────────────┐
│ <<interface>> Component │
├─────────────────────────────────────┤
│ + add(Component) │
│ + remove(Component) │
│ + getChild(int) │
│ + operation() │
└────────────┬────────────────────────┘
│
┌────────┴────────┐
│ │
┌───┴──────────┐ ┌──┴──────────────────┐
│ Leaf │ │ Composite │
├──────────────┤ ├──────────────────────┤
│ + operation()│ │ - children: List<Comp>│
│ │ │ + add(Component) │
│ │ │ + remove(Component) │
│ │ │ + getChild(int) │
│ │ │ + operation() │
│ │ │ for child: op() │
└──────────────┘ └──────────────────────┘3.3 安全组合模式
┌─────────────────────────────────────┐
│ <<interface>> Component │
├─────────────────────────────────────┤
│ + operation() │
└────────────┬────────────────────────┘
│
┌────────┴────────┐
│ │
┌───┴──────────┐ ┌──┴──────────────────┐
│ Leaf │ │ Composite │
├──────────────┤ ├──────────────────────┤
│ + operation()│ │ - children: List<Comp>│
└──────────────┘ │ + add(Component) │
│ + remove(Component) │
│ + getChild(int) │
│ + operation() │
└──────────────────────┘四、代码实现
4.1 基础实现
场景:文件系统
java
// ============ Component:抽象构件 ============
abstract class FileSystemNode {
protected String name;
public FileSystemNode(String name) {
this.name = name;
}
public abstract void display(int depth);
public abstract int getSize();
// 默认实现:叶子节点不支持
public void add(FileSystemNode node) {
throw new UnsupportedOperationException();
}
public void remove(FileSystemNode node) {
throw new UnsupportedOperationException();
}
}
// ============ Leaf:叶子节点(文件)============
class File extends FileSystemNode {
private int size;
public File(String name, int size) {
super(name);
this.size = size;
}
@Override
public void display(int depth) {
System.out.println(" ".repeat(depth) + "📄 " + name + " (" + size + "KB)");
}
@Override
public int getSize() {
return size;
}
}
// ============ Composite:容器节点(文件夹)============
class Folder extends FileSystemNode {
private List<FileSystemNode> children = new ArrayList<>();
public Folder(String name) {
super(name);
}
@Override
public void add(FileSystemNode node) {
children.add(node);
}
@Override
public void remove(FileSystemNode node) {
children.remove(node);
}
@Override
public void display(int depth) {
System.out.println(" ".repeat(depth) + "📁 " + name + "/");
for (FileSystemNode child : children) {
child.display(depth + 1);
}
}
@Override
public int getSize() {
int total = 0;
for (FileSystemNode child : children) {
total += child.getSize();
}
return total;
}
}
// ============ 客户端测试 ============
public class CompositeDemo {
public static void main(String[] args) {
// 构建文件树
Folder root = new Folder("root");
Folder docs = new Folder("docs");
Folder images = new Folder("images");
docs.add(new File("readme.md", 10));
docs.add(new File("design.pdf", 500));
images.add(new File("logo.png", 200));
images.add(new File("banner.jpg", 800));
root.add(docs);
root.add(images);
root.add(new File("config.xml", 5));
// 统一显示
root.display(0);
System.out.println("总大小: " + root.getSize() + "KB");
}
}4.2 进阶实现
4.2.1 透明组合 vs 安全组合
java
// ====== 透明组合:所有方法在 Component 中定义 ======
// 优点:客户端无需区分叶子/容器,完全透明
// 缺点:叶子节点继承了无用的 add/remove 方法,调用可能抛异常
// ====== 安全组合:管理方法只在 Composite 中定义 ======
interface Component {
void operation();
}
class Leaf implements Component {
@Override
public void operation() {
System.out.println("Leaf operation");
}
}
class Composite implements Component {
private List<Component> children = new ArrayList<>();
// 管理方法只在 Composite 中定义(安全)
public void add(Component c) { children.add(c); }
public void remove(Component c) { children.remove(c); }
@Override
public void operation() {
for (Component child : children) {
child.operation();
}
}
}
// 使用安全组合时,客户端需要知道节点类型才能调用 add/remove
Composite composite = (Composite) node; // 需要类型转换
composite.add(new Leaf());4.2.2 递归遍历与迭代器
java
// ============ 深度优先遍历 ============
class FileSystem {
// 深度优先:先遍历子节点
public static void dfs(FileSystemNode node) {
System.out.println(node.name);
if (node instanceof Folder) {
for (FileSystemNode child : ((Folder) node).getChildren()) {
dfs(child);
}
}
}
// 广度优先:使用队列
public static void bfs(FileSystemNode root) {
Queue<FileSystemNode> queue = new LinkedList<>();
queue.offer(root);
while (!queue.isEmpty()) {
FileSystemNode node = queue.poll();
System.out.println(node.name);
if (node instanceof Folder) {
for (FileSystemNode child : ((Folder) node).getChildren()) {
queue.offer(child);
}
}
}
}
// 过滤:查找所有大于 100KB 的文件
public static List<File> findLargeFiles(FileSystemNode node, int threshold) {
List<File> result = new ArrayList<>();
if (node instanceof File && node.getSize() > threshold) {
result.add((File) node);
} else if (node instanceof Folder) {
for (FileSystemNode child : ((Folder) node).getChildren()) {
result.addAll(findLargeFiles(child, threshold));
}
}
return result;
}
}4.2.3 组合模式实现规则引擎
java
// ============ 规则组合 ============
interface Rule {
boolean evaluate(Map<String, Object> context);
}
// 叶子规则:具体条件
class AgeRule implements Rule {
private int minAge;
public AgeRule(int minAge) { this.minAge = minAge; }
@Override
public boolean evaluate(Map<String, Object> context) {
int age = (int) context.getOrDefault("age", 0);
return age >= minAge;
}
}
class VipRule implements Rule {
@Override
public boolean evaluate(Map<String, Object> context) {
return (boolean) context.getOrDefault("isVip", false);
}
}
// 组合规则:And / Or
class AndRule implements Rule {
private List<Rule> rules = new ArrayList<>();
public void add(Rule rule) { rules.add(rule); }
@Override
public boolean evaluate(Map<String, Object> context) {
return rules.stream().allMatch(r -> r.evaluate(context));
}
}
class OrRule implements Rule {
private List<Rule> rules = new ArrayList<>();
public void add(Rule rule) { rules.add(rule); }
@Override
public boolean evaluate(Map<String, Object> context) {
return rules.stream().anyMatch(r -> r.evaluate(context));
}
}
// 测试
// AndRule rule = new AndRule();
// rule.add(new AgeRule(18));
// rule.add(new VipRule());
// rule.evaluate(Map.of("age", 25, "isVip", true)); // true4.3 生产级实现
Spring Boot 动态菜单系统
java
// ============ Component:菜单组件 ============
interface MenuComponent {
Long getId();
String getName();
String getIcon();
String getPath();
List<MenuComponent> getChildren();
boolean hasChildren();
}
// ============ Leaf:菜单项(叶子)============
class MenuItem implements MenuComponent {
private Long id;
private String name;
private String icon;
private String path;
private String permission; // 权限标识
public MenuItem(Long id, String name, String icon, String path, String permission) {
this.id = id;
this.name = name;
this.icon = icon;
this.path = path;
this.permission = permission;
}
@Override public Long getId() { return id; }
@Override public String getName() { return name; }
@Override public String getIcon() { return icon; }
@Override public String getPath() { return path; }
@Override public List<MenuComponent> getChildren() { return Collections.emptyList(); }
@Override public boolean hasChildren() { return false; }
public String getPermission() { return permission; }
}
// ============ Composite:菜单(容器)============
class Menu implements MenuComponent {
private Long id;
private String name;
private String icon;
private String path;
private List<MenuComponent> children = new ArrayList<>();
public Menu(Long id, String name, String icon, String path) {
this.id = id;
this.name = name;
this.icon = icon;
this.path = path;
}
public void addChild(MenuComponent component) {
children.add(component);
}
@Override public Long getId() { return id; }
@Override public String getName() { return name; }
@Override public String getIcon() { return icon; }
@Override public String getPath() { return path; }
@Override public List<MenuComponent> getChildren() { return children; }
@Override public boolean hasChildren() { return !children.isEmpty(); }
}
// ============ 菜单构建服务 ============
@Service
class MenuService {
@Autowired
private MenuRepository menuRepository;
// 构建菜单树
public List<MenuComponent> buildMenuTree(Long parentId) {
List<MenuEntity> entities = menuRepository.findByParentId(parentId);
List<MenuComponent> menus = new ArrayList<>();
for (MenuEntity entity : entities) {
if (entity.getType() == MenuType.CATALOG) {
Menu menu = new Menu(entity.getId(), entity.getName(),
entity.getIcon(), entity.getPath());
// 递归构建子菜单
List<MenuComponent> children = buildMenuTree(entity.getId());
for (MenuComponent child : children) {
menu.addChild(child);
}
menus.add(menu);
} else {
menus.add(new MenuItem(entity.getId(), entity.getName(),
entity.getIcon(), entity.getPath(), entity.getPermission()));
}
}
return menus;
}
// 统一渲染菜单树为前端 JSON
public List<Map<String, Object>> toTreeJson(List<MenuComponent> components) {
List<Map<String, Object>> result = new ArrayList<>();
for (MenuComponent comp : components) {
Map<String, Object> node = new LinkedHashMap<>();
node.put("id", comp.getId());
node.put("name", comp.getName());
node.put("icon", comp.getIcon());
node.put("path", comp.getPath());
if (comp.hasChildren()) {
node.put("children", toTreeJson(comp.getChildren()));
}
result.add(node);
}
return result;
}
}五、优缺点
优点
| 优点 | 说明 |
|---|---|
| 统一处理 | 客户端无需区分叶子和容器,简化代码 |
| 易于扩展 | 新增节点类型只需实现 Component 接口 |
| 递归结构 | 天然支持递归操作,如遍历、统计、过滤 |
| 符合开闭原则 | 新增组件类型不修改现有代码 |
缺点
| 缺点 | 说明 |
|---|---|
| 类型限制困难 | 难以限制容器中只能放特定类型的子节点,需要运行时检查 |
| 设计复杂 | 需要提取公共接口,如果叶子和容器差异很大,抽象困难 |
| 透明性代价 | 透明组合中,叶子节点继承了无用方法,调用时可能抛异常 |
六、适用场景
- 文件系统:文件夹和文件的树形结构。
- 组织架构:公司→部门→小组→员工。
- 菜单系统:后台管理系统的多级菜单。
- UI 组件:容器(Panel/Window)包含子组件(Button/Text)。
- 规则引擎:简单条件和复合条件(AND/OR)的组合。
- XML/HTML 解析:DOM 树中,元素节点和文本节点。
- 分类系统:商品分类的多级目录。
七、JDK / Spring 框架中的实际应用
7.1 JDK 中的组合模式
java
// 1. java.awt.Container 和 java.awt.Component
// Container 继承 Component,可以包含其他 Component
// Button、TextField 是叶子,Panel、Frame 是容器
// 2. java.util.Map 和 java.util.HashMap
// Map.putAll(Map) —— 将另一个 Map 的所有元素加入
// Map 本身可以嵌套 Map
// 3. javax.swing.JComponent 体系
// JPanel 是容器,JButton、JLabel 是叶子7.2 Spring 中的组合模式
java
// 1. Spring Security 的 GrantedAuthority
// 权限可以嵌套(角色包含多个权限)
// 2. Spring Expression Language (SpEL)
// 表达式可以是简单表达式,也可以是复合表达式(AND/OR/NOT)
// 3. Spring Cloud Gateway 的 Route Predicate
// Route 可以包含多个 Predicate(AND 组合)八、与其他模式的关系
| 模式 | 关系 |
|---|---|
| 装饰器模式 | 装饰器和组合模式结构相似,但意图不同:装饰器为对象添加职责,组合模式统一处理整体和部分。装饰器只有一个子节点,组合可以有多个。 |
| 享元模式 | 组合模式中的叶子节点可以使用享元模式共享,减少内存占用。 |
| 迭代器模式 | 常与组合模式配合,遍历树形结构。 |
| 访问者模式 | 访问者模式可以对组合模式中的不同节点执行不同操作,解决组合模式中操作分发困难的问题。 |
| 责任链模式 | 组合模式中,请求可以从子节点向上传递到父节点。 |
九、面试常见问题
Q1:透明组合模式和安全组合模式有什么区别?各有什么优缺点?
答:
- 透明组合:管理子节点的方法(add/remove)定义在
Component接口中。优点是客户端无需区分叶子和容器,完全透明;缺点是叶子节点继承了无用方法,调用时可能抛出UnsupportedOperationException。 - 安全组合:管理子节点的方法只定义在
Composite类中。优点是安全,叶子节点不会有无用方法;缺点是客户端需要知道具体类型才能调用 add/remove,失去了透明性。
Q2:组合模式中如何处理叶子节点的特殊操作?
答:有三种方式:
- 在
Component接口中定义所有方法,叶子节点抛出UnsupportedOperationException(透明组合)。 - 只在具体类中定义特殊方法,客户端通过
instanceof判断后调用(安全组合)。 - 使用访问者模式,让访问者根据不同节点类型执行不同操作。
Q3:组合模式和装饰器模式有什么区别?
答:
- 组合模式关注"整体-部分"关系,一个容器可以有多个子节点,形成树形结构。
- 装饰器模式关注"功能增强",一个装饰器只包装一个对象,形成链式结构。
- 组合模式中,客户端统一对待叶子和容器;装饰器模式中,客户端通常不关心装饰链。
Q4:如何在组合模式中实现缓存优化?
答:在 Composite 节点中缓存计算结果,当子节点变化时失效缓存。例如,文件夹缓存总大小,当子文件/文件夹变化时重新计算。这可以避免每次遍历整棵树带来的性能开销。
Q5:组合模式中如何限制容器只能放特定类型的子节点?
答:在 add 方法中添加类型检查,或在子类中覆盖 add 方法限制类型。也可以使用泛型 <T extends Component> 来限制。但完全限制类型通常需要放弃透明性,因为 Component 接口无法知道子类类型。
