从规则引擎到生产实践:如何用Spec和Command模式构建高可维护业务逻辑

7次阅读
没有评论

共计 2697 个字符,预计需要花费 7 分钟才能阅读完成。

image.webp

核心痛点:业务规则硬编码之殇

在快速迭代的业务系统中,最令人头疼的莫过于频繁变更的业务规则。以电商促销场景为例:

从规则引擎到生产实践:如何用 Spec 和 Command 模式构建高可维护业务逻辑

  • 硬编码的折扣规则导致每次营销活动都要发布新版本
  • 简单的 ” 满 100 减 20″ 需求变更引发全量回归测试
  • 历史订单因规则版本差异出现计算分歧

通过抽样统计发现,业务规则相关代码的变更频率是核心业务逻辑的 3 - 7 倍(数据来源:笔者参与的零售系统监控数据)。这种现状直接导致两个恶性循环:

  1. 开发人员陷入规则维护泥潭,创新功能开发受阻
  2. 测试资源被重复性验证大量消耗

技术选型:规则架构的决策矩阵

方案 学习成本 动态更新 测试便利性 性能开销 适用场景
规则引擎 (Drools) 支持 金融风控等复杂规则场景
领域专用语言 (DSL) 支持 业务人员参与规则配置
Specification 模式 支持 需要灵活组合的业务规则
Command 模式 支持 需要回滚的操作场景

我们选择 Specification+Command 组合架构的原因:

  1. 正交性解耦 :Spec 处理规则判断,Command 处理规则执行
  2. 运行时组合 :通过 Composite 模式实现 AND/OR/NOT 逻辑运算
  3. 事务支持 :Command 模式天然支持 undo 操作

实现细节:从模式到代码

Specification 接口设计

// 泛型 T 表示待验证的业务对象类型
public interface Specification<T> {boolean isSatisfiedBy(T candidate);

    default Specification<T> and(Specification<T> other) {return new AndSpecification<>(this, other);
    }

    // 类似实现 or、not 方法...
}

// 示例:VIP 用户规则
public class VipSpecification implements Specification<User> {
    @Override
    public boolean isSatisfiedBy(User user) {if(user == null) throw new IllegalArgumentException("用户不能为 null");
        return user.getVipLevel() > 0 
               && user.getExpireDate().after(new Date());
    }
}

组合规则实战

class DiscountSpec:
    def __init__(self, *specs):
        self.specs = specs

    def is_satisfied_by(self, order):
        return all(spec.is_satisfied_by(order) for spec in self.specs)

# 创建组合规则:商品品类 + 库存 + 促销时段
spec = AndSpec(CategorySpec('电子产品'),
    InventorySpec(min_stock=100),
    TimeRangeSpec(start='2023-11-11', end='2023-11-12')
)

类型安全警示
– Java 可使用泛型边界确保组合规则类型一致
– Python 需用 ABC 模块配合 @abstractmethod 进行约束

Command 执行引擎

public interface RuleCommand {void execute(Context ctx);
    void undo(Context ctx);
}

public class DiscountCommand implements RuleCommand {
    private final Specification<Order> spec;
    private final BigDecimal amount;

    @Override
    public void execute(Context ctx) {if(spec.isSatisfiedBy(ctx.getOrder())) {ctx.applyDiscount(amount);
            ctx.logAction(this); // 记录以便 undo
        }
    }

    @Override
    public void undo(Context ctx) {ctx.removeDiscount(amount);
    }
}

生产环境关键考量

性能测试方案

使用 JMeter 进行基准测试时重点关注:

  1. 规则加载性能
  2. 测试 10/100/1000 条规则的初始化耗时
  3. 内存占用监控(特别是组合规则场景)

  4. 执行吞吐量

  5. 单线程与并发模式下的 TPS 对比
  6. 带 GC 日志的压力测试(建议配置:-XX:+PrintGCDetails)

测试环境基准数据
– 4 核 8G 阿里云 ECS
– 100 条组合规则平均执行时间:0.8ms
– 95% 线响应时间:3ms

版本兼容策略

  1. 规则快照:每次变更保存规则 JSON 快照
  2. 版本标记:为订单记录关联规则版本号
  3. 灰度发布:
    if 用户 ID % 100 < 5 → 使用新规则
    else → 使用旧规则 

避坑指南

循环依赖检测

def check_circular_ref(spec, visited=None):
    if visited is None:
        visited = set()

    if id(spec) in visited:
        raise CircularReferenceError(f"规则循环引用: {spec}")

    visited.add(id(spec))

    if isinstance(spec, CompositeSpec):
        for child in spec.children:
            check_circular_ref(child, visited)

    visited.remove(id(spec))

分布式一致性

  1. 规则版本号作为 Redis 缓存 key 组成部分
  2. 通过 ZooKeeper 监听规则变更事件
  3. 本地规则缓存设置合理的 TTL

实践任务:折扣规则组合器

改造前

// 硬编码的折扣逻辑
if(user.isVip() && order.getAmount() > 100) {order.applyDiscount(20);
}

改造后

# 你的任务:实现以下规则组合
# 规则 1:用户是 VIP 或企业客户
# 规则 2:订单金额大于 100 且商品类别是电子产品
# 规则 3:非秒杀商品

class DiscountRuleCombiner:
    def build_spec(self):
        raise NotImplementedError()

参考答案关键点
1. 使用 OrSpec 组合 VIP 和企业客户条件
2. 通过 AndSpec 连接金额和品类条件
3. 用 NotSpec 排除秒杀商品

通过这种架构,我们成功将某电商平台的促销规则发布时间从平均 2 周缩短到 2 小时,回归测试用例减少 68%。更重要的是,业务人员现在可以通过 JSON 配置简单规则组合,真正实现了技术赋能业务。

正文完
 0
评论(没有评论)