用Java实现一个简单的基于规则的评估系统并不难。表达式的解析器可能是最复杂的东西。下面的示例代码使用两种模式来实现所需的功能。
单例模式用于将每个可用操作存储在成员映射中。该操作本身使用命令模式来提供灵活的可扩展性,而有效表达式的相应操作确实使用了调度模式。最后一次失败是,解释器模式用于验证每个规则。
上面示例中显示的表达式由操作,变量和值组成。以Wiki为例,所有可以声明的内容都是一个
expression。因此,界面如下所示:
import java.util.Map;public interface expression{ public boolean interpret(final Map<String, ?> bindings);}虽然Wiki页面上的示例返回一个int(它们实现了一个计算器),但是我们在这里仅需要一个布尔返回值来确定如果表达式的计算结果为,则该表达式是否应该触发动作
true。
一个表达式可以,如上所述,是为操作等
=,
AND,
NOT,…或
Variable或它的
Value。a的定义
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:
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); }}本
baseType类包含一个工厂方法来生成具体的值类型为一个特定的Java类型。
一个
Operation现在就像是一个特殊的表情
AND,
NOT,
=,…抽象基类
Operation并定义一个左,右操作作为操作可以参考以上的表达。Fe
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 parse(String[], int,Stack<expression>);重构将具体操作解析为相应操作类的逻辑,因为它可能最清楚地实例化了一个有效操作所需的内容。
IntegerfindNextexpression(String[], int,stack);用于在将字符串解析为表达式时查找操作的右侧。在这里返回一个int而不是一个表达式听起来很奇怪,但是该表达式被压入堆栈,并且这里的返回值仅返回所创建的表达式使用的最后一个标记的位置。因此,int值用于跳过已处理的令牌。
该
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方法可以看出,将值分配给变量,该变量在
=符号的左侧,而值在右侧。
此外,解释会检查变量绑定中变量名称的可用性。如果不可用,我们知道此术语无法评估为真,因此我们可以跳过评估过程。如果存在,我们从右侧(=
Value部分)提取信息,并首先检查类类型是否相等,如果是,则实际变量值是否与绑定匹配。
由于将表达式的实际解析重构到操作中,因此实际的解析器非常苗条:
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方法可能是最有趣的事情。由于解析是相当通用的,因此我们无法预先知道当前正在处理哪个操作。返回已注册操作中找到的操作后,将导致此对象的修改。如果表达式中只有一个这样的操作,那就没关系-
但是,如果我们有多个操作(例如两个或多个equals-
operations),则该操作将被重用,并因此使用新值进行更新。由于这也会更改以前创建的此类操作,因此我们需要创建该操作的新实例-
copy()实现此目的。
Operations是一个容器,用于保存先前注册的操作并将该操作映射到指定的符号:
import java.util.HashMap;import java.util.Map;import java.util.Set;public enum Operations{ 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(); }}除了枚举单例模式之外,这里什么都没有。
一个
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值,因为仅应执行a
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);调用传播到每个定义的规则。
我不包括其他操作,因为这里的帖子已经很长了,但是如果您愿意的话,自己实施它们也不会太难。此外,我没有包括我的包结构,因为您可能会使用自己的包结构。此外,我不包含任何异常处理,我将其留给将要复制和粘贴代码的所有人:)
可能有人争辩说,解析显然应该在解析器中进行,而不是在具体类中进行。我知道这一点,但另一方面,在添加新操作时,您必须修改解析器以及新操作,而不必只涉及一个类。
代替使用基于规则的系统,可以将Petri网甚至BPMN与开源Activiti
Engine结合使用来实现此任务。这里的操作已经在语言中定义了,您只需要将具体的语句定义为可以自动执行的任务-
根据任务的结果(即单个语句),它将通过“图形”继续进行。因此,建模通常是在图形编辑器或前端中完成的,以避免处理BPMN语言的XML性质。



