用java创建一个简单的规则引擎

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/20763189/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-13 04:09:16  来源:igfitidea点击:

creating a simple rule engine in java

javarule-enginebusiness-rules

提问by Jay

I am exploring different ways to create a simple business rule engine in Java. I need to present the client with a simple webapp that lets him configure a bunch of rules. A sample of rule base might look like this :

我正在探索用 Java 创建简单业务规则引擎的不同方法。我需要向客户端展示一个简单的 web 应用程序,让他配置一堆规则。规则库示例可能如下所示:

Here's example:

下面是例子:

 IF (PATIENT_TYPE = "A" AND ADMISSION_TYPE="O")
 SEND TO OUTPATIENT
 ELSE IF PATIENT_TYPE = "B" 
 SEND TO INPATIENT

The rule engine is pretty simple, the final action could be just one of two actions, sending to inpatient or outpatient. The operators involved in an expression could be =,>,<,!=and logical operators between expressions are AND, OR and NOT.

规则引擎非常简单,最终动作可能只是两个动作之一,发送给住院病人或门诊病人。表达式中涉及的运算符可以是=,>,<,!=,表达式之间的逻辑运算符可以是AND, OR and NOT

I want to build a web application where user will write in a small script in a textarea, and I would evaluate the expression - this way, business rules are explained in simple English and business user has complete control on logic.

我想构建一个 Web 应用程序,用户将在其中编写一个小脚本textarea,然后我将评估表达式 - 这样,业务规则用简单的英语解释,业务用户可以完全控制逻辑。

From the research I did so far, I came across, ANTLRand writing my own scripting language as possible options to solve this problem. I haven't explore options like Drools rules engine, because I have a feeling that it might be an overkill here. Have you had any experience in solving these kind of problems? If yes, how did you go about it?

从我到目前为止所做的研究中,我遇到了,ANTLR并编写自己的脚本语言作为解决此问题的可能选项。我还没有探索像 Drools 规则引擎这样的选项,因为我觉得这在这里可能有点矫枉过正。您有解决此类问题的经验吗?如果是,你是怎么做的?

采纳答案by Roman Vottner

Implementing a simple rule-based evaluation system in Java isn't that hard to achieve. Probably the parser for the expression is the most complicated stuff. The example code below uses a couple of patterns to achieve your desired functionality.

在 Java 中实现一个简单的基于规则的评估系统并不难。表达式的解析器可能是最复杂的东西。下面的示例代码使用几种模式来实现您想要的功能。

A singleton pattern is used to store each available operation in a member map. The operation itself use a command pattern to provide flexible extensibility while the respective action for a valid expression does make use of the dispatching pattern. Last bust not least, a interpreter pattern is used for validating each rule.

单例模式用于将每个可用操作存储在成员映射中。操作本身使用命令模式来提供灵活的可扩展性,而有效表达式的相应操作确实使用了调度模式。最后,同样重要的是,解释器模式用于验证每个规则。

An expression like presented in your example above consists of operations, variables and values. In reference to a wiki-exampleeverything that can be declared is an Expression. The interface therefore looks like this:

上面示例中的表达式由操作、变量和值组成。参考wiki 示例,可以声明的所有内容都是Expression. 因此界面如下所示:

import java.util.Map;

public interface Expression
{
    public boolean interpret(final Map<String, ?> bindings);
}

While the example on the wiki-page returns an int (they implement a calculator), we only need a boolean return value here to decide if a expression should trigger an action if the expression evaluates to true.

虽然 wiki 页面上的示例返回一个 int(他们实现了一个计算器),但我们在这里只需要一个布尔返回值来决定如果表达式的计算结果为 ,则表达式是否应触发操作true

An expression can, as stated above, be either an operation like =, AND, NOT, ... or a Variableor its Value. The definition of a Variableis enlisted below:

如上所述,表达式可以是像=, AND, NOT, ...这样的运算,也可以是aVariable或其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;
    }
}

