我正在探索用Java创建简单业务规则引擎的不同方法.我需要向客户端提供一个简单的webapp,让他配置一堆规则.规则库的示例可能如下所示:
这是一个例子:
IF (PATIENT_TYPE = "A" AND ADMISSION_TYPE="O") SEND TO OUTPATIENT ELSE IF PATIENT_TYPE = "B" SEND TO INPATIENT
规则引擎非常简单,最终操作可能只是两个操作中的一个,发送给住院病人或门诊病人.表达式中涉及的运算符可以是=,>,<,!=
表达式之间的逻辑运算符AND, OR and NOT
.
我想构建一个Web应用程序,用户将在一个小脚本中编写textarea
,我会评估表达式 - 这样,业务规则用简单的英语解释,业务用户可以完全控制逻辑.
从我迄今为止所做的研究中,我遇到了,ANTLR
并编写了自己的脚本语言作为解决此问题的可能选项.我没有探索像Drools规则引擎这样的选项,因为我觉得这可能是一种矫枉过正.你有解决这类问题的经验吗?如果是的话,你是怎么做到的?
从过去的经验来看,基于"纯文本"规则的解决方案是一个非常糟糕的想法,它留下了很大的错误空间,一旦你必须添加简单或复杂的多个规则,它将成为代码/的噩梦调试/维护/修改...
我所做的(并且效果非常好)是创建严格/具体的类,扩展抽象规则(每种类型的规则为1).每个实现都知道它需要什么信息以及如何处理该信息以获得所需的结果.
在Web /前端,您将创建一个严格匹配该规则的组件(对于每个规则实现).然后,您可以为用户提供他们想要使用的规则的选项,并相应地更新界面(通过页面重新加载/ javascript).
当规则被添加/修改时,迭代所有规则实现以获得相应的实现并让该实现从前端解析原始数据(id推荐使用json),然后执行该规则.
public abstract class AbstractRule{
public boolean canHandle(JSONObject rawRuleData){
return StringUtils.equals(getClass().getSimpleName(), rawRuleData.getString("ruleClassName"));
}
public abstract void parseRawRuleDataIntoThis(JSONObject rawRuleData); //throw some validation exception
public abstract RuleResult execute();
}
public class InOutPatientRule extends AbstractRule{
private String patientType;
private String admissionType;
public void parseRawRuleDataIntoThis(JSONObject rawRuleData){
this.patientType = rawRuleData.getString("patientType");
this.admissionType= rawRuleData.getString("admissionType");
}
public RuleResultInOutPatientType execute(){
if(StringUtils.equals("A",this.patientType) && StringUtils.equals("O",this.admissionType)){
return //OUTPATIENT
}
return //INPATIENT
}
}
我建议使用像Drools这样的东西.创建自己的自定义解决方案将是一种过度杀伤,因为您必须对其进行调试,并且仍然提供的功能肯定少于Drools等规则引擎提供的功能.我知道Drools有一个学习曲线,但我不会将其与创建自定义语言或自定义解决方案进行比较......
在我看来,为了让用户编写规则,他/她将不得不学习一些东西.虽然我认为你可以提供比drools规则语言更简单的语言,但你永远不会满足他/她的所有需求.Drools规则语言对于简单规则来说足够简单.另外,您可以为他/她提供完善的文档.如果您计划控制最终用户创建并应用于系统的规则,那么创建一个将形成应用于drools的规则的gui可能更明智.
希望我帮忙!
在Java中实现一个简单的基于规则的评估系统并不难实现.表达式的解析器可能是最复杂的东西.下面的示例代码使用了几种模式来实现您所需的功能.
单例模式用于在成员映射中存储每个可用操作.操作本身使用命令模式来提供灵活的可扩展性,而有效表达式的相应操作确实使用了调度模式.最后一个例子,解释器模式用于验证每个规则.
上例中显示的表达式包含操作,变量和值.在参考wiki示例时,可以声明的所有内容都是Expression
.因此界面如下所示:
import java.util.Map; public interface Expression { public boolean interpret(final Map<String, ?> bindings); }
虽然wiki-page上的示例返回一个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>);
重构将具体操作解析为相应操作类的逻辑,因为它可能最清楚地知道实例化有效操作所需的内容.Integer findNextExpression(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
方法可以看出,将值赋给变量,其中变量位于=
符号的左侧,值位于右侧.
此外,解释检查变量绑定中变量名的可用性.如果它不可用,我们知道该术语无法评估为真,因此我们可以跳过评估过程.如果存在,我们从右侧(=值部分)提取信息,首先检查类类型是否相等,如果实际变量值与绑定匹配则如此.
由于表达式的实际解析被重构为操作,实际的解析器相当纤薄:
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(); } }
除了enum singleton模式,这里没有什么真正的花哨.
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
定义a NullActionDispatcher
.如果成功评估表达式,则调度程序将触发一个fire()
方法,该方法将处理应在成功验证时执行的操作.这里使用null模式以避免在不需要动作执行的情况下处理空值,因为只应执行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"); } }
最后但并非最不重要的,一个简单的主要方法来测试代码的行为:
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特性.
如果您正在寻找比drools更轻但具有类似功能的东西,您可以查看http://smartparam.org/project.它允许在属性文件和数据库中存储参数.
你有两个主要原因让自己陷入失败:
解析用户的自由文本是很难的.
用Java编写解析器有点麻烦
解决1.要么将您推入NLP的模糊域,您可以使用OpenNLP等工具或该生态系统中的某些工具.由于用户可以用大量不同的方式写下来,你会发现你的思维倾向于更正式的语法.完成这项工作将使您最终处于DSL类型的解决方案中,或者您必须设计自己的编程语言.
使用Scala解析器组合器解析自然语言和更正式化的语法,我得到了合理的结果.问题是相同的,但是为解决问题而编写的代码更具可读性.
最重要的是,即使你正在考虑一种非常简单的规则语言,你也会发现你低估了你必须测试的场景数量.NeilA建议您通过为每种规则创建适当的UI来降低复杂性,这是正确的.不要试图过于通用,否则它会在你的脸上爆炸.
基本上......不要这样做
要理解为什么看到:
http://thedailywtf.com/Articles/The_Customer-Friendly_System.aspx
http://thedailywtf.com/Articles/The-Mythical-Business-Layer.aspx
http://thedailywtf.com/Articles/Soft_Coding.aspx
我知道这看起来是远方的一个好主意,但业务规则引擎总是最终难以维护,部署和调试它编写的编程语言 - 如果你能提供帮助,不要编写自己的编程语言它.
我个人一直走在前任公司的路上,而且我已经看到它在几年之后的位置(巨大的不可亵渎的脚本坐在一个用直接来自平行维度的语言编写的数据库中,上帝讨厌我们end永远不会满足100%的客户期望,因为它们不像正确的编程语言那样强大,同时它们对于开发人员来说太过于复杂和邪恶(从不介意客户端)).
我知道有一种客户倾向于认为他们不会花费程序员工作时间进行"业务规则调整",并且很少理解他们最终会变得更糟,并吸引这样的客户端你会必须朝着这个方向做点什么 - 但不管你做什么都不会发明自己的东西.
有很多不错的脚本语言带有好的工具(不需要编译,因此可以动态上传等),可以从Java代码中轻松地接口和调用,并利用您实现的Java apis可用,请参阅http://www.slideshare.net/jazzman1980/j-ruby-injavapublic#btnNext例如,Jython也可能,
当客户放弃写这些脚本,你将只剩下保持自己的失败遗产的快乐责任 -确保该遗产是无痛的,因为它可以.