creating a simple rule engine in java

人走茶凉 提交于 2019-11-27 10:20:17

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.

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-example everything that can be declared is an Expression. The interface therefore looks like this:

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.

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

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 true is returned by default. The same holds true for a value of a variable which is kept as generic as possible on defining a 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 class contains a factory method to generate concrete value types for a specific Java type.

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

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.

The AND operation does look like this:

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 you 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 AND operation containing both, the left and right hand expression, back onto the stack.

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

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 interpret method.

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 parse method 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.

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 copy method 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.

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

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 Rule now 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.

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 Rule defines a NullActionDispatcher by 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 true or false validation should be performed. The interface therefore is simple too:

public interface ActionDispatcher
{
    public void fire();
}

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

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:

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 here is just a simple container class for rules and propagates the eval(bindings); invocation to each defined rule.

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 BPMN in combination with the open source Activiti Engine would 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.

Basically... Don't do it

To understand why see:

  1. http://thedailywtf.com/Articles/The_Customer-Friendly_System.aspx
  2. http://thedailywtf.com/Articles/The-Mythical-Business-Layer.aspx
  3. http://thedailywtf.com/Articles/Soft_Coding.aspx

I know it looks like a great idea from afar, but the business rules engine will invariably end up being harder to maintain, deploy and debug then the programming language it was written in - don't make up your own programming languages if you can help it.

I've personally been down that road in an ex firm and I've seen where it goes after a couple years (giant undebuggable scripts sitting in a database written in a language that came straight from a parallel dimension where God hates us that in the end never meet 100% of customer expectation because they're not as powerful as a proper programming language and at the same time they're far too convoluted and evil for devs to handle (never mind the client)).

I know there's a certain kind of client that's enamoured with the idea that they won't pay programmer hours for "business rule adaptations" and little understand that they'll be worse off in the end and to attract this kind of client you'll have to make something in this direction - but whatever you do don't invent something of your own.

There's a plethora of decent scripting languages that come with good tools (that don't require compilation, so can be uploaded dynamically etc) out there that can be slickly interfaced and called from Java code and take advantage of your implemented Java apis that you make available, see http://www.slideshare.net/jazzman1980/j-ruby-injavapublic#btnNext for example, Jython possibly too,

and when the client gives up writing these scripts you will be left with the happy duty of maintaining his failed legacy - make sure that that legacy is as painless as it can be.

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...

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.

Hope I helped!

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.

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).

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.

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

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

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.

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.

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.

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.

Rather than build your own rules engine, you might want to consider the Open Source N-CUBE engine, an Open Source Java rules engine that uses Groovy as the Domain Specific Language (DSL).

It is a sequential rules engine as opposed to a non-sequential rules engine like a RETE-based rules engine. The benefit of a sequential rules engine is that it is much easy to debug the rules. Trying to decipher inferences from really large rule sets can be very difficult, but with a sequential rule engine like N-CUBE, tracing the rules is very similar to following sequential 'code logic'.

N-CUBE has built-in support for both Decision Tables and Decision Trees. The Decision Tables and Trees within N-CUBE allow data or code to execute within the cells, very much like a multi-dimensional Excel. The 'macro' language (DSL) is Groovy. When writing code within a cell, you do not need to define a package statement, imports, a class name, or function - all of this is added for you, making the DSL code snippets easy to read / write.

This rule engine is available on GitHub at https://github.com/jdereg/n-cube.

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.

A simple rule engine can be build upon closures, i.e in 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.

And Groovy can be easily embedded into Java, example:

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

There's a rule engine for Clojure called Clara, which can be used from java as well as Clojure[Java]Script. I think it would be quite easy to create something usable from that.

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 CommandLineParser implementation on the server.

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

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 FLEX and YACC. In this way you can generate simple tokens with Jflex (a token is a keyword like IF, ELSE etc) while CUP will consume those token in order to execute some code.

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:

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)

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

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.

Implementing a rule engine is not trivial. A meaningful rule based system has an inference engine that supports both forward chaining and backward chaining, as well as breadth first and depth first search strategies. Easy Rules has none of this, it just executes all rules once and only once. Drools supports forward- and backward chaining, and afaik also supports depth first and breadth first. It's explained here.

From my experience, Drools is the only meaningful Rule Engine for java. It does have its limitations. I must say, I have used Drools 5+ years ago.

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!