Validating a variable name does not make that much sense, therefore trueis returned by default. The same holds true for a value of a variable which is kept as generic as possible on defining a BaseTypeonly:

验证变量名没有多大意义,因此true默认返回。这同样适用于在定义 a 时尽可能保持通用的变量值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);
    }
}

The BaseTypeclass contains a factory method to generate concrete value types for a specific Java type.

BaseType类包含一个工厂方法来生成具体的值类型为一个特定的Java类型。

An Operationis now a special expression like AND, NOT, =, ... The abstract base class Operationdoes define a left and right operand as the operand can refer to more than one expression. F.e. NOTprobably only refers to its right-hand expression and negates its validation-result, so trueturn into falseand vice versa. But ANDon the other handside combines a left and right expression logically, forcing both expression to be true on validation.

AnOperation现在是一个特殊的表达式,如AND, NOT, =, ... 抽象基类Operation确实定义了左右操作数,因为操作数可以引用多个表达式。FeNOT可能只引用它的右手表达式并否定它的验证结果,所以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;
     }
}

Two operations probably jump into the eye. int parse(String[], int, Stack<Expression>);refactors the logic of parsing the concrete operation to the respective operation-class as it probably knows best what it needs to instantiate a valid operation. Integer findNextExpression(String[], int, stack);is used to find the right hand side of the operation while parsing the string into an expression. It might sound strange to return an int here instead of an expression but the expression is pushed onto the stack and the return value here just returns the position of the last token used by the created expression. So the int value is used to skip already processed tokens.

两个操作大概会跳入眼帘。int parse(String[], int, Stack<Expression>);将解析具体操作的逻辑重构为相应的操作类,因为它可能最了解实例化有效操作所需的内容。Integer findNextExpression(String[], int, stack);用于在将字符串解析为表达式时查找操作的右侧。在这里返回一个 int 而不是一个表达式可能听起来很奇怪,但该表达式被压入堆栈,这里的返回值只返回创建的表达式使用的最后一个标记的位置。因此 int 值用于跳过已处理的标记。

The ANDoperation does look like this:

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 parseyou probably see that the already generated expression from the left side is taken from the stack, then the right hand side is parsed and again taken from the stack to finally push the new ANDoperation containing both, the left and right hand expression, back onto the stack.

parse您可能看到左侧已经生成的表达式是从堆栈中取出的,然后右侧被解析并再次从堆栈中取出,最后将AND包含左右表达式的新操作推回到堆栈。

NOTis similar in that case but only sets the right hand side as described previously:

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 =operator is used to check the value of a variable if it actually equals a specific value in the bindings map provided as argument in the interpretmethod.

=运算符用于检查变量的值是否实际上等于绑定映射中作为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;
    }
}

As can be seen from the parsemethod a value is assigned to a variable with the variable being on the left side of the =symbol and the value on the right side.

parse方法中可以看出,一个值被分配给一个变量,变量在=符号的左侧,值在右侧。

Moreover the interpretation checks for the availability of the variable name in the variable bindings. If it is not available we know that this term can not evaluate to true so we can skip the evaluation process. If it is present, we extract the information from the right hand side (=Value part) and first check if the class type is equal and if so if the actual variable value matches the binding.

此外,解释检查变量绑定中变量名称的可用性。如果它不可用,我们知道该术语不能评估为真,因此我们可以跳过评估过程。如果存在,我们从右侧(=值部分)提取信息并首先检查类类型是否相等,如果是,则实际变量值是否与绑定匹配。

As the actual parsing of the expressions is refactored into the operations, the actual parser is rather slim:

由于表达式的实际解析被重构为操作,实际的解析器是相当苗条的:

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();
    }
}

