用 Java 实现一个简单的基于规则的评估系统并不难实现。表达式的解析器可能是最复杂的东西。下面的示例代码使用几种模式来实现您所需的功能。
单例模式用于将每个可用操作存储在成员映射中。操作本身使用命令模式来提供灵活的可扩展性,而有效表达式的相应操作则使用调度模式。最后,解释器模式用于验证每个规则。
上面示例中所示的表达式由操作、变量和值组成。参考一个维基示例 http://en.wikipedia.org/wiki/Interpreter_pattern一切可以声明的东西都是Expression
。因此,界面如下所示:
import java.util.Map;
public interface Expression
{
public boolean interpret(final Map<String, ?> bindings);
}
虽然 wiki 页面上的示例返回一个 int (它们实现了一个计算器),但我们在这里只需要一个布尔返回值来决定如果表达式的计算结果为:true
.
如上所述,表达式可以是类似的操作=
, AND
, NOT
, ... 或一个Variable
or its 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;
}
}
验证变量名没有多大意义,因此true
默认返回。对于在定义变量时尽可能保持通用的变量值也是如此。BaseType
only:
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);
}
}
The BaseType
类包含一个工厂方法来为特定的 Java 类型生成具体的值类型。
An Operation
现在是一个特殊的表达方式,例如AND
, NOT
, =
, ... 抽象基类Operation
确实定义了左操作数和右操作数,因为操作数可以引用多个表达式。 F.e.NOT
可能仅引用其右侧表达式并否定其验证结果,因此true
变成false
反之亦然。但AND
另一方面,逻辑上组合左右表达式,强制两个表达式在验证时都为 true。
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 parse(String[], int, Stack<Expression>);
重构将具体操作解析为相应操作类的逻辑,因为它可能最了解实例化有效操作所需的内容。Integer findNextExpression(String[], int, stack);
用于在将字符串解析为表达式时查找操作的右侧。在这里返回 int 而不是表达式可能听起来很奇怪,但是表达式被压入堆栈,并且这里的返回值仅返回创建的表达式使用的最后一个标记的位置。因此 int 值用于跳过已经处理过的标记。
The 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);
}
}
In 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);
}
}
The =
运算符用于检查变量的值是否实际上等于作为参数提供的绑定映射中的特定值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
方法将一个值赋给一个变量,该变量位于该变量的左侧=
符号和右侧的值。
此外,解释检查变量绑定中变量名的可用性。如果它不可用,我们知道该术语无法评估为 true,因此我们可以跳过评估过程。如果存在,我们从右侧(=值部分)提取信息,并首先检查类类型是否相等,如果是,则实际变量值是否与绑定匹配。
由于表达式的实际解析被重构为操作,因此实际的解析器相当精简:
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();
}
}
除了枚举单例模式之外,这里没有什么特别的。
A 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;
}
}
这里使用构建模式只是为了能够在同一操作需要时添加多个表达式。此外,Rule
定义了一个NullActionDispatcher
默认情况下。如果表达式计算成功,调度程序将触发fire()
方法,它将处理成功验证时应执行的操作。这里使用 null 模式是为了避免在不需要执行任何操作的情况下处理 null 值,因为只需一个true
or false
应进行验证。因此,界面也很简单:
public interface ActionDispatcher
{
public void fire();
}
因为我真的不知道你的INPATIENT
or 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);
调用每个定义的规则。
我不包括其他操作,因为这里的帖子已经太长了,但如果您愿意的话,自己实现它们应该不会太难。此外,我没有包含我的包结构,因为您可能会使用自己的包结构。此外,我没有包含任何异常处理,我将其留给每个要复制和粘贴代码的人:)
有人可能会争辩说,解析显然应该在解析器中进行,而不是在具体的类中进行。我知道这一点,但另一方面,在添加新操作时,您必须修改解析器以及新操作,而不必只触及一个类。
不是使用基于规则的系统,而是使用 Petri 网甚至是BPMN http://en.wikipedia.org/wiki/Business_Process_Model_and_Notation与开源结合活动引擎 http://activiti.org/才有可能完成这个任务。这里的操作已经在语言中定义,您只需将具体语句定义为可以自动执行的任务 - 并且根据任务的结果(即单个语句),它将继续通过“图” 。因此,建模通常在图形编辑器或前端中完成,以避免处理 BPMN 语言的 XML 性质。