组合模式:设计与实践

14 阅读15分钟

组合模式:设计与实践

一、什么是组合模式

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接口,现有代码无需修改
  • 状态聚合:主订单状态通过子订单状态自动聚合,避免手动维护复杂的状态映射关系

五、开源框架中组合模式的运用

MyBatisSqlNode为例,说明组合模式在框架级别的应用:

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. 实践建议

  • 识别系统中的“部分-整体”关系,设计合理的组件接口
  • 叶子节点应严格限制不可包含子节点(抛出异常),避免误用
  • 容器节点的操作应递归应用到所有子节点,确保整体一致性
  • 结合迭代器模式实现高效遍历,避免容器节点暴露内部存储结构
  • 避免过度设计,简单的层级关系(仅一层)可不用组合模式

组合模式通过统一“部分”与“整体”的接口,简化了复杂树形结构的操作,在支付系统的订单管理、权限控制、账单汇总等场景中具有重要价值。它不仅降低了代码复杂度,还提高了系统的可扩展性,是处理层级结构的理想方案。