Here the copymethod is probably the most interesting thing. As the parsing is rather generic, we do not know in advance which operation is currently processed. On returning a found operation among the registered ones results in a modification of this object. If we only have one operation of that kind in our expression this does not matter - if we however have multiple operations (f.e. two or more equals-operations) the operation is reused and therefore updated with the new value. As this also changes previously created operations of that kind we need to create a new instance of the operation - copy()achieves this.

这里的copy方法可能是最有趣的事情。由于解析相当通用,我们事先不知道当前正在处理哪个操作。在已注册的操作中返回找到的操作会导致对该对象的修改。如果我们的表达式中只有一个这种类型的操作,这无关紧要 - 但是如果我们有多个操作(两个或多个等于操作),则该操作被重用,因此使用新值更新。由于这也改变了以前创建的那种操作,我们需要创建一个新的操作实例 -copy()实现这一点。

Operationsis a container which holds previously registered operations and maps the operation to a specified symbol:

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();
    }
}

Beside the enum singleton pattern nothing really fancy here.

除了枚举单例模式之外,这里没什么特别的。

A Rulenow contains one or more expressions which on evaluation may trigger a certain action. The rule therefore needs to hold the previously parsed expressions and the action which should be triggered in success case.

ARule现在包含一个或多个表达式,这些表达式在评估时可能会触发某个动作。因此,该规则需要保存先前解析的表达式以及在成功情况下应触发的操作。

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;
    }
}

Here a building pattern is used just to be able to add multiple expression if desired for the same action. Furthermore, the Ruledefines a NullActionDispatcherby default. If an expression is evaluated successfully, the dispatcher will trigger a fire()method, which will process the action which should be executed on successful validation. The null pattern is used here to avoid dealing with null values in case no action execution is required as only a trueor falsevalidation should be performed. The interface therefore is simple too:

这里使用构建模式只是为了能够在需要时为同一操作添加多个表达式。此外,默认情况下Rule定义了 a NullActionDispatcher。如果表达式计算成功,调度程序将触发一个fire()方法,该方法将处理成功验证时应执行的操作。此处使用 null 模式是为了避免在不需要执行操作的情况下处理 null 值,因为只应执行atruefalse验证。因此界面也很简单:

public interface ActionDispatcher
{
    public void fire();
}

As I do not really know what your INPATIENTor OUTPATIENTactions should be, the fire()method only triggers a System.out.println(...);method invocation:

由于我真的不知道您的INPATIENTOUTPATIENT操作应该是什么,因此该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");
    }
}

Last but not least, a simple main method to test the behavior of the code:

最后但并非最不重要的是,一个简单的 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);
    }
}

Ruleshere is just a simple container class for rules and propagates the eval(bindings);invocation to each defined rule.

Rules这里只是一个简单的规则容器类,并将eval(bindings);调用传播到每个定义的规则。

I do not include other operations as the post here is already way to long, but it should not be too hard to implement them on your own if you desire so. I furthermore did not include my package structure as you probably will use your own one. Furhtermore, I didn't include any exception handling, I leave that to everyone who is going to copy & paste the code :)

我不包括其他操作,因为这里的帖子已经很长了,但是如果你愿意的话,自己实现它们应该不会太难。此外,我没有包括我的包结构,因为您可能会使用自己的包结构。此外,我没有包括任何异常处理,我把它留给要复制和粘贴代码的每个人:)

One might argue that the parsing should obviously happen in the parser instead of the concrete classes. I'm aware of that, but on the other hand on adding new operations you have to modify the parser as well as the new operation instead of only having to touch one single class.

有人可能会争辩说,解析显然应该发生在解析器中,而不是具体的类中。我知道这一点,但另一方面,在添加新操作时,您必须修改解析器以及新操作,而不是只需要接触一个类。

Instead of using a rule based system a petri net or even a BPMNin combination with the open source Activiti Enginewould be possible to achieve this task. Here the operations are already defined within the language, you only need to define the concrete statements as tasks which can be executed automatically - and depending on the outcome of a task (i.e. the single statement) it will proceed its way through the "graph". The modeling therefore is usually done in a graphical editor or frontend to avoid dealing with the XML nature of the BPMN language.

