组合模式构建树形验证引擎

拆解成3层意思:

  1. "树形验证引擎"

    把一堆校验规则(比如:库存>0、用户有资格、场次未结束)组织成一棵多叉树。树的每个节点都是一个校验规则,根节点触发整棵树执行。

  2. "采用组合模式构建"

    组合模式 是设计模式的一种,它让"单个对象"和"对象组合"有一致的操作接口。

    在这里,叶子节点 是具体校验(如检查库存),容器节点是组合校验(如"与"关系------所有子校验都通过才通过;"或"关系------任一子校验通过即可)。

  3. "新增规则只需添加叶子节点,符合开闭原则"

    开闭原则(对扩展开放,对修改关闭)意味着:你要加一个新校验(比如"检查用户是否在黑名单"),只需新建一个叶子节点类 ,然后把它挂到树上合适位置,完全不用改动现有的校验节点和调度逻辑

传统写法是用一堆 if-else 顺序执行校验:

java 复制代码
if (库存不足) return 失败;
if (资格不符) return 失败;
if (场次已结束) return 失败;
// 新增黑名单校验 -> 必须改这段代码,违反开闭原则

这种写法的问题:

  • 新增/删除规则要改主流程,容易改出Bug

  • 规则之间无法灵活组合(且/或关系写死)

  • 复用性差,不同场景(下单、秒杀、预约)要复制粘贴

我们以一个购票资格校验为例,验证:库存>0、用户是VIP、场次未满。

第1步:定义统一的校验接口(Component)
java 复制代码
// 抽象节点
public interface Validator {
    boolean validate(ValidateContext context);  // 返回是否通过
}
第2步:实现叶子节点(具体校验规则)
java 复制代码
// 叶子节点1:库存校验
public class StockValidator implements Validator {
    @Override
    public boolean validate(ValidateContext ctx) {
        return ctx.getStock() > 0;
    }
}

// 叶子节点2:VIP资格校验
public class VipValidator implements Validator {
    @Override
    public boolean validate(ValidateContext ctx) {
        return ctx.getUser().isVip();
    }
}

// 叶子节点3:场次校验(新增规则,只需新建这个类)
public class SessionValidator implements Validator {
    @Override
    public boolean validate(ValidateContext ctx) {
        return !ctx.getSession().isFull();
    }
}
第3步:实现容器节点(组合逻辑,支持且/或)
java 复制代码
// 容器节点:支持 "与" 逻辑
public class AndValidator implements Validator {
    private List<Validator> children = new ArrayList<>();

    public AndValidator(Validator... validators) {
        Collections.addAll(children, validators);
    }

    @Override
    public boolean validate(ValidateContext ctx) {
        for (Validator child : children) {
            if (!child.validate(ctx)) return false;
        }
        return true;
    }
}

// 容器节点:支持 "或" 逻辑(同理)
public class OrValidator implements Validator {
    // ... 类似实现,任一true则true
}
第4步:组装成验证树(在配置或启动时完成)
java 复制代码
// 构建验证树: 必须(库存>0 且 场次未满)且 (是VIP 或 是内部员工)
Validator tree = new AndValidator(
    new AndValidator(
        new StockValidator(),
        new SessionValidator()
    ),
    new OrValidator(
        new VipValidator(),
        new InternalEmployeeValidator()   // 这也是新增叶子
    )
);

// 执行
ValidateContext context = new ValidateContext(order);
boolean result = tree.validate(context);

四、如何做到"新增规则只需加叶子节点"?

假设现在要新增一个"黑名单校验":

  1. 新建 BlacklistValidator 实现 Validator 接口

  2. 在组装树的地方,把 new BlacklistValidator() 作为子节点加到 AndValidatorOrValidator

整个过程不需要修改

  • 已有的 Validator 实现类

  • 执行引擎(即 tree.validate() 的调用代码)

  • 容器节点的组合逻辑

这就是开闭原则的体现。


五、进阶增强(实用建议)

