Skip to content
Mahmoud Ben Hassine edited this page Dec 6, 2020 · 18 revisions

1. Why is Easy Rules called the "The stupid Java rules engine"?

The goal of Easy Rules is to provide a lightweight rules engine without the features that the majority of applications do not need. The term "stupid" is actually the perfect term to describe how the default rules engine works: It iterates over a set of ordered rules and execute them when their conditions are met. This what makes it easy to learn and use following the KISS principle.

Easy Rules also provides an inference rules engine. This engine is less stupid than the default one: Given a set of facts, it continuously selects and applies rules until no more rules are applicable. In pseudo code, it does something like the following:

Set<Rule> candidateRules;
do {
   candidateRules = selectCandidates(rules, facts);
   file(candidateRules);
} while (! candidateRules.isEmpty());

You can find an example of how to use it in the air conditioning tutorial.

2. I would like to return a value upon a rule execution, how to do that?

By design, rules do not return values. A rule's action is not a function (that returns a value), it should be considered as an action that has a side effect, for example, adding a new fact to the set of known facts. This new fact might drive the execution flow of subsequent rules. This is how production systems are designed to work and Easy Rules is no different. Here is an example of a rule that adds the result of its action to the set of known facts:

@Rule(name = "my rule")
public class MyRule {

    //@Condition
    public boolean when() {
        return true;
    }

    //@Action
    public void then(Facts facts) {
        result = ..
        facts.add("myRuleResult", result)
    }

}

The result of the rule may or may not be the trigger of next rules in the flow. But you can always make your rule return a result after execution. Here is an example:

@Rule(name = "my rule")
public class MyRule<T> {

    private boolean executed;

    private T result;

    //@Condition
    public boolean when() {
        return true;
    }

    //@Action
    public void then() throws MyException {
        try {
            System.out.println("my rule has been executed");
            result = null; // assign your result here
            executed = true;
        } catch (MyException e) {
            // executed flag will remain false if an exception occurs
            throw e;
        }
    }

    public boolean isExecuted() {
        return executed;
    }

    public T getResult() {
        return result;
    }

}

This rule will return a result if it is successfully executed. After firing rules, you query the executed flag on your rule instance and get the execution result.

3. I've registered multiple instances of the same rule with different inputs, but it seems only the first instance is registered. What's happening?

Rules have unique names within a rules set of type Rules. If you register multiple instances of the same rule, only the first instance will be considered. Other instances will be ignored since they have the same name.

4. Is Easy Rules usable with Android?

Yes. Easy Rules has been made Android compatible since version 1.3

5. Can I use Easy Rules in a web application?

Yes. Easy Rules is very lightweight and can be used both in a standalone application or embedded in an application server, a servlet container or a dependency injection container.

You can find an example of how to use Easy Rules in a web application here.

6. How to deal with thread safety?

Starting from v3, rules in Easy Rules are stateless, so they are thread safe. The rules engine is also thread safe and can be shared between threads. However, a Facts object is not thread safe, each thread should have its own set of data to work on.

CompositeRule and its sub-classes are not thread-safe.

7. Why there is no else support in Easy Rules?

Easy Rules was designed as an implementation of a production system (and inspired by this article). In these systems, there is no else statement. The whole point is to be able to design the logic as a number of conditions/actions and let the engine decide which rule to apply given the set of facts. Rules are not meant to replace just any if/then/else logic, it's a different kind of thinking.

However, production systems have always been able to cover any complex if/then/else (nested) statements by using multiple productions. This is no different in Easy Rules where it is possible to implement an if/then/else logic using two rules: one for the condition and another one for the negation of the condition.

Here are some resources on that topic:

  • "Productions consist of two parts: a sensory precondition (or "IF" statement) and an action (or "THEN")" [ source ]
  • "These types of rules are used to represent behaviors of the type IF condition THEN action." [ source ]
  • "Organize logic through a set of production rules, each having a condition and an action." [ source ]

8. How to add/assign new facts in rules written in an expression language?

This is not possible by design. Starting from v4, facts are null-safe (#294) and type-safe (#276), meaning every fact must be a non-null typed value. If you try to add/assign a new fact in an action defined in an EL, Easy Rules won't be able to dynamically determine the type of the fact to create at runtime. The reason for this design choice is to avoid the many type-safety issues that might arise if Easy Rules adds untyped facts at runtime. For more details about the matter, please refer to issue #280.

If you really want to add facts at runtime from EL expressions, you can add the map of facts as a fact and use it in conditions/actions:

import org.jeasy.rules.api.Facts;
import org.jeasy.rules.api.Rule;
import org.jeasy.rules.api.RuleListener;
import org.jeasy.rules.api.Rules;
import org.jeasy.rules.core.DefaultRulesEngine;
import org.jeasy.rules.mvel.MVELRule;

public class Launcher {

    public static void main(String[] args) {
        Facts facts = new Facts();
        Rules rules = new Rules();
        MVELRule x = new MVELRule()
                .when("true")
                .then("facts.put(\"result\", \"foobar\");");
        rules.register(x);

        DefaultRulesEngine rulesEngine = new DefaultRulesEngine();
        rulesEngine.registerRuleListener(new RuleListener() {
            @Override
            public void beforeExecute(Rule rule, Facts facts) {
                facts.put("facts", facts);
            }

            @Override
            public void onSuccess(Rule rule, Facts facts) {
                facts.remove("facts");
            }

            @Override
            public void onFailure(Rule rule, Facts facts, Exception exception) {
                facts.remove("facts");
            }
        });
        rulesEngine.fire(rules, facts);

        System.out.println("facts = " + facts); // prints: facts = [Fact{name='result', value=foobar}]
    }
}

It is a good practice to define the facts that rules will operate on upfront. In this case, a holder class (ie a wrapper object) can be created to encapsulate facts and used in EL expressions:

class Holder {
   in x;
   long y;
   // other variables with correct types
}

Facts facts = new Facts();
Holder holder = new Holder();
// initialize holder if needed
facts.put("h", holder); // Then use `h.` prefix in your EL expressions.

If you don't want to use a custom wrapper type, you can always use wrapper types from the java.util.concurrent package which provide methods to get/set/increment/decrement numeric values. Here is a quick example of the air conditioning tutorial:

import java.util.concurrent.atomic.AtomicInteger;

import org.jeasy.rules.api.Facts;
import org.jeasy.rules.api.Rule;
import org.jeasy.rules.api.Rules;
import org.jeasy.rules.api.RulesEngine;
import org.jeasy.rules.core.InferenceRulesEngine;
import org.jeasy.rules.mvel.MVELRule;

public class Launcher {

    public static void main(String[] args) {
        // define facts
        Facts facts = new Facts();
        facts.put("temperature", new AtomicInteger(30));

        // define rules
        Rule airConditioningRule = new MVELRule()
                .name("air conditioning rule")
                .when("temperature.get() > 25")
                .then("temperature.decrementAndGet()");
        Rules rules = new Rules();
        rules.register(airConditioningRule);

        // fire rules on known facts
        RulesEngine rulesEngine = new InferenceRulesEngine();
        rulesEngine.fire(rules, facts);
    }

}

9. I have another question, what should I do?

For any further question, you can open an issue on the issue tracker.