欢迎你来读这篇博客,这篇博客主要是关于组合模式。
其中包括组合模式的核心思想、适用场景、透明组合模式、安全组合模式、优缺点、与装饰器模式和桥接模式的区别,以及 Java 后端开发中菜单权限树的完整案例。
序言
在 Java 后端开发中,我们经常会遇到树形结构。
例如:
- 系统菜单树;
- 权限资源树;
- 部门组织架构;
- 商品分类树;
- 文件目录树;
- 评论回复树;
- 地区行政区划;
- 知识库目录;
- 工作流节点树。
这些结构都有一个共同特点:
整体和部分具有相似的结构。
比如文件系统中:
1 2 3 4 5 6
| 目录 ├── 文件 ├── 文件 └── 子目录 ├── 文件 └── 子目录
|
目录下面可以有文件,也可以有子目录。
文件是叶子节点,目录是组合节点。
但是在很多操作上,我们希望它们能被统一处理。
比如:
- 计算大小;
- 打印结构;
- 判断权限;
- 渲染菜单;
- 递归遍历;
- 批量启用或禁用。
如果每次都写一堆 if else 判断当前节点到底是文件还是目录,代码会越来越乱。
组合模式就是为了解决这种问题:
将对象组合成树形结构,让客户端可以用统一的方式处理单个对象和组合对象。
简单说:
组合模式让“一个对象”和“一组对象”看起来像同一种对象。
这句话很关键。
它不是为了炫技,而是为了让树形结构的处理逻辑变得自然。
正文
chapter 1:什么是组合模式
组合模式,英文是 Composite Pattern,属于结构型设计模式。
它的定义是:
将对象组合成树形结构以表示“部分-整体”的层次结构,使客户端可以统一处理单个对象和组合对象。
组合模式一般包含三个角色:
- Component 抽象组件:定义叶子节点和组合节点的公共接口。
- Leaf 叶子节点:没有子节点的对象。
- Composite 组合节点:可以包含子节点的对象。
结构如下:
1 2 3 4 5 6 7 8
| Component ▲ │ ├── Leaf │ └── Composite │ └── List<Component>
|
组合节点内部保存的是:
而不是:
这意味着组合节点既可以包含叶子节点,也可以包含其他组合节点。
于是就形成了树。
chapter 2:为什么需要组合模式
假设我们要做一个菜单权限系统。
菜单结构如下:
1 2 3 4 5 6 7 8 9
| 系统管理 ├── 用户管理 │ ├── 新增用户按钮 │ ├── 修改用户按钮 │ └── 删除用户按钮 │ └── 角色管理 ├── 新增角色按钮 └── 分配权限按钮
|
这里有两类节点:
- 菜单目录或菜单项:可以有子节点;
- 按钮权限:通常是叶子节点,没有子节点。
如果不用组合模式,可能会写成这样:
1 2 3 4 5 6 7
| if (node instanceof MenuNode) { }
if (node instanceof ButtonNode) { }
|
当树形结构越来越复杂时,类似判断会散落在各处。
例如:
- 构建菜单树时判断;
- 渲染菜单时判断;
- 过滤权限时判断;
- 打印结构时判断;
- 禁用菜单时判断;
- 统计节点数时判断。
这会导致客户端代码强依赖具体节点类型。
组合模式的思路是:
不要让客户端关心节点到底是叶子还是组合节点,而是让它们实现同一个接口。
例如:
1 2 3 4 5 6 7 8
| public interface PermissionComponent {
String getName();
void print(String indent);
boolean hasPermission(String permissionCode); }
|
菜单节点和按钮节点都实现这个接口。
客户端只处理 PermissionComponent。
这就是组合模式的价值。
chapter 3:组合模式解决的核心问题
组合模式主要解决三个问题。
1. 树形结构表达问题
组合模式天然适合表达树形结构。
例如:
1 2 3 4 5
| root ├── child ├── child └── child └── child
|
组合节点保存子组件列表:
1
| private final List<Component> children = new ArrayList<>();
|
这种结构可以无限递归下去。
2. 统一处理问题
叶子节点和组合节点都实现同一个接口。
客户端可以统一调用:
1 2 3
| component.print(""); component.hasPermission("user:add"); component.count();
|
不需要关心具体类型。
3. 递归操作问题
组合模式非常适合递归操作。
例如:
- 递归打印;
- 递归搜索;
- 递归统计;
- 递归过滤;
- 递归删除;
- 递归计算。
这些操作在树形结构中非常常见。
chapter 4:组合模式的两种形式
组合模式通常有两种形式:
- 透明组合模式;
- 安全组合模式。
这两个概念很重要。
chapter 5:透明组合模式
透明组合模式的特点是:
在抽象组件中声明所有方法,包括管理子节点的方法。
例如:
1 2 3 4 5 6 7 8 9 10
| public interface Component {
void add(Component component);
void remove(Component component);
List<Component> getChildren();
void operation(); }
|
叶子节点也实现这些方法。
但是叶子节点没有子节点,所以通常会抛异常:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| public class Leaf implements Component {
@Override public void add(Component component) { throw new UnsupportedOperationException("Leaf can not add child"); }
@Override public void remove(Component component) { throw new UnsupportedOperationException("Leaf can not remove child"); }
@Override public List<Component> getChildren() { return List.of(); }
@Override public void operation() { System.out.println("Leaf operation"); } }
|
透明组合模式的优点
客户端可以完全统一地看待叶子节点和组合节点。
1 2
| Component component = getComponent(); component.add(child);
|
透明组合模式的缺点
叶子节点也暴露了不应该有的方法。
例如文件节点不应该有 add(),但接口要求它实现。
这会牺牲类型安全。
chapter 6:安全组合模式
安全组合模式的特点是:
抽象组件中只声明公共业务方法,不声明管理子节点的方法。
例如:
1 2 3 4
| public interface Component {
void operation(); }
|
只有组合节点才有 add()、remove()、getChildren()。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| public class Composite implements Component {
private final List<Component> children = new ArrayList<>();
public void add(Component component) { children.add(component); }
public void remove(Component component) { children.remove(component); }
@Override public void operation() { for (Component child : children) { child.operation(); } } }
|
叶子节点只实现公共业务方法:
1 2 3 4 5 6 7
| public class Leaf implements Component {
@Override public void operation() { System.out.println("Leaf operation"); } }
|
安全组合模式的优点
叶子节点不会暴露不该有的子节点管理方法。
类型更安全。
安全组合模式的缺点
客户端如果要管理树结构,需要知道某个节点是不是组合节点。
例如:
1 2
| Composite root = new Composite(); root.add(new Leaf());
|
不能完全只依赖 Component 来完成所有操作。
实际项目中如何选择
真实项目中,我更推荐安全组合模式。
原因很简单:
不要让叶子节点拥有它不该拥有的能力。
比如按钮权限节点不应该有 addChild() 方法。
如果它有这个方法,只是运行时报错,那不如在编译期就不暴露。
chapter 7:案例背景:菜单权限树
下面用一个 Java 后端开发中非常常见的案例来讲组合模式:菜单权限树。
假设我们要设计一个后台管理系统的权限资源结构。
它包含三类节点:
- 目录:例如“系统管理”;
- 菜单:例如“用户管理”;
- 按钮:例如“新增用户”“删除用户”。
目录和菜单可以包含子节点。
按钮是叶子节点,不能包含子节点。
树形结构如下:
1 2 3 4 5 6 7 8 9
| 系统管理 ├── 用户管理 │ ├── 新增用户 │ ├── 修改用户 │ └── 删除用户 │ └── 角色管理 ├── 新增角色 └── 分配权限
|
我们希望实现几个功能:
- 打印权限树;
- 判断是否包含某个权限编码;
- 统计节点数量;
- 收集所有权限编码;
- 支持组合节点添加子节点。
这个场景非常适合组合模式。
chapter 8:定义抽象组件 PermissionComponent
先定义抽象组件接口。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| import java.util.Set;
public interface PermissionComponent {
String getCode();
String getName();
void print(String indent);
boolean containsPermission(String permissionCode);
int count();
Set<String> collectCodes(); }
|
这个接口中放的是所有节点都具备的能力:
- 获取权限编码;
- 获取权限名称;
- 打印结构;
- 判断是否包含权限;
- 统计节点数量;
- 收集权限编码。
注意,这里没有定义:
1 2 3
| addChild() removeChild() getChildren()
|
因为按钮节点不应该有这些方法。
这是安全组合模式。
按钮权限是叶子节点。
它没有子节点。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51
| import java.util.Set;
public class ButtonPermission implements PermissionComponent {
private final String code;
private final String name;
public ButtonPermission(String code, String name) { if (code == null || code.isBlank()) { throw new IllegalArgumentException("code can not be blank"); }
if (name == null || name.isBlank()) { throw new IllegalArgumentException("name can not be blank"); }
this.code = code; this.name = name; }
@Override public String getCode() { return code; }
@Override public String getName() { return name; }
@Override public void print(String indent) { System.out.println(indent + "- 按钮:" + name + " [" + code + "]"); }
@Override public boolean containsPermission(String permissionCode) { return this.code.equals(permissionCode); }
@Override public int count() { return 1; }
@Override public Set<String> collectCodes() { return Set.of(code); } }
|
按钮节点只关心自己。
比如判断权限时,它只判断自己的 code 是否匹配。
1
| return this.code.equals(permissionCode);
|
这就是叶子节点的特点。
菜单节点可以包含子节点。
子节点可以是按钮,也可以是其他菜单。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99
| import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set;
public class MenuPermission implements PermissionComponent {
private final String code;
private final String name;
private final List<PermissionComponent> children = new ArrayList<>();
public MenuPermission(String code, String name) { if (code == null || code.isBlank()) { throw new IllegalArgumentException("code can not be blank"); }
if (name == null || name.isBlank()) { throw new IllegalArgumentException("name can not be blank"); }
this.code = code; this.name = name; }
public void addChild(PermissionComponent child) { if (child == null) { throw new IllegalArgumentException("child can not be null"); }
children.add(child); }
public void removeChild(PermissionComponent child) { children.remove(child); }
public List<PermissionComponent> getChildren() { return List.copyOf(children); }
@Override public String getCode() { return code; }
@Override public String getName() { return name; }
@Override public void print(String indent) { System.out.println(indent + "+ 菜单:" + name + " [" + code + "]");
for (PermissionComponent child : children) { child.print(indent + " "); } }
@Override public boolean containsPermission(String permissionCode) { if (this.code.equals(permissionCode)) { return true; }
for (PermissionComponent child : children) { if (child.containsPermission(permissionCode)) { return true; } }
return false; }
@Override public int count() { int total = 1;
for (PermissionComponent child : children) { total += child.count(); }
return total; }
@Override public Set<String> collectCodes() { Set<String> codes = new HashSet<>(); codes.add(code);
for (PermissionComponent child : children) { codes.addAll(child.collectCodes()); }
return codes; } }
|
这个类是组合模式中的 Composite。
它的核心字段是:
1
| private final List<PermissionComponent> children = new ArrayList<>();
|
注意,这里保存的是 PermissionComponent,而不是 MenuPermission 或 ButtonPermission。
这意味着它可以保存任何组件:
chapter 11:构建权限树
现在可以构建一棵权限树。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| public class PermissionTreeDemo {
public static void main(String[] args) { MenuPermission systemManagement = new MenuPermission("system", "系统管理");
MenuPermission userManagement = new MenuPermission("user", "用户管理"); userManagement.addChild(new ButtonPermission("user:add", "新增用户")); userManagement.addChild(new ButtonPermission("user:update", "修改用户")); userManagement.addChild(new ButtonPermission("user:delete", "删除用户"));
MenuPermission roleManagement = new MenuPermission("role", "角色管理"); roleManagement.addChild(new ButtonPermission("role:add", "新增角色")); roleManagement.addChild(new ButtonPermission("role:assign", "分配权限"));
systemManagement.addChild(userManagement); systemManagement.addChild(roleManagement);
systemManagement.print("");
System.out.println("节点数量:" + systemManagement.count());
System.out.println("是否包含 user:add 权限:" + systemManagement.containsPermission("user:add"));
System.out.println("所有权限编码:" + systemManagement.collectCodes()); } }
|
输出类似:
1 2 3 4 5 6 7 8 9 10 11
| + 菜单:系统管理 [system] + 菜单:用户管理 [user] - 按钮:新增用户 [user:add] - 按钮:修改用户 [user:update] - 按钮:删除用户 [user:delete] + 菜单:角色管理 [role] - 按钮:新增角色 [role:add] - 按钮:分配权限 [role:assign] 节点数量:8 是否包含 user:add 权限:true 所有权限编码:[role:assign, user:update, system, role:add, user:delete, user:add, role, user]
|
客户端只需要面对统一的 PermissionComponent 接口。
不管节点是菜单还是按钮,都可以调用:
1 2 3 4
| print() containsPermission() count() collectCodes()
|
这就是组合模式带来的统一性。
chapter 12:组合模式中的递归思想
组合模式的核心之一就是递归。
例如打印树:
1 2 3 4 5 6 7 8
| @Override public void print(String indent) { System.out.println(indent + "+ 菜单:" + name + " [" + code + "]");
for (PermissionComponent child : children) { child.print(indent + " "); } }
|
菜单节点打印自己之后,再让每个子节点打印自己。
如果子节点是按钮,按钮直接打印。
如果子节点是菜单,菜单继续递归打印自己的子节点。
权限判断也是递归:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| @Override public boolean containsPermission(String permissionCode) { if (this.code.equals(permissionCode)) { return true; }
for (PermissionComponent child : children) { if (child.containsPermission(permissionCode)) { return true; } }
return false; }
|
统计数量也是递归:
1 2 3 4 5 6 7 8 9 10
| @Override public int count() { int total = 1;
for (PermissionComponent child : children) { total += child.count(); }
return total; }
|
组合模式让递归逻辑自然地分布在节点内部,而不是全部堆在客户端。
chapter 13:从数据库构建树形结构
真实后端系统中,菜单和权限通常来自数据库。
例如表结构可能是:
1 2 3 4 5 6 7
| sys_permission id parent_id code name type sort
|
其中 type 表示节点类型:
可以先定义数据库实体对应的 DTO。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
| public class PermissionNodeDTO {
private Long id;
private Long parentId;
private String code;
private String name;
private String type;
private Integer sort;
public PermissionNodeDTO(Long id, Long parentId, String code, String name, String type, Integer sort) { this.id = id; this.parentId = parentId; this.code = code; this.name = name; this.type = type; this.sort = sort; }
public Long getId() { return id; }
public Long getParentId() { return parentId; }
public String getCode() { return code; }
public String getName() { return name; }
public String getType() { return type; }
public Integer getSort() { return sort; } }
|
然后写一个构建器。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| import java.util.Comparator; import java.util.List; import java.util.Map; import java.util.stream.Collectors;
public class PermissionTreeBuilder {
public List<PermissionComponent> buildTree(List<PermissionNodeDTO> nodes) { Map<Long, List<PermissionNodeDTO>> childrenMap = nodes.stream() .collect(Collectors.groupingBy(PermissionNodeDTO::getParentId));
return buildChildren(0L, childrenMap); }
private List<PermissionComponent> buildChildren(Long parentId, Map<Long, List<PermissionNodeDTO>> childrenMap) { return childrenMap.getOrDefault(parentId, List.of()).stream() .sorted(Comparator.comparing(PermissionNodeDTO::getSort)) .map(node -> buildNode(node, childrenMap)) .collect(Collectors.toList()); }
private PermissionComponent buildNode(PermissionNodeDTO node, Map<Long, List<PermissionNodeDTO>> childrenMap) { if ("BUTTON".equals(node.getType())) { return new ButtonPermission(node.getCode(), node.getName()); }
MenuPermission menu = new MenuPermission(node.getCode(), node.getName());
List<PermissionComponent> children = buildChildren(node.getId(), childrenMap);
for (PermissionComponent child : children) { menu.addChild(child); }
return menu; } }
|
使用方式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| import java.util.List;
public class PermissionTreeBuildDemo {
public static void main(String[] args) { List<PermissionNodeDTO> nodes = List.of( new PermissionNodeDTO(1L, 0L, "system", "系统管理", "MENU", 1), new PermissionNodeDTO(2L, 1L, "user", "用户管理", "MENU", 1), new PermissionNodeDTO(3L, 2L, "user:add", "新增用户", "BUTTON", 1), new PermissionNodeDTO(4L, 2L, "user:update", "修改用户", "BUTTON", 2), new PermissionNodeDTO(5L, 2L, "user:delete", "删除用户", "BUTTON", 3), new PermissionNodeDTO(6L, 1L, "role", "角色管理", "MENU", 2), new PermissionNodeDTO(7L, 6L, "role:add", "新增角色", "BUTTON", 1), new PermissionNodeDTO(8L, 6L, "role:assign", "分配权限", "BUTTON", 2) );
PermissionTreeBuilder builder = new PermissionTreeBuilder();
List<PermissionComponent> roots = builder.buildTree(nodes);
for (PermissionComponent root : roots) { root.print(""); } } }
|
这就是组合模式在后端菜单权限系统中的真实落地方式。
chapter 14:在 Spring Boot 中落地
在 Spring Boot 项目中,可以把权限树构建逻辑放到 Service 中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
| import org.springframework.stereotype.Service;
import java.util.List; import java.util.Set; import java.util.stream.Collectors;
@Service public class PermissionService {
private final PermissionRepository permissionRepository;
private final PermissionTreeBuilder permissionTreeBuilder;
public PermissionService(PermissionRepository permissionRepository, PermissionTreeBuilder permissionTreeBuilder) { this.permissionRepository = permissionRepository; this.permissionTreeBuilder = permissionTreeBuilder; }
public List<PermissionComponent> getPermissionTree(Long userId) { List<PermissionNodeDTO> nodes = permissionRepository.findPermissionsByUserId(userId);
return permissionTreeBuilder.buildTree(nodes); }
public boolean hasPermission(Long userId, String permissionCode) { List<PermissionComponent> roots = getPermissionTree(userId);
for (PermissionComponent root : roots) { if (root.containsPermission(permissionCode)) { return true; } }
return false; }
public Set<String> getPermissionCodes(Long userId) { List<PermissionComponent> roots = getPermissionTree(userId);
return roots.stream() .flatMap(root -> root.collectCodes().stream()) .collect(Collectors.toSet()); } }
|
这里的 PermissionRepository 可以是 JPA、MyBatis、MyBatis-Plus,只负责从数据库取数据。
1 2 3 4
| public interface PermissionRepository {
List<PermissionNodeDTO> findPermissionsByUserId(Long userId); }
|
组合模式负责表达和操作树结构。
Repository 负责数据访问。
Service 负责应用逻辑。
职责边界清晰,后面维护不会像在毛线团里找耳机线一样痛苦。
chapter 15:组合模式和前端菜单树返回
后端通常需要给前端返回菜单树。
可以定义一个 VO。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| import java.util.ArrayList; import java.util.List;
public class PermissionTreeVO {
private String code;
private String name;
private List<PermissionTreeVO> children = new ArrayList<>();
public PermissionTreeVO(String code, String name) { this.code = code; this.name = name; }
public void addChild(PermissionTreeVO child) { this.children.add(child); }
public String getCode() { return code; }
public String getName() { return name; }
public List<PermissionTreeVO> getChildren() { return children; } }
|
为了转换,可以在 PermissionComponent 中增加一个方法:
1
| PermissionTreeVO toTreeVO();
|
接口变成:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| import java.util.Set;
public interface PermissionComponent {
String getCode();
String getName();
void print(String indent);
boolean containsPermission(String permissionCode);
int count();
Set<String> collectCodes();
PermissionTreeVO toTreeVO(); }
|
按钮节点实现:
1 2 3 4
| @Override public PermissionTreeVO toTreeVO() { return new PermissionTreeVO(code, name); }
|
菜单节点实现:
1 2 3 4 5 6 7 8 9 10
| @Override public PermissionTreeVO toTreeVO() { PermissionTreeVO vo = new PermissionTreeVO(code, name);
for (PermissionComponent child : children) { vo.addChild(child.toTreeVO()); }
return vo; }
|
这样就可以递归生成前端需要的树形结构。
1 2 3
| List<PermissionTreeVO> tree = roots.stream() .map(PermissionComponent::toTreeVO) .toList();
|
组合模式非常适合这类树形 DTO 转换。
chapter 16:组合模式和文件系统案例
组合模式最经典的案例是文件系统。
文件和目录都可以看作文件系统节点。
定义统一接口:
1 2 3 4 5 6 7 8
| public interface FileSystemNode {
String getName();
long size();
void print(String indent); }
|
文件是叶子节点:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| public class FileNode implements FileSystemNode {
private final String name;
private final long size;
public FileNode(String name, long size) { this.name = name; this.size = size; }
@Override public String getName() { return name; }
@Override public long size() { return size; }
@Override public void print(String indent) { System.out.println(indent + "- 文件:" + name + ",大小:" + size); } }
|
目录是组合节点:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
| import java.util.ArrayList; import java.util.List;
public class DirectoryNode implements FileSystemNode {
private final String name;
private final List<FileSystemNode> children = new ArrayList<>();
public DirectoryNode(String name) { this.name = name; }
public void add(FileSystemNode node) { children.add(node); }
public void remove(FileSystemNode node) { children.remove(node); }
@Override public String getName() { return name; }
@Override public long size() { long total = 0;
for (FileSystemNode child : children) { total += child.size(); }
return total; }
@Override public void print(String indent) { System.out.println(indent + "+ 目录:" + name);
for (FileSystemNode child : children) { child.print(indent + " "); } } }
|
使用方式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| public class FileSystemDemo {
public static void main(String[] args) { DirectoryNode root = new DirectoryNode("root");
root.add(new FileNode("README.md", 10)); root.add(new FileNode("pom.xml", 20));
DirectoryNode src = new DirectoryNode("src"); src.add(new FileNode("Application.java", 100)); src.add(new FileNode("UserService.java", 200));
root.add(src);
root.print("");
System.out.println("总大小:" + root.size()); } }
|
这个例子可以非常直观地体现组合模式:
- 文件和目录都可以计算大小;
- 文件返回自己的大小;
- 目录递归计算子节点大小;
- 客户端统一调用
size()。
chapter 17:组合模式和递归查询的关系
树形结构常常有两种处理方式:
- 数据库层递归;
- 内存中递归。
1. 数据库层递归
如果数据库支持递归查询,例如 PostgreSQL 的递归 CTE,可以直接查询整棵树。
这种方式适合数据量较大、需要数据库层过滤的场景。
2. 内存中递归
先查询一批节点,再在内存中构建树。
这种方式适合:
- 菜单数量不大;
- 权限节点数量可控;
- 需要复杂对象行为;
- 需要组合模式封装业务操作。
组合模式通常是在内存对象模型中发挥作用。
它不替代数据库查询,而是让查询出来的数据变成一个更好用的对象结构。
chapter 18:组合模式和递归过深问题
组合模式经常使用递归,因此要注意递归深度。
如果树非常深,例如几千层,可能出现:
普通菜单树、分类树一般不会这么深。
但是以下场景要小心:
- 用户自定义目录;
- 评论无限嵌套;
- 流程节点嵌套;
- 组织层级异常数据;
- 知识库目录无限层级。
解决方式包括:
- 限制最大层级;
- 构建树前做环检测;
- 使用迭代方式遍历;
- 数据库层限制递归深度;
- 保存路径字段,例如
path;
- 使用闭包表或物化路径模型。
组合模式很优雅,但树如果长歪了,优雅也会栈溢出。
chapter 19:组合模式和循环引用问题
树结构理论上不应该有环。
但是数据库数据可能脏。
例如:
1 2 3
| A 的 parent 是 B B 的 parent 是 C C 的 parent 是 A
|
这就不是树了,是环。
如果直接递归,会无限循环。
所以构建树时最好做检测。
简单检测方式:
1 2 3 4 5 6 7 8 9 10 11
| private void checkCycle(Long nodeId, Set<Long> visiting) { if (visiting.contains(nodeId)) { throw new IllegalStateException("Cycle detected in permission tree, nodeId = " + nodeId); }
visiting.add(nodeId);
visiting.remove(nodeId); }
|
在真实项目中,菜单、部门、分类这些层级数据都应该有防环校验。
不要等线上递归爆栈之后再说“数据库里怎么会有这种数据”。数据库不会背锅,它只是忠实记录了你允许写进去的混乱。
chapter 20:组合模式和装饰器模式的区别
组合模式和装饰器模式都属于结构型模式,也都使用组合关系。
但它们的目的完全不同。
| 对比项 |
组合模式 |
装饰器模式 |
| 目的 |
表示部分-整体的树形结构 |
动态增强对象功能 |
| 结构 |
一个节点包含多个子节点 |
一个装饰器包装一个对象 |
| 关系 |
树形关系 |
链式包裹关系 |
| 典型场景 |
菜单树、文件树、组织树 |
日志增强、缓存增强、重试增强 |
| 客户端关注点 |
统一处理单个对象和组合对象 |
在不改原类的情况下增强行为 |
组合模式强调:
装饰器模式强调:
文件系统是组合模式。
给文件系统操作加缓存、日志、限流,才是装饰器模式。
chapter 21:组合模式和桥接模式的区别
组合模式和桥接模式也都是结构型模式。
| 对比项 |
组合模式 |
桥接模式 |
| 目的 |
表达树形的部分-整体关系 |
拆分多个独立变化维度 |
| 核心结构 |
节点包含子节点 |
抽象层持有实现层 |
| 典型关系 |
递归包含 |
横向组合 |
| 示例 |
菜单包含子菜单和按钮 |
消息类型组合发送渠道 |
| 重点 |
一致处理树中节点 |
独立扩展两个维度 |
组合模式适合:
桥接模式适合:
不要因为两者都用了组合关系就混在一起。
设计模式里“组合”这个词出现很多,但语义不一样。就像 Java 里到处都有 Context,但每个 Context 都像不同部门的“临时负责人”。
chapter 22:组合模式和责任链模式的区别
责任链模式也可能出现链式结构,但它和组合模式不同。
| 对比项 |
组合模式 |
责任链模式 |
| 结构 |
树 |
链 |
| 目的 |
表示整体和部分 |
将请求沿链传递 |
| 调用方式 |
通常遍历所有或部分子节点 |
一个节点处理不了交给下一个 |
| 典型场景 |
菜单树、文件树 |
审批流、过滤器链 |
| 重点 |
统一处理节点 |
请求传递和处理 |
组合模式强调树形层级。
责任链强调请求流转。
chapter 23:组合模式的优点
1. 统一处理叶子节点和组合节点
客户端可以统一调用组件接口。
1 2 3
| component.print(""); component.count(); component.collectCodes();
|
2. 非常适合树形结构
菜单树、组织树、文件树、分类树都很自然。
3. 易于扩展新节点类型
可以新增新的叶子节点或组合节点,只要实现统一接口即可。
4. 递归逻辑更清晰
递归操作封装在组件内部,客户端不需要到处写复杂判断。
5. 符合开闭原则
新增节点类型时,尽量不修改已有客户端代码。
chapter 24:组合模式的缺点
1. 抽象设计需要谨慎
抽象组件接口如果设计过大,叶子节点可能被迫实现不需要的方法。
2. 可能隐藏节点差异
统一接口有时会抹平叶子节点和组合节点之间的重要差异。
3. 递归可能带来性能问题
树很大时,递归遍历可能有性能和栈深度风险。
4. 数据结构约束需要额外保证
组合模式假设是树,但数据库中可能出现环、孤儿节点、重复节点。
这些需要在数据层或构建层处理。
5. 调试递归逻辑不如线性逻辑直观
尤其是节点层级很深时,排查问题需要更清晰的日志。
chapter 25:适用场景
组合模式适合以下场景。
1. 需要表示树形结构
例如:
- 菜单树;
- 文件目录树;
- 部门组织树;
- 商品分类树;
- 评论回复树。
2. 希望统一处理整体和部分
例如文件和目录都支持:
菜单和按钮都支持:
1 2
| containsPermission() collectCodes()
|
3. 需要递归操作
例如:
- 递归统计;
- 递归搜索;
- 递归过滤;
- 递归转换;
- 递归渲染。
4. 节点类型比较稳定
如果树中节点类型相对稳定,组合模式会很舒服。
5. 客户端不想关心具体节点类型
客户端只依赖抽象组件即可。
chapter 26:不适合使用的场景
以下场景不建议使用组合模式。
1. 数据不是树形结构
如果对象之间不是部分-整体关系,就不要强行使用组合模式。
2. 叶子节点和组合节点差异极大
如果两者几乎没有共同操作,强行抽象会很别扭。
3. 树结构非常简单
如果只是两层结构,而且操作很少,普通 DTO + List 就够了。
4. 节点之间有复杂图关系
如果数据结构是图,而不是树,组合模式不一定适合。
例如社交关系、知识图谱、网状依赖关系。
5. 需要频繁跨层查询
如果每次都要在巨大树中递归搜索,可能要考虑索引、缓存、数据库模型优化。
chapter 27:真实项目中的实践建议
1. 优先使用安全组合模式
尽量不要让叶子节点暴露 addChild() 这种不该有的方法。
1 2 3
| public interface PermissionComponent { void print(String indent); }
|
组合节点自己提供:
1
| public void addChild(PermissionComponent child)
|
2. 抽象组件接口不要过大
只把真正所有节点都需要的行为放到接口里。
不要为了统一而统一。
3. 构建树时处理脏数据
至少要考虑:
- parentId 不存在;
- 节点循环引用;
- 重复节点;
- 排序字段为空;
- 根节点不唯一;
- 深度过大。
4. 大树要考虑缓存
菜单权限树通常可以缓存。
例如:
- 按用户缓存权限编码;
- 按角色缓存权限树;
- 按租户缓存菜单结构。
但是要注意权限变更后的缓存失效。
5. 后端返回前端时不要暴露领域对象
PermissionComponent 是内部对象模型。
返回前端建议转换成 VO。
6. 递归方法要注意命名和边界
例如:
1 2 3 4
| collectCodes() containsPermission() count() toTreeVO()
|
这些方法语义清晰,维护时更容易理解。
7. 不要把数据库实体直接做成组合节点
数据库 Entity 通常只负责持久化数据。
组合模式对象更适合作为领域对象、业务对象或视图构建对象。
否则 Entity 会越来越重,最后变成“数据库字段 + 业务递归 + 前端渲染 + 权限判断”的缝合怪。
chapter 28:完整案例代码汇总
抽象组件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| import java.util.Set;
public interface PermissionComponent {
String getCode();
String getName();
void print(String indent);
boolean containsPermission(String permissionCode);
int count();
Set<String> collectCodes(); }
|
叶子节点
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51
| import java.util.Set;
public class ButtonPermission implements PermissionComponent {
private final String code;
private final String name;
public ButtonPermission(String code, String name) { if (code == null || code.isBlank()) { throw new IllegalArgumentException("code can not be blank"); }
if (name == null || name.isBlank()) { throw new IllegalArgumentException("name can not be blank"); }
this.code = code; this.name = name; }
@Override public String getCode() { return code; }
@Override public String getName() { return name; }
@Override public void print(String indent) { System.out.println(indent + "- 按钮:" + name + " [" + code + "]"); }
@Override public boolean containsPermission(String permissionCode) { return this.code.equals(permissionCode); }
@Override public int count() { return 1; }
@Override public Set<String> collectCodes() { return Set.of(code); } }
|
组合节点
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99
| import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set;
public class MenuPermission implements PermissionComponent {
private final String code;
private final String name;
private final List<PermissionComponent> children = new ArrayList<>();
public MenuPermission(String code, String name) { if (code == null || code.isBlank()) { throw new IllegalArgumentException("code can not be blank"); }
if (name == null || name.isBlank()) { throw new IllegalArgumentException("name can not be blank"); }
this.code = code; this.name = name; }
public void addChild(PermissionComponent child) { if (child == null) { throw new IllegalArgumentException("child can not be null"); }
children.add(child); }
public void removeChild(PermissionComponent child) { children.remove(child); }
public List<PermissionComponent> getChildren() { return List.copyOf(children); }
@Override public String getCode() { return code; }
@Override public String getName() { return name; }
@Override public void print(String indent) { System.out.println(indent + "+ 菜单:" + name + " [" + code + "]");
for (PermissionComponent child : children) { child.print(indent + " "); } }
@Override public boolean containsPermission(String permissionCode) { if (this.code.equals(permissionCode)) { return true; }
for (PermissionComponent child : children) { if (child.containsPermission(permissionCode)) { return true; } }
return false; }
@Override public int count() { int total = 1;
for (PermissionComponent child : children) { total += child.count(); }
return total; }
@Override public Set<String> collectCodes() { Set<String> codes = new HashSet<>(); codes.add(code);
for (PermissionComponent child : children) { codes.addAll(child.collectCodes()); }
return codes; } }
|
客户端
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| public class PermissionTreeDemo {
public static void main(String[] args) { MenuPermission systemManagement = new MenuPermission("system", "系统管理");
MenuPermission userManagement = new MenuPermission("user", "用户管理"); userManagement.addChild(new ButtonPermission("user:add", "新增用户")); userManagement.addChild(new ButtonPermission("user:update", "修改用户")); userManagement.addChild(new ButtonPermission("user:delete", "删除用户"));
MenuPermission roleManagement = new MenuPermission("role", "角色管理"); roleManagement.addChild(new ButtonPermission("role:add", "新增角色")); roleManagement.addChild(new ButtonPermission("role:assign", "分配权限"));
systemManagement.addChild(userManagement); systemManagement.addChild(roleManagement);
systemManagement.print("");
System.out.println("节点数量:" + systemManagement.count());
System.out.println("是否包含 user:add 权限:" + systemManagement.containsPermission("user:add"));
System.out.println("所有权限编码:" + systemManagement.collectCodes()); } }
|
chapter 29:一句话总结
组合模式的本质是:
用统一的组件接口表示树形结构中的叶子节点和组合节点,让客户端可以一致地处理单个对象和一组对象。
它特别适合:
- 菜单树;
- 权限树;
- 文件树;
- 分类树;
- 组织架构树;
- 评论回复树。
如果你的业务结构天然是“整体-部分”的树形关系,并且你希望对单个节点和组合节点进行统一操作,那么组合模式就非常合适。
但如果数据不是树,或者节点之间几乎没有共同操作,就不要硬套组合模式。
好的组合模式应该让树形结构更清晰,而不是把简单问题递归成森林迷宫。
参考资料
- Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides. Design Patterns: Elements of Reusable Object-Oriented Software.
- Robert C. Martin. Agile Software Development, Principles, Patterns, and Practices.
- Joshua Bloch. Effective Java.
- Spring Framework Documentation: Core Technologies - The IoC Container.
- Refactoring Guru: Composite Pattern.
- SourceMaking: Composite Design Pattern.
启示录
富贵岂由人,时会高志须酬。
能成功于千载者,必以近察远。