增强点 做法
支持动态配置 将树的组装结构放在数据库/配置中心,用JSON描述,运行时动态构建
短路执行 容器节点内部可记录失败原因,提前终止遍历
支持优先级/权重 给节点增加 order 字段,按序执行,失败快速返回
统一异常/错误码 每个Validator返回 ValidationResult 对象(含code和message),不只是boolean
支持嵌套复杂规则 任意组合 And/Or/Not 等逻辑节点,形成任意深度的树

二、完整代码实现(带详细注释)

第1步:定义统一接口

java 复制代码
/**
 * 校验器接口 - 所有校验规则的"根"
 * 
 * 不管是单个校验(如检查库存),还是组合校验(如多个条件同时满足),
 * 都实现这个接口。这样对调用方来说,它们的使用方式完全一样。
 * 
 * 这就是组合模式的核心:让"单个对象"和"组合对象"有一致的操作方式。
 */
public interface Validator {
    
    /**
     * 执行校验
     * 
     * @param context 校验上下文,包含了所有需要的数据(订单、用户、场次等)
     * @return true表示校验通过,false表示校验失败
     * 
     * 举个例子:
     * 库存校验器:从context中取出库存数,判断是否大于0
     * VIP校验器:从context中取出用户信息,判断是否是VIP
     */
    boolean validate(ValidateContext context);
}

第2步:定义上下文对象(承载数据)

java 复制代码
/**
 * 校验上下文 - 校验过程中所有数据的"大袋子"
 * 
 * 为什么要用这个?
 * 因为不同的校验规则需要不同的数据:
 * - 库存校验需要知道商品库存数
 * - VIP校验需要知道用户等级
 * - 场次校验需要知道场次状态
 * 
 * 把所有这些数据都装在一个对象里,传递给所有校验器,
 * 每个校验器只取自己需要的数据就行。
 */
public class ValidateContext {
    
    // 订单相关
    private Long orderId;
    private Long userId;
    private Long productId;
    private Integer buyCount;
    
    // 库存相关(从数据库查出来放到这里)
    private Integer stock;
    
    // 用户相关
    private User user;  // 包含是否VIP、是否内部员工等信息
    
    // 场次相关
    private Session session;  // 包含场次开始时间、是否已满等信息
    
    // 黑名单相关
    private List<Long> blacklist;  // 黑名单用户ID列表
    
    // 构造函数、getter/setter 省略(实际开发用Lombok @Data)
    
    // 为了示例简洁,假设都有对应的getter方法
}

第3步:实现叶子节点(具体校验规则)

java 复制代码
/**
 * 库存校验器 - 叶子节点
 * 
 * 业务含义:检查商品库存是否足够
 * 比如:用户买2张票,库存剩5张,则通过;如果剩1张,则失败
 */
public class StockValidator implements Validator {
    
    @Override
    public boolean validate(ValidateContext context) {
        // 从上下文中取出库存数
        Integer stock = context.getStock();
        // 从上下文中取出用户要买的数量
        Integer buyCount = context.getBuyCount();
        
        // 核心校验逻辑:库存 >= 购买数量
        boolean passed = stock != null && stock >= buyCount;
        
        // 在实际项目中,这里通常会记录日志
        // log.info("库存校验:库存={}, 需要={}, 结果={}", stock, buyCount, passed);
        
        return passed;
    }
}

/**
 * VIP资格校验器 - 叶子节点
 * 
 * 业务含义:检查用户是否是VIP会员
 * 只有VIP会员才能继续(比如某些场次仅限VIP购买)
 */
public class VipValidator implements Validator {
    
    @Override
    public boolean validate(ValidateContext context) {
        User user = context.getUser();
        
        // 核心校验逻辑:用户存在且是VIP
        boolean passed = user != null && user.isVip();
        
        // 可以加更详细的判断:比如VIP是否过期等
        // if (passed && user.getVipExpireTime().before(new Date())) {
        //     passed = false;  // VIP已过期
        // }
        
        return passed;
    }
}

