设计模式与规则引擎:解释器模式解析规则、策略模式执行规则、组合模式组合规则

规则引擎,说白了就是让业务规则从代码里「抽」出来。我早年做金融风控系统时,业务方三天两头改规则——「如果交易金额大于5万且用户等级低于VIP,需要人工审核」。每次改规则都要发版,烦得不行。后来我用设计模式搭了一套轻量级规则引擎,才算是解脱了。

今天咱们聊聊三个模式如何联手搞定规则引擎:解释器模式负责解析规则文本,策略模式负责执行具体规则,组合模式负责把规则拼成树状结构。这三个模式配合起来,能让你写出既灵活又易维护的规则系统。

1. 整体架构:三模式如何协作

先看一张整体结构图,心里有个谱:

规则引擎三模式协作架构 规则文本(字符串) 解释器模式 解析规则文本 → 生成抽象语法树(AST) 组合模式 组合规则节点 → 树状规则结构 策略模式 执行叶子节点规则逻辑

流程很清晰:规则文本先进来解释器模式,解析成一棵抽象语法树(AST)。这棵树由组合模式管理——叶子节点是原子规则,非叶子节点是 AND/OR 等组合逻辑。最后,策略模式负责执行每个叶子节点的具体判断逻辑。

核心思想:解释器负责「读」,组合负责「拼」,策略负责「算」。各司其职,互不干扰。

2. 解释器模式:把规则文本变成树

解释器模式,我习惯叫它「小编译器」。它把一段规则文本解析成抽象语法树,每个节点都是一个表达式。

举个例子,规则文本:amount > 50000 AND level == "VIP"

解析后变成这样的树:

        AND
       /   \
  > 50000   == "VIP"
   /          \
amount        level

每个节点都是一个表达式对象,实现了统一的 interpret(context) 接口。

2.1 抽象表达式接口

// Java
public interface Expression {
    boolean interpret(Map<String, Object> context);
}
// C++
class Expression {
public:
    virtual ~Expression() = default;
    virtual bool interpret(const std::map<std::string, std::any>& context) = 0;
};

2.2 终结符表达式(叶子节点)

比如比较表达式:

// Java
public class ComparisonExpression implements Expression {
    private String field;
    private String operator;
    private Object value;

    public ComparisonExpression(String field, String operator, Object value) {
        this.field = field;
        this.operator = operator;
        this.value = value;
    }

    @Override
    public boolean interpret(Map<String, Object> context) {
        Object actual = context.get(field);
        if (actual == null) return false;
        
        switch (operator) {
            case ">": return ((Number) actual).doubleValue() > ((Number) value).doubleValue();
            case "==": return actual.equals(value);
            case "contains": return actual.toString().contains(value.toString());
            default: throw new IllegalArgumentException("Unknown operator: " + operator);
        }
    }
}
// C++
class ComparisonExpression : public Expression {
private:
    std::string field;
    std::string op;
    std::any value;
public:
    ComparisonExpression(std::string f, std::string o, std::any v)
        : field(f), op(o), value(v) {}

    bool interpret(const std::map<std::string, std::any>& context) override {
        auto it = context.find(field);
        if (it == context.end()) return false;
        
        if (op == ">") {
            double a = std::any_cast<double>(it->second);
            double b = std::any_cast<double>(value);
            return a > b;
        }
        if (op == "==") {
            return it->second == value;
        }
        return false;
    }
};

2.3 非终结符表达式(组合节点)

// Java
public class AndExpression implements Expression {
    private Expression left;
    private Expression right;

    public AndExpression(Expression left, Expression right) {
        this.left = left;
        this.right = right;
    }

    @Override
    public boolean interpret(Map<String, Object> context) {
        return left.interpret(context) && right.interpret(context);
    }
}

public class OrExpression implements Expression {
    private Expression left;
    private Expression right;

    public OrExpression(Expression left, Expression right) {
        this.left = left;
        this.right = right;
    }

    @Override
    public boolean interpret(Map<String, Object> context) {
        return left.interpret(context) || right.interpret(context);
    }
}

避坑指南:我曾经在解析规则时直接用字符串拼接生成代码,然后动态编译。结果线上出了几次内存泄漏,排查了一整天才发现是动态类加载没卸载。后来老老实实用解释器模式,虽然代码多了几行,但稳定多了。

3. 组合模式:把规则拼成树

你发现没有?上面的 AndExpressionOrExpression 其实已经用到了组合模式的思想——它们既是一个表达式,又包含子表达式。这就是组合模式的精髓:单个对象和组合对象有一致的接口

我画个图你就明白了:

组合模式规则树示例 AND OR amount > 10000 level == "VIP" risk < 0.3

这棵树对应的规则是:(level == "VIP" OR risk < 0.3) AND amount > 10000

组合模式让树的构建变得非常自然:

// Java
Expression rule = new AndExpression(
    new OrExpression(
        new ComparisonExpression("level", "==", "VIP"),
        new ComparisonExpression("risk", "<", 0.3)
    ),
    new ComparisonExpression("amount", ">", 10000)
);

关键点:组合模式让客户端代码无需区分「单个规则」和「组合规则」。你调用 interpret() 时,根本不用管它是叶子节点还是分支节点——这就是透明性。

4. 策略模式:让叶子规则可插拔

