在Java中实现一个简单的基于规则的评估系统并不难实现。表达式的解析器可能是最复杂的东西。下面的示例代码使用几种模式来实现所需的功能。
单例模式用于将每个可用操作存储在成员映射中。操作本身使用命令模式来提供灵活的可扩展性,而有效表达式的相应操作确实利用了调度模式。最后,同样重要的是,解释器模式用于验证每个规则。
如上面示例中所示的表达式由运算、变量和值组成。在引用 wiki 示例时,所有可以声明的内容都是 .因此,界面如下所示:Expression
import java.util.Map;
public interface Expression
{
public boolean interpret(final Map<String, ?> bindings);
}
虽然 wiki 页面上的示例返回一个 int(它们实现了一个计算器),但我们只需要一个布尔返回值来决定如果表达式的计算结果为 .true
如上所述,表达式可以是类似 、 ...或 a 或其 .下面列出了 a 的定义:=
AND
NOT
Variable
Value
Variable
import java.util.Map;
public class Variable implements Expression
{
private String name;
public Variable(String name)
{
this.name = name;
}
public String getName()
{
return this.name;
}
@Override
public boolean interpret(Map<String, ?> bindings)
{
return true;
}
}
验证变量名称没有多大意义,因此默认情况下返回。这同样适用于变量的值,该值在定义 only 时尽可能保持通用:true
BaseType
import java.util.Map;
public class BaseType<T> implements Expression
{
public T value;
public Class<T> type;
public BaseType(T value, Class<T> type)
{
this.value = value;
this.type = type;
}
public T getValue()
{
return this.value;
}
public Class<T> getType()
{
return this.type;
}
@Override
public boolean interpret(Map<String, ?> bindings)
{
return true;
}
public static BaseType<?> getBaseType(String string)
{
if (string == null)
throw new IllegalArgumentException("The provided string must not be null");
if ("true".equals(string) || "false".equals(string))
return new BaseType<>(Boolean.getBoolean(string), Boolean.class);
else if (string.startsWith("'"))
return new BaseType<>(string, String.class);
else if (string.contains("."))
return new BaseType<>(Float.parseFloat(string), Float.class);
else
return new BaseType<>(Integer.parseInt(string), Integer.class);
}
}
该类包含一个工厂方法,用于为特定的 Java 类型生成具体的值类型。BaseType
An 现在是一个特殊的表达式,如 、 、 、 ...抽象基类确实定义了左操作数和右操作数,因为操作数可以引用多个表达式。断续器 可能只是指它的右手表达式并否定其验证结果,所以变成,反之亦然。但另一方面,在逻辑上组合了左表达式和右表达式,强制两个表达式在验证时都为真。Operation
AND
NOT
=
Operation
NOT
true
false
AND
import java.util.Stack;
public abstract class Operation implements Expression
{
protected String symbol;
protected Expression leftOperand = null;
protected Expression rightOperand = null;
public Operation(String symbol)
{
this.symbol = symbol;
}
public abstract Operation copy();
public String getSymbol()
{
return this.symbol;
}
public abstract int parse(final String[] tokens, final int pos, final Stack<Expression> stack);
protected Integer findNextExpression(String[] tokens, int pos, Stack<Expression> stack)
{
Operations operations = Operations.INSTANCE;
for (int i = pos; i < tokens.length; i++)
{
Operation op = operations.getOperation(tokens[i]);
if (op != null)
{
op = op.copy();
// we found an operation
i = op.parse(tokens, i, stack);
return i;
}
}
return null;
}
}
两个操作可能会跳入眼睛。 重构将具体操作解析为相应操作类的逻辑,因为它可能最了解实例化有效操作所需的内容。 用于在将字符串解析为表达式时查找操作的右侧。此处返回 int 而不是表达式可能听起来很奇怪,但表达式被推送到堆栈上,此处的返回值仅返回所创建表达式使用的最后一个标记的位置。因此,int 值用于跳过已处理的标记。int parse(String[], int, Stack<Expression>);
Integer findNextExpression(String[], int, stack);
该操作如下所示:AND
import java.util.Map;
import java.util.Stack;
public class And extends Operation
{
public And()
{
super("AND");
}
public And copy()
{
return new And();
}
@Override
public int parse(String[] tokens, int pos, Stack<Expression> stack)
{
Expression left = stack.pop();
int i = findNextExpression(tokens, pos+1, stack);
Expression right = stack.pop();
this.leftOperand = left;
this.rightOperand = right;
stack.push(this);
return i;
}
@Override
public boolean interpret(Map<String, ?> bindings)
{
return leftOperand.interpret(bindings) && rightOperand.interpret(bindings);
}
}
您可能会看到,从左侧已经生成的表达式是从堆栈中获取的,然后从堆栈中解析右侧并再次从堆栈中获取,最终将包含左手和右手表达式的新操作推回堆栈。parse
AND
NOT
在这种情况下是相似的,但仅设置右侧,如前所述:
import java.util.Map;
import java.util.Stack;
public class Not extends Operation
{
public Not()
{
super("NOT");
}
public Not copy()
{
return new Not();
}
@Override
public int parse(String[] tokens, int pos, Stack<Expression> stack)
{
int i = findNextExpression(tokens, pos+1, stack);
Expression right = stack.pop();
this.rightOperand = right;
stack.push(this);
return i;
}
@Override
public boolean interpret(final Map<String, ?> bindings)
{
return !this.rightOperand.interpret(bindings);
}
}
该运算符用于检查变量的值(如果它实际上等于方法中作为参数提供的绑定映射中的特定值)。=
interpret
import java.util.Map;
import java.util.Stack;
public class Equals extends Operation
{
public Equals()
{
super("=");
}
@Override
public Equals copy()
{
return new Equals();
}
@Override
public int parse(final String[] tokens, int pos, Stack<Expression> stack)
{
if (pos-1 >= 0 && tokens.length >= pos+1)
{
String var = tokens[pos-1];
this.leftOperand = new Variable(var);
this.rightOperand = BaseType.getBaseType(tokens[pos+1]);
stack.push(this);
return pos+1;
}
throw new IllegalArgumentException("Cannot assign value to variable");
}
@Override
public boolean interpret(Map<String, ?> bindings)
{
Variable v = (Variable)this.leftOperand;
Object obj = bindings.get(v.getName());
if (obj == null)
return false;
BaseType<?> type = (BaseType<?>)this.rightOperand;
if (type.getType().equals(obj.getClass()))
{
if (type.getValue().equals(obj))
return true;
}
return false;
}
}
从该方法中可以看出,值被分配给变量,变量位于符号的左侧,值位于右侧。parse
=
此外,解释检查变量绑定中变量名称的可用性。如果它不可用,我们知道这个术语不能评估为真,所以我们可以跳过评估过程。如果存在,我们从右侧(=值部分)提取信息,并首先检查类类型是否相等,如果是,则实际变量值是否与绑定匹配。
由于表达式的实际解析被重构到操作中,因此实际的解析器相当精简:
import java.util.Stack;
public class ExpressionParser
{
private static final Operations operations = Operations.INSTANCE;
public static Expression fromString(String expr)
{
Stack<Expression> stack = new Stack<>();
String[] tokens = expr.split("\\s");
for (int i=0; i < tokens.length-1; i++)
{
Operation op = operations.getOperation(tokens[i]);
if ( op != null )
{
// create a new instance
op = op.copy();
i = op.parse(tokens, i, stack);
}
}
return stack.pop();
}
}
在这里,方法可能是最有趣的事情。由于解析是相当通用的,我们事先不知道当前正在处理哪个操作。在已注册的操作中返回找到的操作时,会导致对此对象的修改。如果我们的表达式中只有一个这样的操作,这并不重要 - 但是如果我们有多个操作(例如两个或多个相等操作),则该操作将被重用,因此使用新值进行更新。由于这也改变了以前创建的此类操作,我们需要创建该操作的新实例 - 实现这一点。copy
copy()
Operations
是一个容器,它保存以前注册的操作并将操作映射到指定的符号:
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
public enum Operations
{
/** Application of the Singleton pattern using enum **/
INSTANCE;
private final Map<String, Operation> operations = new HashMap<>();
public void registerOperation(Operation op, String symbol)
{
if (!operations.containsKey(symbol))
operations.put(symbol, op);
}
public void registerOperation(Operation op)
{
if (!operations.containsKey(op.getSymbol()))
operations.put(op.getSymbol(), op);
}
public Operation getOperation(String symbol)
{
return this.operations.get(symbol);
}
public Set<String> getDefinedSymbols()
{
return this.operations.keySet();
}
}
除了枚举单例模式之外,这里没有什么真正花哨的。
now 包含一个或多个表达式,这些表达式在评估时可能会触发某个操作。因此,该规则需要保存以前分析的表达式和在成功情况下应触发的操作。Rule
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
public class Rule
{
private List<Expression> expressions;
private ActionDispatcher dispatcher;
public static class Builder
{
private List<Expression> expressions = new ArrayList<>();
private ActionDispatcher dispatcher = new NullActionDispatcher();
public Builder withExpression(Expression expr)
{
expressions.add(expr);
return this;
}
public Builder withDispatcher(ActionDispatcher dispatcher)
{
this.dispatcher = dispatcher;
return this;
}
public Rule build()
{
return new Rule(expressions, dispatcher);
}
}
private Rule(List<Expression> expressions, ActionDispatcher dispatcher)
{
this.expressions = expressions;
this.dispatcher = dispatcher;
}
public boolean eval(Map<String, ?> bindings)
{
boolean eval = false;
for (Expression expression : expressions)
{
eval = expression.interpret(bindings);
if (eval)
dispatcher.fire();
}
return eval;
}
}
这里使用建筑模式只是为了能够为同一操作添加多个表达式。此外,默认情况下,定义了 。如果表达式计算成功,调度程序将触发一个方法,该方法将处理应在成功验证时执行的操作。此处使用 null 模式是为了避免在不需要执行操作的情况下处理 null 值,因为只应执行 或 验证。因此,界面也很简单:Rule
NullActionDispatcher
fire()
true
false
public interface ActionDispatcher
{
public void fire();
}
由于我真的不知道您的或操作应该是什么,因此该方法仅触发方法调用:INPATIENT
OUTPATIENT
fire()
System.out.println(...);
public class InPatientDispatcher implements ActionDispatcher
{
@Override
public void fire()
{
// send patient to in_patient
System.out.println("Send patient to IN");
}
}
最后但并非最不重要的一点是,一个简单的 main 方法来测试代码的行为:
import java.util.HashMap;
import java.util.Map;
public class Main
{
public static void main( String[] args )
{
// create a singleton container for operations
Operations operations = Operations.INSTANCE;
// register new operations with the previously created container
operations.registerOperation(new And());
operations.registerOperation(new Equals());
operations.registerOperation(new Not());
// defines the triggers when a rule should fire
Expression ex3 = ExpressionParser.fromString("PATIENT_TYPE = 'A' AND NOT ADMISSION_TYPE = 'O'");
Expression ex1 = ExpressionParser.fromString("PATIENT_TYPE = 'A' AND ADMISSION_TYPE = 'O'");
Expression ex2 = ExpressionParser.fromString("PATIENT_TYPE = 'B'");
// define the possible actions for rules that fire
ActionDispatcher inPatient = new InPatientDispatcher();
ActionDispatcher outPatient = new OutPatientDispatcher();
// create the rules and link them to the accoridng expression and action
Rule rule1 = new Rule.Builder()
.withExpression(ex1)
.withDispatcher(outPatient)
.build();
Rule rule2 = new Rule.Builder()
.withExpression(ex2)
.withExpression(ex3)
.withDispatcher(inPatient)
.build();
// add all rules to a single container
Rules rules = new Rules();
rules.addRule(rule1);
rules.addRule(rule2);
// for test purpose define a variable binding ...
Map<String, String> bindings = new HashMap<>();
bindings.put("PATIENT_TYPE", "'A'");
bindings.put("ADMISSION_TYPE", "'O'");
// ... and evaluate the defined rules with the specified bindings
boolean triggered = rules.eval(bindings);
System.out.println("Action triggered: "+triggered);
}
}
Rules
这里只是一个简单的规则容器类,并将调用传播到每个定义的规则。eval(bindings);
我不包括其他操作,因为这里的帖子已经很长了,但是如果您愿意,自己实现它们应该不会太难。此外,我没有包括我的包结构,因为您可能会使用自己的包结构。此外,我没有包含任何异常处理,我将其留给每个要复制和粘贴代码的人:)
有人可能会争辩说,解析显然应该发生在解析器中,而不是在具体的类中。我知道这一点,但另一方面,在添加新操作时,您必须修改解析器以及新操作,而不必只触及一个类。
而不是使用基于规则的系统,培养皿网甚至BPMN与开源Activiti引擎相结合,都可以完成这项任务。在这里,操作已经在语言中定义,您只需要将具体语句定义为可以自动执行的任务 - 并且根据任务的结果(即单个语句),它将通过“图形”进行。因此,建模通常在图形编辑器或前端完成,以避免处理BPMN语言的XML性质。