/**
 * 场次校验器 - 叶子节点
 * 
 * 业务含义:检查场次是否还可以购票
 * 比如:场次未结束、场次未满、场次在有效时间段内
 */
public class SessionValidator implements Validator {
    
    @Override
    public boolean validate(ValidateContext context) {
        Session session = context.getSession();
        
        // 核心校验逻辑:场次存在、未结束、未满
        boolean passed = session != null 
                && !session.isEnded()      // 未结束
                && !session.isFull();       // 未满
        
        // 还可以加更多条件,比如:
        // - 场次是否在可购票时间范围内(提前30分钟停止购票)
        // - 场次是否被管理员取消
        
        return passed;
    }
}

/**
 * 黑名单校验器 - 叶子节点(这就是新增的规则)
 * 
 * 业务含义:检查用户是否在黑名单中
 * 黑名单用户不能购买任何商品(比如有恶意刷单记录)
 * 
 * 注意:这是后来新增的校验规则,我们只需要新建这个类,
 * 然后在组装验证树时加上它,完全不用改其他代码!
 */
public class BlacklistValidator implements Validator {
    
    @Override
    public boolean validate(ValidateContext context) {
        Long userId = context.getUserId();
        List<Long> blacklist = context.getBlacklist();
        
        // 核心校验逻辑:用户不在黑名单中
        boolean passed = blacklist == null || !blacklist.contains(userId);
        
        // 如果用户在黑名单,记录一下原因
        if (!passed) {
            // log.warn("用户{}在黑名单中,禁止购买", userId);
        }
        
        return passed;
    }
}

/**
 * 内部员工校验器 - 叶子节点
 * 
 * 业务含义:检查用户是否是内部员工
 * 内部员工有特殊权限,可以享受VIP待遇(和VIP是"或"的关系)
 */
public class InternalEmployeeValidator implements Validator {
    
    @Override
    public boolean validate(ValidateContext context) {
        User user = context.getUser();
        
        // 核心校验逻辑:用户存在且是内部员工
        boolean passed = user != null && user.isInternalEmployee();
        
        return passed;
    }
}

第4步:实现容器节点(组合逻辑)

java 复制代码
/**
 * 与(AND)校验器 - 容器节点
 * 
 * 业务含义:所有子校验都必须通过,整体才算通过
 * 类似于:if (条件A && 条件B && 条件C)
 * 
 * 特点:支持"短路" - 只要有一个子校验失败,立即返回false,不再继续
 * 
 * 使用场景:
 * - 必须先满足库存,再满足场次,再满足资格
 * - 任何一项不满足,直接拒绝
 */
public class AndValidator implements Validator {
    
    /**
     * 子校验器列表 - 所有需要"与"关系连接的校验规则
     * 
     * 比如:[库存校验器, 场次校验器, 黑名单校验器]
     * 表示:库存够 且 场次有效 且 不在黑名单
     */
    private List<Validator> children;
    
    /**
     * 构造函数 - 支持传入多个校验器
     * 
     * 使用示例:
     * new AndValidator(stockValidator, sessionValidator, blacklistValidator)
     * 
     * @param validators 可变参数,可以传入任意多个校验器
     */
    public AndValidator(Validator... validators) {
        this.children = new ArrayList<>();
        // 把所有传入的校验器添加到列表中
        for (Validator v : validators) {
            if (v != null) {
                this.children.add(v);
            }
        }
    }
    
    @Override
    public boolean validate(ValidateContext context) {
        // 遍历所有子校验器
        for (Validator child : children) {
            // 执行子校验
            boolean result = child.validate(context);
            
            // 如果任何一个子校验失败,整体返回false(短路)
            if (!result) {
                // 可以在这里记录是哪个校验失败了,方便排查
                // log.debug("校验失败:{}", child.getClass().getSimpleName());
                return false;
            }
        }
        
        // 所有子校验都通过,整体返回true
        return true;
    }
}