代替使用基于规则的系统,Petri 网或什至BPMN与开源Activiti 引擎相结合,可以完成此任务。这里的操作已经在语言中定义,您只需要将具体的语句定义为可以自动执行的任务 - 并且根据任务的结果(即单个语句),它将通过“图形”进行. 因此,建模通常在图形编辑器或前端完成,以避免处理 BPMN 语言的 XML 特性。

回答by Sathya

Instead of textArea, provide is as a choice box for fixed state(PATIENT_TYPE) and fixed operators() and you will be done with it. Anyway you control how web app looks like.

代替 textArea,提供作为固定状态(PATIENT_TYPE)和固定操作符()的选择框,您将完成它。无论如何,您可以控制 Web 应用程序的外观。

回答by Pantelis Natsiavas

I would suggest using something like Drools. Creating your own custom solution would be an overkill because you would have to debug it, and still provide functionality certainly less than the one provided by a rule engine like Drools. I understand that Drools has a learning curve, but I would not compare it with creating a custom language, or a custom solution...

我建议使用类似Drools 的东西。创建您自己的自定义解决方案将是一种矫枉过正,因为您必须对其进行调试,而且所提供的功能肯定比 Drools 等规则引擎提供的功能要少。我知道 Drools 有一个学习曲线,但我不会将它与创建自定义语言或自定义解决方案进行比较......

In my opinion, in order for a user to write rules, he/she would have to learn something. While I suppose you could provide for a language simpler than the drools rule language, you would never capture all of his/her needs. Drools rule language would be simple enough for simple rules. Plus, you could provide him/her with a well formed documentation. If you plan to control the rules created by the end user and applied on the system, then perhaps it would be wiser to create a gui that would form the rules applied on drools.

在我看来,为了让用户编写规则,他/她必须学习一些东西。虽然我认为您可以提供一种比drools 规则语言更简单的语言,但您永远无法满足他/她的所有需求。Drools 规则语言对于简单的规则来说已经足够简单了。另外,您可以为他/她提供格式良好的文档。如果您计划控制由最终用户创建并应用于系统的规则,那么创建一个 gui 来形成应用于 drools 的规则可能会更明智。

Hope I helped!

希望我有所帮助!

回答by Andrey Chaschev

A simple rule engine can be build upon closures, i.e in Groovy:

一个简单的规则引擎可以建立在闭包之上,即在 Groovy 中:

def sendToOutPatient = { ... };

def sendToInPatient = { ... };

def patientRule = { PATIENT_TYPE ->
    {'A': sendToOutPatient,
     'B': sendToInPatient}.get(PATIENT_TYPE)
}

static main(){
    (patientRule('A'))()
}

You could define your rules as closures, reuse/reassign them or even build a DSL over them.

您可以将规则定义为闭包,重用/重新分配它们,甚至可以在它们之上构建 DSL。

And Groovy can be easily embedded into Java, example:

而且 Groovy 可以很容易地嵌入到 Java 中,例如:

GroovyShell shell = new GroovyShell(binding);
binding.setVariable("foo", "World");
System.out.println(shell.evaluate("println 'Hello ${foo}!';));

回答by Siva Tumma

This is what I would do. I create a set of regex variables, depending on the matching, I code the business logic. If the rule-set goes complex than this, I would go for apache commons CommandLineParserimplementation on the server.

这就是我要做的。我创建了一组正则表达式变量,根据匹配,我对业务逻辑进行编码。如果规则集比这更复杂,我会去CommandLineParser服务器上的apache commons实现。

But you can use GUI / HTML and a set of dropdowns and sub dropdowns. That way you can make database queries clearly.

但是您可以使用 GUI/HTML 和一组下拉菜单和子下拉菜单。这样您就可以清楚地进行数据库查询。

回答by NeilA

From past experience, the "plain text" rule based solution is a VERY bad idea, it leaves to much room for error, also, as soon as you have to add multiple rules simple or complex, its going to become a nightmare to code/debug/maintain/modify...

根据过去的经验,基于“纯文本”规则的解决方案是一个非常糟糕的主意,它给错误留下了很大的空间,而且,一旦您必须添加多个简单或复杂的规则,它就会成为编码的噩梦/调试/维护/修改...

What I did (and it works exceptionally well) is create strict/concrete classes that extend an abstract rule (1 for each type of rule). Each implementation knows what information it requires and how to process that information to get you desired result.

我所做的(并且效果非常好)是创建扩展抽象规则的严格/具体类(每种规则类型 1 个)。每个实现都知道它需要什么信息以及如何处理这些信息以获得所需的结果。

On the web/front-end side, you will create a component (for each rule implementation) that strictly matches that rule. You could then give the user the option of what rule they would like to use and update the interface accordingly (by page reload/javascript).

在 Web/前端,您将创建一个严格匹配该规则的组件(针对每个规则实现)。然后,您可以让用户选择他们想要使用的规则并相应地更新界面(通过页面重新加载/javascript)。

When the rule gets added/modified iterate over all rule implementations to get corresponding implementation and have that implementation parse the raw data (id recommend using json) from the front-end, then execute that rule.

当规则被添加/修改时,迭代所有规则实现以获得相应的实现,并让该实现从前端解析原始数据(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
  }
}

回答by iwein

You're setting yourself up for failure for two major reasons:

你为失败做准备主要有两个原因:

  1. Parsing free text from the user is HARD.
  2. Writing parsers in Java is somewhat cumbersome
  1. 解析来自用户的自由文本是困难的。
  2. 用Java编写解析器有点麻烦

Solving 1. is either going to push you into the fuzzy domain of NLP, for which you can use a tool like OpenNLP or something from that ecosystem. Because of the large amount of subtly different ways the user can write things down you will find your thinking skew towards a more formal grammar. Making this work will end you up in a DSL type solution, or you'll have to design your own programming language.

解决 1. 要么会将您推入 NLP 的模糊领域,为此您可以使用 OpenNLP 之类的工具或该生态系统中的某些东西。由于用户可以写下大量不同的方式,你会发现你的思维倾向于更正式的语法。完成这项工作将使您最终获得 DSL 类型的解决方案,或者您将不得不设计自己的编程语言。

I've had reasonable results using Scala parser combinators to parse both natural language and more formalised grammars. The problems are the same, but the code you have to write to solve them is more readable.

我使用 Scala 解析器组合器来解析自然语言和更正式的语法,得到了合理的结果。问题是相同的,但是您必须编写来解决这些问题的代码更具可读性。

Bottom line, even if you're thinking of a very simple rule language, you're going to find you underestimate the amount of scenario's you have to test for. NeilA is right to advice you to reduce the complexity by creating a proper UI for each type of rule. Don't try to be too generic, or it will blow up in your face.

最重要的是,即使您正在考虑一种非常简单的规则语言,您也会发现您低估了您必须测试的场景数量。NeilA 建议您通过为每种类型的规则创建适当的 UI 来降低复杂性,这是正确的。不要试图太笼统,否则它会在你的脸上炸开。

回答by Jakub Kubrynski

If you're looking for something lighter than drools but with similar functionality you can check http://smartparam.org/project. It allows storing parameters in properties files as well as in database.

如果您正在寻找比 drools 更轻但具有类似功能的东西,您可以查看http://smartparam.org/项目。它允许在属性文件和数据库中存储参数。

回答by HAL9000

As parsing code with Java only is an implementation suicide, you may want to write a simple compiler using Jflex and CUP, which are the Java version of GNU FLEXand YACC. In this way you can generate simple tokens with Jflex(a token is a keyword like IF, ELSEetc) while CUP will consume those token in order to execute some code.

由于仅使用 Java 解析代码是一种实现自杀,您可能希望使用Jflex 和 CUP编写一个简单的编译器,它们是 GNUFLEXYACC. 通过这种方式,你可以生成简单的令牌Jflex(令牌就像一个关键字IFELSE等等),而中国银联将消耗那些以执行一些代码标记。

回答by flup

Have a good talk with your users, asking them why this needs to be configurable, and what changes in the configuration they expect to be coming up. Find out what upcoming changes are certain, likely, remotely possible, outrageously unlikely. And how quickly they'd need to be implemented. For each change, would writing a small update release be acceptable or not?

与您的用户进行良好的交谈,询问他们为什么需要可配置,以及他们希望在配置中发生哪些变化。找出即将发生的变化是确定的、可能的、极可能的、极不可能的。以及它们需要多快实施。对于每次更改,编写一个小的更新版本是否可以接受?

With this amount of flexibility needed in mind, evaluate the option of rolling your own solution against that of incorporating a full engine. "Test" your simple solution against the upcoming change scenarios by briefly writing down how each change would be implemented. It is quite okay if some unlikely scenarios have big cost. If likely scenarios are costly too, however, you had better pick a more generic solution.

考虑到需要的这种灵活性,评估滚动您自己的解决方案的选项与合并完整引擎的选项。通过简要写下每个更改将如何实施,针对即将到来的更改场景“测试”您的简单解决方案。如果一些不太可能的场景成本很高,那也没关系。但是,如果可能的方案成本也很高,您最好选择更通用的解决方案。

As for the options to consider, I like both drools and the suggestion to write your own. A third option: When implementing a financial registration package with yearly legal updates, we've had quite good success implementing the rules in code but leaving their settings configurable in sql tables. So in your case that might mean a table something like this:

至于要考虑的选项,我喜欢流口水和写自己的建议。第三种选择:在实施具有年度法律更新的财务注册包时,我们在代码中实施规则取得了相当大的成功,但将其设置保留在 sql 表中进行配置。所以在你的情况下,这可能意味着一个像这样的表:

patient_type | admission_type | inpatient_or_outpatient
-------------------------------------------------------
'A'          | 'O'            | 'Outpatient'
'B'          | NULL           | 'Inpatient'

(Our tables tend to have date-from and date-to validity columns which allow the user to stage changes)

(我们的表往往具有允许用户进行更改的 date-from 和 date-to 有效性列)

If you end up writing a DSL, take a look at http://martinfowler.com/books/dsl.htmlwhich offers thorough descriptions of the several approaches. As a caveat: in his Q and A sectionMartin Fowler writes:

如果您最终编写了 DSL,请查看http://martinfowler.com/books/dsl.html,其中提供了几种方法的详尽描述。作为警告:在他的问答部分Martin Fowler 写道:

So is this the hook - business people write the rules themselves?

In general I don't think so. It's a lot of work to make an environment that allows business people to write their own rules. You have to make a comfortable editing tool, debugging tools, testing tools, and so on. You get most of the benefit of business facing DSLs by doing enough to allow business people to be able to read the rules. They can then review them for accuracy, talk about them with the developers and draft changes for developers to implement properly. Getting DSLs to be business readable is far less effort than business writable, but yields most of the benefits. There are times where it's worth making the effort to make the DSLs business-writable, but it's a more advanced goal.

那么这就是问题所在——业务人员自己编写规则吗?

总的来说,我不这么认为。创造一个允许业务人员编写自己的规则的环境需要大量的工作。你必须制作一个舒适的编辑工具、调试工具、测试工具等等。通过让业务人员能够阅读规则,您可以获得面向业务的 DSL 的大部分好处。然后他们可以检查它们的准确性,与开发人员讨论它们并草拟更改以供开发人员正确实施。使 DSL 成为业务可读的比业务可写要少得多,但会产生大部分好处。有时值得努力使 DSL 具有业务可写性,但这是一个更高级的目标。