设计模式与规则引擎:解释器模式解析规则、策略模式执行规则、组合模式组合规则
规则引擎,说白了就是让业务规则从代码里「抽」出来。我早年做金融风控系统时,业务方三天两头改规则——「如果交易金额大于5万且用户等级低于VIP,需要人工审核」。每次改规则都要发版,烦得不行。后来我用设计模式搭了一套轻量级规则引擎,才算是解脱了。
今天咱们聊聊三个模式如何联手搞定规则引擎:解释器模式负责解析规则文本,策略模式负责执行具体规则,组合模式负责把规则拼成树状结构。这三个模式配合起来,能让你写出既灵活又易维护的规则系统。
1. 整体架构:三模式如何协作
先看一张整体结构图,心里有个谱:
流程很清晰:规则文本先进来解释器模式,解析成一棵抽象语法树(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. 组合模式:把规则拼成树
你发现没有?上面的 AndExpression 和 OrExpression 其实已经用到了组合模式的思想——它们既是一个表达式,又包含子表达式。这就是组合模式的精髓:单个对象和组合对象有一致的接口。
我画个图你就明白了:
这棵树对应的规则是:(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)) 即可。以后要新增 between、in 等操作符,只需新增策略类,完全不用改解析逻辑。
我的经验:策略模式配合工厂模式使用效果更佳。我在项目中用了一个 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。
说白了,设计模式不是炫技,而是为了应对变化。当你发现业务规则三天两头变的时候,就该考虑用这套组合拳了。