/**
 * 或(OR)校验器 - 容器节点
 * 
 * 业务含义:只要有一个子校验通过,整体就算通过
 * 类似于:if (条件A || 条件B || 条件C)
 * 
 * 使用场景:
 * - VIP会员 或 内部员工,二选一即可
 * - 普通用户 或 特殊权限用户,满足其一就行
 */
public class OrValidator implements Validator {
    
    private List<Validator> children;
    
    public OrValidator(Validator... validators) {
        this.children = new ArrayList<>();
        for (Validator v : validators) {
            if (v != null) {
                this.children.add(v);
            }
        }
    }
    
    @Override
    public boolean validate(ValidateContext context) {
        // 遍历所有子校验器
        for (Validator child : children) {
            // 执行子校验
            boolean result = child.validate(context);
            
            // 只要有一个子校验通过,整体返回true(短路)
            if (result) {
                // 记录是哪个校验通过了
                // log.debug("校验通过:{}", child.getClass().getSimpleName());
                return true;
            }
        }
        
        // 所有子校验都失败,整体返回false
        return false;
    }
}

第5步:组装验证树(核心配置)

java 复制代码
/**
 * 验证引擎构建器 - 负责组装验证树
 * 
 * 这里把前面定义的所有校验规则,按照业务需求组装成一棵树
 * 
 * 组装好的树结构:
 * 
 *                    AndValidator(根节点)
 *                   /                  \
 *          AndValidator              OrValidator
 *         /     |     \             /           \
 *    [库存]  [场次]  [黑名单]   [VIP]      [内部员工]
 * 
 * 业务含义:必须满足 (库存够 且 场次有效 且 不在黑名单) 并且 (VIP 或 内部员工)
 * 
 * 翻译成自然语言就是:
 * "只有VIP或内部员工,在库存充足、场次有效且不在黑名单的情况下,才能购票"
 */
public class ValidationEngineBuilder {
    
    /**
     * 构建验证树
     * 
     * 这个方法通常在系统启动时执行一次,然后把构建好的树缓存起来
     * 
     * @return 验证树的根节点
     */
    public static Validator buildValidationTree() {
        
        // 1. 创建叶子节点(具体校验规则)
        Validator stockValidator = new StockValidator();
        Validator sessionValidator = new SessionValidator();
        Validator blacklistValidator = new BlacklistValidator();  // 新增的规则
        Validator vipValidator = new VipValidator();
        Validator employeeValidator = new InternalEmployeeValidator();
        
        // 2. 组装"与"分支:库存够 且 场次有效 且 不在黑名单
        //    这是硬性条件,必须全部满足
        Validator basicConditions = new AndValidator(
            stockValidator,        // 库存要够
            sessionValidator,      // 场次要有效
            blacklistValidator     // 不能在黑名单(这是新增的)
        );
        // 注意:新增黑名单校验时,只需要在这里加上即可,不需要修改任何Validator类
        
        // 3. 组装"或"分支:VIP 或 内部员工
        //    这是软性条件,满足其一即可
        Validator privilegeConditions = new OrValidator(
            vipValidator,          // VIP会员
            employeeValidator       // 内部员工
        );
        
        // 4. 组装根节点:必须同时满足 基本条件 和 特权条件
        //    AndValidator 会先执行 basicConditions,再执行 privilegeConditions
        //    如果 basicConditions 失败了,就不会执行 privilegeConditions(短路)
        Validator root = new AndValidator(
            basicConditions,       // 基本条件(硬性)
            privilegeConditions    // 特权条件(软性)
        );
        
        return root;
    }
}

第6步:执行校验(调用方代码)

java 复制代码
/**
 * 下单服务 - 使用验证树进行校验
 * 
 * 调用方不需要关心树内部有多复杂,只需要调用 root.validate(context) 即可
 * 就像调用一个简单的校验方法一样,非常简洁
 */
public class OrderService {
    