解释器模式解析出了树,组合模式管理了树的结构,那叶子节点里的具体判断逻辑呢?比如 amount > 50000 这个比较,如果以后要改成 amount >= 50000 或者 amount > 50000 AND currency == "USD",怎么办?

这时候策略模式就派上用场了。我把每个叶子节点的判断逻辑封装成一个策略,运行时可以自由切换。

4.1 策略接口

// Java
@FunctionalInterface
public interface RuleStrategy {
    boolean evaluate(Map<String, Object> context);
}
// C++
class RuleStrategy {
public:
    virtual ~RuleStrategy() = default;
    virtual bool evaluate(const std::map<std::string, std::any>& context) = 0;
};

4.2 具体策略

// Java
public class GreaterThanStrategy implements RuleStrategy {
    private String field;
    private double threshold;

    public GreaterThanStrategy(String field, double threshold) {
        this.field = field;
        this.threshold = threshold;
    }

    @Override
    public boolean evaluate(Map<String, Object> context) {
        Object value = context.get(field);
        if (value instanceof Number) {
            return ((Number) value).doubleValue() > threshold;
        }
        return false;
    }
}

public class EqualsStrategy implements RuleStrategy {
    private String field;
    private Object expected;

    public EqualsStrategy(String field, Object expected) {
        this.field = field;
        this.expected = expected;
    }

    @Override
    public boolean evaluate(Map<String, Object> context) {
        Object actual = context.get(field);
        return actual != null && actual.equals(expected);
    }
}

4.3 策略与解释器结合

在解释器模式的叶子节点中,持有策略对象:

// Java
public class StrategyExpression implements Expression {
    private RuleStrategy strategy;

    public StrategyExpression(RuleStrategy strategy) {
        this.strategy = strategy;
    }

    @Override
    public boolean interpret(Map<String, Object> context) {
        return strategy.evaluate(context);
    }
}

这样,解析器在解析 amount > 50000 时,只需要创建 new StrategyExpression(new GreaterThanStrategy("amount", 50000)) 即可。以后要新增 betweenin 等操作符,只需新增策略类,完全不用改解析逻辑。

我的经验:策略模式配合工厂模式使用效果更佳。我在项目中用了一个 StrategyFactory,根据操作符字符串(如 ">"、"=="、"contains")自动创建对应的策略对象。这样解析器只需要关注语法结构,策略的创建完全解耦。

5. 完整示例:一个简单的风控规则引擎

把三个模式串起来,写一个完整的 demo:

// Java 完整示例
public class RuleEngine {
    // 解析规则文本,构建AST
    public Expression parse(String ruleText) {
        // 简化版解析器,实际可用ANTLR等工具
        // 这里假设规则文本格式固定
        if (ruleText.contains("AND")) {
            String[] parts = ruleText.split("AND");
            Expression left = parse(parts[0].trim());
            Expression right = parse(parts[1].trim());
            return new AndExpression(left, right);
        }
        if (ruleText.contains("OR")) {
            String[] parts = ruleText.split("OR");
            Expression left = parse(parts[0].trim());
            Expression right = parse(parts[1].trim());
            return new OrExpression(left, right);
        }
        // 原子规则:amount > 50000
        String[] tokens = ruleText.split(" ");
        String field = tokens[0];
        String op = tokens[1];
        Object value = parseValue(tokens[2]);
        RuleStrategy strategy = StrategyFactory.create(op, field, value);
        return new StrategyExpression(strategy);
    }

    public boolean evaluate(Expression rule, Map<String, Object> context) {
        return rule.interpret(context);
    }
}

// 使用
RuleEngine engine = new RuleEngine();
Expression rule = engine.parse("level == VIP OR (amount > 50000 AND risk < 0.3)");

Map<String, Object> context = new HashMap<>();
context.put("level", "VIP");
context.put("amount", 60000);
context.put("risk", 0.2);

boolean result = engine.evaluate(rule, context); // true

注意:实际生产环境中,规则文本的解析建议用 ANTLR 或 JavaCC 等成熟的解析器生成工具。手写解析器容易出错,而且难以处理复杂的嵌套和优先级。我早期手写过一次,被各种括号优先级搞到崩溃,后来老老实实用 ANTLR 了。

6. 三种模式的对比与总结

模式 职责 典型场景 我的建议
解释器模式 解析规则文本,生成AST 规则字符串 → 表达式树 适合规则语法固定、变化少的场景
组合模式 管理树状结构,统一接口 AND/OR 组合、嵌套规则 天然适合规则树的构建
策略模式 封装叶子节点的判断逻辑 比较操作、集合判断、自定义函数 让规则逻辑可扩展、可替换

这三个模式配合起来,能搭建一个非常灵活的规则引擎。解释器模式负责「翻译」规则文本,组合模式负责「组织」规则结构,策略模式负责「执行」具体逻辑。各层职责清晰,改动一个不影响其他。

我记得有一次业务方要求新增一个「交易时间在9:00-17:00之间」的规则。我只加了一个 TimeBetweenStrategy 类,然后在解析器里加了一个 between 操作符的映射,前后不到半小时就搞定了。要是没有这套设计,估计又得改一堆 if-else。

说白了,设计模式不是炫技,而是为了应对变化。当你发现业务规则三天两头变的时候,就该考虑用这套组合拳了。


公众号:蓝海资料掘金营,微信deep3321