组合模式:设计与实践
一、什么是组合模式
1. 基本定义
组合模式(Composite Pattern)是一种结构型设计模式,由《设计模式:可复用面向对象软件的基础》(GOF著作)定义为:将对象组合成树形结构以表示“部分-整体”的层次结构。组合模式使得用户对单个对象和组合对象的使用具有一致性。
该模式通过定义一个统一的组件接口,将单个对象(叶子节点)和组合对象(容器节点)纳入同一层次结构,客户端可以忽略两者的差异,以统一的方式处理整体和部分,本质是用树形结构组织对象,实现“整体-部分”的递归操作。
2. 核心思想
组合模式的核心在于统一整体与部分的接口。当系统中存在“部分-整体”的层级关系(如订单与子订单、组织与部门),且需要对整体和部分进行统一操作(如计算总金额、批量处理)时,组合模式通过将叶子节点(不可再分的部分)和容器节点(包含部分的整体)实现相同的接口,使客户端可以透明地递归处理整个树形结构,无需区分操作的是单个对象还是组合对象。
二、组合模式的特点
1. 树形结构
以树形结构组织对象,明确“部分-整体”的层级关系,容器节点可以包含叶子节点或其他容器节点。
2. 接口统一
叶子节点和容器节点实现相同的组件接口,客户端以一致的方式处理两者,无需区分类型。
3. 递归操作
支持对整个树形结构进行递归操作(如遍历所有节点、计算总和),简化批量处理逻辑。
4. 灵活性与扩展性
新增叶子节点或容器节点无需修改现有代码,只需实现组件接口,符合开闭原则。
5. 透明性
客户端无需知道操作的是单个对象还是组合对象,操作方式完全一致,降低使用复杂度。
| 特点 | 说明 |
|---|---|
| 树形结构 | 以层级结构组织对象,体现“部分-整体”关系 |
| 接口统一 | 叶子与容器实现相同接口,客户端操作一致 |
| 递归操作 | 支持对树形结构进行递归遍历和处理 |
| 灵活扩展 | 新增节点无需修改现有代码,符合开闭原则 |
| 透明性 | 客户端无需区分叶子与容器,操作方式统一 |
三、组合模式的标准代码实现
1. 模式结构
组合模式包含三个核心角色:
- 组件(Component):定义叶子节点和容器节点的统一接口,声明对节点的操作(如添加、移除、遍历)
- 叶子(Leaf):表示不可再分的原子对象,实现组件接口的基本操作,不包含子节点
- 容器(Composite):表示包含子节点的组合对象,实现组件接口的所有操作,可包含叶子或其他容器
2. 代码实现示例
2.1 组件接口(Component)
import java.util.Iterator;
/**
* 组件接口
* 定义叶子和容器的统一操作
*/
public interface Component {
/**
* 执行组件操作
*/
void operation();
/**
* 添加子组件
* @param component 子组件
*/
void add(Component component);
/**
* 移除子组件
* @param component 子组件
*/
void remove(Component component);
/**
* 获取子组件迭代器
* @return 迭代器
*/
Iterator<Component> getChildren();
}
2.2 叶子节点(Leaf)
import java.util.Iterator;
import java.util.NoSuchElementException;
/**
* 叶子节点
* 不可包含子节点,实现基本操作
*/
public class Leaf implements Component {
private String name;
public Leaf(String name) {
this.name = name;
}
@Override
public void operation() {
System.out.println("执行叶子节点[" + name + "]的操作");
}
/**
* 叶子节点不支持添加子组件,抛出异常
*/
@Override
public void add(Component component) {
throw new UnsupportedOperationException("叶子节点[" + name + "]不支持添加子组件");
}
/**
* 叶子节点不支持移除子组件,抛出异常
*/
@Override
public void remove(Component component) {
throw new UnsupportedOperationException("叶子节点[" + name + "]不支持移除子组件");
}
/**
* 叶子节点没有子组件,返回空迭代器
*/
@Override
public Iterator<Component> getChildren() {
return new Iterator<Component>() {
@Override
public boolean hasNext() {
return false;
}
@Override
public Component next() {
throw new NoSuchElementException("叶子节点[" + name + "]没有子组件");
}
};
}
}
2.3 容器节点(Composite)
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
/**
* 容器节点
* 可包含子节点(叶子或其他容器),实现所有操作
*/
public class Composite implements Component {
private String name;
// 存储子组件的集合
private List<Component> children = new ArrayList<>();
public Composite(String name) {
this.name = name;
}
/**
* 容器节点的操作:递归执行所有子组件的操作
*/
@Override
public void operation() {
System.out.println("开始执行容器节点[" + name + "]的操作");
// 递归调用子组件的操作
for (Component child : children) {
child.operation();
}
System.out.println("完成执行容器节点[" + name + "]的操作");
}
/**
* 添加子组件
*/
@Override
public void add(Component component) {
children.add(component);
System.out.println("容器节点[" + name + "]添加子组件:" + component);
}
/**
* 移除子组件
*/
@Override
public void remove(Component component) {
if (children.remove(component)) {
System.out.println("容器节点[" + name + "]移除子组件:" + component);
}
}
/**
* 返回子组件的迭代器
*/
@Override
public Iterator<Component> getChildren() {
return children.iterator();
}
}
2.4 客户端使用示例
/**
* 客户端
* 以统一方式操作叶子和容器节点
*/
public class Client {
public static void main(String[] args) {
// 创建叶子节点
Component leaf1 = new Leaf("叶子1");
Component leaf2 = new Leaf("叶子2");
Component leaf3 = new Leaf("叶子3");
// 创建容器节点并添加子组件
Component composite1 = new Composite("容器1");
composite1.add(leaf1);
composite1.add(leaf2);
// 创建嵌套容器节点
Component composite2 = new Composite("容器2");
composite2.add(composite1); // 添加容器作为子节点
composite2.add(leaf3);
// 统一操作:执行容器2的操作(会递归执行所有子节点)
System.out.println("=== 执行容器2的操作 ===");
composite2.operation();
// 统一操作:遍历容器2的所有子节点
System.out.println("\n=== 遍历容器2的子节点 ===");
traverse(composite2, 0);
}
/**
* 递归遍历树形结构
* @param component 组件
* @param depth 层级深度(用于缩进显示)
*/
private static void traverse(Component component, int depth) {
// 打印当前节点(根据深度缩进)
String indent = " ".repeat(depth);
System.out.println(indent + "节点:" + component);
// 递归遍历子节点
component.getChildren().forEach(child -> traverse(child, depth + 1));
}
}
3. 代码实现特点总结
| 角色 | 核心职责 | 代码特点 |
|---|---|---|
| 组件(Component) | 定义统一接口 | 声明操作、添加、移除、遍历方法,是叶子和容器的超类型 |
| 叶子(Leaf) | 表示不可再分的原子对象 | 实现组件接口,不支持添加/移除操作(抛出异常),无子节点 |
| 容器(Composite) | 表示组合对象,包含子节点 | 实现组件接口,维护子节点集合,递归调用子节点的操作 |
四、支付框架设计中组合模式的运用
以复杂订单结构的组合管理为例,说明组合模式在支付系统中的具体实现:
1. 场景分析
支付系统中,订单存在“主订单-子订单”的层级关系:
- 主订单(MainOrder):包含多个子订单,可视为“整体”
- 子订单(SubOrder):不可再分的原子订单,可视为“部分”
- 嵌套场景:主订单中可包含其他主订单(如合并支付多个主订单)
业务需要对订单进行统一操作:
- 计算总金额(主订单总金额=所有子订单金额之和)
- 批量取消(主订单取消=所有子订单取消)
- 查询状态(主订单状态由子订单状态聚合而来)
使用组合模式可将主订单和子订单纳入同一接口,实现对整个订单树的统一操作,避免区分处理逻辑。
2. 设计实现
2.1 订单组件接口(Component)
import java.math.BigDecimal;
import java.util.Iterator;
/**
* 订单组件接口
* 定义主订单和子订单的统一操作
*/
public interface OrderComponent {
/**
* 获取订单ID
*/
String getOrderId();
/**
* 获取订单总金额
*/
BigDecimal getTotalAmount();
/**
* 获取订单状态
* @return 状态(CREATED/PAYING/PAID/CANCELED)
*/
String getStatus();
/**
* 取消订单
*/
void cancel();
/**
* 添加子订单(仅主订单支持)
*/
void addSubOrder(OrderComponent subOrder);
/**
* 移除子订单(仅主订单支持)
*/
void removeSubOrder(OrderComponent subOrder);
/**
* 获取子订单迭代器
*/
Iterator<OrderComponent> getSubOrders();
}
2.2 子订单(叶子节点)
import java.math.BigDecimal;
import java.util.Iterator;
import java.util.NoSuchElementException;
/**
* 子订单(叶子节点)
* 不可包含子订单,是最小订单单位
*/
public class SubOrder implements OrderComponent {
private String orderId;
private BigDecimal amount;
private String status;
private String productId; // 商品ID(子订单特有属性)
public SubOrder(String orderId, BigDecimal amount, String productId) {
this.orderId = orderId;
this.amount = amount;
this.productId = productId;
this.status = "CREATED"; // 初始状态:已创建
}
@Override
public String getOrderId() {
return orderId;
}
@Override
public BigDecimal getTotalAmount() {
return amount; // 子订单金额即自身金额
}
@Override
public String getStatus() {
return status;
}
@Override
public void cancel() {
if ("CREATED".equals(status) || "PAYING".equals(status)) {
this.status = "CANCELED";
System.out.println("子订单[" + orderId + "]已取消");
} else {
System.out.println("子订单[" + orderId + "]状态为[" + status + "],无法取消");
}
}
/**
* 子订单不支持添加子订单
*/
@Override
public void addSubOrder(OrderComponent subOrder) {
throw new UnsupportedOperationException("子订单[" + orderId + "]不支持添加子订单");
}
/**
* 子订单不支持移除子订单
*/
@Override
public void removeSubOrder(OrderComponent subOrder) {
throw new UnsupportedOperationException("子订单[" + orderId + "]不支持移除子订单");
}
/**
* 子订单没有子订单,返回空迭代器
*/
@Override
public Iterator<OrderComponent> getSubOrders() {
return new Iterator<OrderComponent>() {
@Override
public boolean hasNext() {
return false;
}
@Override
public OrderComponent next() {
throw new NoSuchElementException("子订单[" + orderId + "]没有子订单");
}
};
}
}
2.3 主订单(容器节点)
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
/**
* 主订单(容器节点)
* 可包含子订单或其他主订单
*/
public class MainOrder implements OrderComponent {
private String orderId;
private String status;
private List<OrderComponent> subOrders = new ArrayList<>();
private String merchantId; // 商户ID(主订单特有属性)
public MainOrder(String orderId, String merchantId) {
this.orderId = orderId;
this.merchantId = merchantId;
this.status = "CREATED";
}
@Override
public String getOrderId() {
return orderId;
}
@Override
public BigDecimal getTotalAmount() {
// 主订单总金额 = 所有子订单金额之和
return subOrders.stream()
.map(OrderComponent::getTotalAmount)
.reduce(BigDecimal.ZERO, BigDecimal::add);
}
@Override
public String getStatus() {
// 主订单状态由子订单状态聚合而来
if (subOrders.isEmpty()) {
return status;
}
// 所有子订单已支付 → 主订单已支付
boolean allPaid = subOrders.stream().allMatch(o -> "PAID".equals(o.getStatus()));
if (allPaid) {
return "PAID";
}
// 存在子订单取消 → 主订单部分取消
boolean hasCanceled = subOrders.stream().anyMatch(o -> "CANCELED".equals(o.getStatus()));
if (hasCanceled) {
return "PARTIALLY_CANCELED";
}
// 其他情况(如部分支付中)
return "PROCESSING";
}
@Override
public void cancel() {
if ("CREATED".equals(status) || "PROCESSING".equals(status)) {
// 递归取消所有子订单
subOrders.forEach(OrderComponent::cancel);
// 更新自身状态
this.status = "CANCELED";
System.out.println("主订单[" + orderId + "]已取消(包含" + subOrders.size() + "个子订单)");
} else {
System.out.println("主订单[" + orderId + "]状态为[" + status + "],无法取消");
}
}
@Override
public void addSubOrder(OrderComponent subOrder) {
// 校验订单状态(只有创建状态可添加子订单)
if (!"CREATED".equals(status)) {
throw new IllegalStateException("主订单[" + orderId + "]状态为[" + status + "],无法添加子订单");
}
subOrders.add(subOrder);
System.out.println("主订单[" + orderId + "]添加子订单[" + subOrder.getOrderId() + "]");
}
@Override
public void removeSubOrder(OrderComponent subOrder) {
if (subOrders.remove(subOrder)) {
System.out.println("主订单[" + orderId + "]移除子订单[" + subOrder.getOrderId() + "]");
}
}
@Override
public Iterator<OrderComponent> getSubOrders() {
return subOrders.iterator();
}
}
2.4 订单服务(客户端)
import java.math.BigDecimal;
import java.util.Iterator;
/**
* 订单服务
* 以统一方式处理主订单和子订单
*/
public class OrderService {
/**
* 计算订单总金额(支持主订单和子订单)
*/
public BigDecimal calculateTotal(OrderComponent order) {
return order.getTotalAmount();
}
/**
* 取消订单(支持主订单和子订单,主订单会递归取消所有子订单)
*/
public void cancelOrder(OrderComponent order) {
order.cancel();
}
/**
* 打印订单详情(递归遍历所有子订单)
*/
public void printOrder(OrderComponent order) {
System.out.println("\n=== 订单详情 ===");
printOrderRecursive(order, 0);
System.out.println("=== 订单汇总 ===");
System.out.println("订单ID:" + order.getOrderId());
System.out.println("总金额:" + order.getTotalAmount());
System.out.println("订单状态:" + order.getStatus());
}
/**
* 递归打印订单树形结构
*/
private void printOrderRecursive(OrderComponent order, int depth) {
String indent = " ".repeat(depth);
String type = order instanceof MainOrder ? "主订单" : "子订单";
System.out.printf("%s%s:%s,金额:%s,状态:%s%n",
indent, type, order.getOrderId(),
order.getTotalAmount(), order.getStatus());
// 递归打印子订单
Iterator<OrderComponent> iterator = order.getSubOrders();
iterator.forEachRemaining(child -> printOrderRecursive(child, depth + 1));
}
// 使用示例
public static void main(String[] args) {
OrderService service = new OrderService();
// 1. 创建子订单(叶子节点)
OrderComponent sub1 = new SubOrder("SUB001", new BigDecimal("100.00"), "PROD001");
OrderComponent sub2 = new SubOrder("SUB002", new BigDecimal("200.00"), "PROD002");
OrderComponent sub3 = new SubOrder("SUB003", new BigDecimal("150.00"), "PROD003");
// 2. 创建主订单(容器节点)并添加子订单
OrderComponent main1 = new MainOrder("MAIN001", "MCH001");
main1.addSubOrder(sub1);
main1.addSubOrder(sub2);
// 3. 创建嵌套主订单(包含其他主订单)
OrderComponent main2 = new MainOrder("MAIN002", "MCH001");
main2.addSubOrder(main1); // 添加主订单作为子节点
main2.addSubOrder(sub3);
// 4. 统一操作:打印订单(会递归遍历所有子节点)
service.printOrder(main2);
// 5. 统一操作:取消主订单2(会递归取消所有子订单)
System.out.println("\n=== 取消订单 ===");
service.cancelOrder(main2);
// 6. 打印取消后的订单状态
service.printOrder(main2);
}
}
3. 模式价值体现
- 接口统一:主订单和子订单实现相同接口,订单服务无需区分类型,用
calculateTotal(order)即可计算任意订单的总金额 - 递归操作:通过组合模式的树形结构,
cancel()方法会自动递归取消所有子订单,避免编写多层循环逻辑 - 层级透明:客户端可以像操作单个订单一样操作整个订单树,如
printOrder(main2)会自动遍历所有嵌套的子订单 - 灵活扩展:新增“季度订单”(包含多个主订单)只需实现
OrderComponent接口,现有代码无需修改 - 状态聚合:主订单状态通过子订单状态自动聚合,避免手动维护复杂的状态映射关系
五、开源框架中组合模式的运用
以MyBatis的SqlNode为例,说明组合模式在框架级别的应用:
1. 核心实现分析
MyBatis的动态SQL功能(如<if>、<where>、<trim>)通过组合模式实现,将SQL片段组织成树形结构,最终拼接为完整SQL语句。
1.1 SqlNode接口(组件)
SqlNode是所有动态SQL节点的统一接口,定义了“应用SQL片段”的操作:
/**
* SQL节点接口(组件)
* 定义动态SQL片段的统一操作
*/
public interface SqlNode {
/**
* 应用SQL片段到上下文
* @param context 上下文(包含参数等信息)
* @return 是否应用成功
*/
boolean apply(DynamicContext context);
}
1.2 叶子节点(静态SQL片段)
TextSqlNode表示静态文本SQL片段,不可再分:
/**
* 文本SQL节点(叶子)
* 表示静态SQL片段
*/
public class TextSqlNode implements SqlNode {
private final String text; // 静态SQL文本
@Override
public boolean apply(DynamicContext context) {
// 将静态文本添加到上下文
context.appendSql(text);
return true;
}
}
1.3 容器节点(动态SQL组合)
MyBatis提供多种容器节点,组合其他SQL节点:
IfSqlNode:包含一个子节点,根据条件决定是否应用TrimSqlNode:包含多个子节点,对拼接结果进行修剪(如去除多余的AND/OR)MixedSqlNode:包含多个子节点,按顺序应用所有节点
以MixedSqlNode为例:
/**
* 混合SQL节点(容器)
* 包含多个子节点,按顺序应用
*/
public class MixedSqlNode implements SqlNode {
private final List<SqlNode> contents; // 子节点集合
public MixedSqlNode(List<SqlNode> contents) {
this.contents = contents;
}
@Override
public boolean apply(DynamicContext context) {
// 递归应用所有子节点
contents.forEach(node -> node.apply(context));
return true;
}
}
IfSqlNode实现(条件判断容器):
/**
* IfSQL节点(容器)
* 根据条件决定是否应用子节点
*/
public class IfSqlNode implements SqlNode {
private final ExpressionEvaluator evaluator; // 表达式计算器
private final String test; // 条件表达式(如"id != null")
private final SqlNode contents; // 子节点(满足条件时应用)
@Override
public boolean apply(DynamicContext context) {
// 计算条件表达式
if (evaluator.evaluateBoolean(test, context.getBindings())) {
// 条件满足,应用子节点
contents.apply(context);
return true;
}
return false;
}
}
1.4 组合模式在动态SQL中的价值
- 结构清晰:将复杂动态SQL拆分为嵌套的SQL节点,形成树形结构,便于解析
- 操作统一:
apply()方法统一处理所有SQL节点,无论静态还是动态节点 - 灵活扩展:新增动态SQL标签(如自定义
<foreach>)只需实现SqlNode接口 - 递归拼接:通过递归调用
apply()方法,自动拼接所有SQL片段,形成完整语句
六、总结
1. 组合模式的适用场景
- 当系统中存在“部分-整体”的层级关系(如订单与子订单、组织与部门)时
- 当需要对单个对象和组合对象进行统一操作,且不希望区分处理时
- 当需要递归遍历复杂树形结构(如打印、计算总和)时
- 当希望新增节点类型(叶子或容器)不影响现有代码时
2. 组合模式与其他模式的区别
- 与桥接模式:两者都处理层级关系,但组合模式关注“部分-整体”的树形结构,桥接模式关注两个独立维度的分离
- 与装饰器模式:两者都使用递归组合,但装饰器模式用于动态扩展功能,不改变接口;组合模式用于表示层级结构,强调统一操作
- 与迭代器模式:两者常结合使用,组合模式提供树形结构,迭代器模式提供遍历方式,但组合模式本身包含遍历能力
3. 支付系统中的实践价值
- 简化复杂结构操作:将主订单-子订单的多层结构视为统一对象,减少代码复杂度
- 递归批量处理:支持一键取消所有子订单、计算总金额等批量操作,避免多层循环
- 灵活扩展订单类型:新增订单类型(如预售订单)只需实现组件接口,现有逻辑无需修改
- 透明处理嵌套关系:客户端无需感知订单层级深度,统一调用接口即可处理任意复杂订单
- 状态自动聚合:通过递归获取子节点状态,自动聚合主订单状态,减少状态同步逻辑
4. 实践建议
- 识别系统中的“部分-整体”关系,设计合理的组件接口
- 叶子节点应严格限制不可包含子节点(抛出异常),避免误用
- 容器节点的操作应递归应用到所有子节点,确保整体一致性
- 结合迭代器模式实现高效遍历,避免容器节点暴露内部存储结构
- 避免过度设计,简单的层级关系(仅一层)可不用组合模式
组合模式通过统一“部分”与“整体”的接口,简化了复杂树形结构的操作,在支付系统的订单管理、权限控制、账单汇总等场景中具有重要价值。它不仅降低了代码复杂度,还提高了系统的可扩展性,是处理层级结构的理想方案。