    // 验证树的根节点(通常通过Spring注入,在启动时构建好)
    private Validator validationTree = ValidationEngineBuilder.buildValidationTree();
    
    /**
     * 用户下单
     * 
     * @param orderRequest 下单请求
     * @return 下单结果(成功或失败+原因)
     */
    public OrderResult placeOrder(OrderRequest request) {
        
        // 1. 准备校验上下文(从数据库查询各种数据)
        ValidateContext context = new ValidateContext();
        context.setOrderId(request.getOrderId());
        context.setUserId(request.getUserId());
        context.setProductId(request.getProductId());
        context.setBuyCount(request.getBuyCount());
        
        // 查询库存(假设从Redis或DB查)
        context.setStock(queryStock(request.getProductId()));
        
        // 查询用户信息(包含VIP、内部员工等)
        context.setUser(queryUser(request.getUserId()));
        
        // 查询场次信息
        context.setSession(querySession(request.getSessionId()));
        
        // 查询黑名单
        context.setBlacklist(queryBlacklist());
        
        // 2. 执行校验(调用验证树)
        boolean isValid = validationTree.validate(context);
        
        // 3. 根据校验结果处理
        if (!isValid) {
            // 校验失败,返回失败信息
            // 注意:实际项目中,每个Validator应该返回更详细的结果(包含错误码和错误信息)
            // 这里为了示例简单,只返回boolean
            return OrderResult.fail("校验失败,请检查资格或库存");
        }
        
        // 4. 校验通过,执行下单逻辑
        // ... 扣库存、生成订单等
        
        return OrderResult.success("下单成功");
    }
    
    // 各种查询方法省略...
}

三、新增规则演示(开闭原则)

假设现在产品经理说:"我们要增加一个校验:用户今日购买次数不能超过3次"

传统做法(坏味道):

java

复制代码
// 在OrderService的placeOrder方法中增加if判断
if (getTodayOrderCount(userId) >= 3) {
    return OrderResult.fail("今日购买次数已达上限");
}

问题:改动了已有方法,可能引入Bug,而且和其他校验逻辑分散在各处。

组合模式做法(优雅):

复制代码
/**
 * 每日限购校验器 - 新增的叶子节点
 * 
 * 业务含义:用户当天购买次数不能超过3次
 * 防止恶意刷单或黄牛抢票
 */
public class DailyLimitValidator implements Validator {
    
    private static final int MAX_DAILY_ORDERS = 3;
    
    @Override
    public boolean validate(ValidateContext context) {
        Long userId = context.getUserId();
        // 查询用户今日已下单数量
        int todayCount = queryTodayOrderCount(userId);
        
        // 判断是否超限
        boolean passed = todayCount < MAX_DAILY_ORDERS;
        
        // if (!passed) {
        //     log.warn("用户{}今日已下单{}次,超过{}次限制", 
        //              userId, todayCount, MAX_DAILY_ORDERS);
        // }
        
        return passed;
    }
    
    private int queryTodayOrderCount(Long userId) {
        // 从数据库查询:SELECT COUNT(*) FROM orders 
        // WHERE user_id = ? AND create_time >= 今天00:00:00
        return 2;  // 示例返回
    }
}

然后只需要修改组装树的地方

复制代码
public static Validator buildValidationTree() {
    // ... 原来的代码 ...
    
    // 新增每日限购校验
    Validator dailyLimitValidator = new DailyLimitValidator();
    
    // 将每日限购加入基本条件(与库存、场次等并列)
    Validator basicConditions = new AndValidator(
        stockValidator,
        sessionValidator,
        blacklistValidator,
        dailyLimitValidator  // ← 新增这一行即可
    );
    
    // ... 其他代码不变 ...
}

整个过程

  • ✅ 没有修改任何已有类(StockValidator、SessionValidator等)

  • ✅ 没有修改OrderService的代码

  • ✅ 没有修改AndValidator、OrValidator的代码

  • ✅ 只需新建1个类 + 修改1处组装配置

这就是开闭原则的完美体现!