A Really Simple But Powerful Rule Engine
Join the DZone community and get the full member experience.
Join For FreeI have the requirement to use a rule engine. I want something light weight and fairly simple, yet powerful. While there are products out there which are super good, I don't want something with the big learning overhead. And, I fancied writing my own!
Here are my few basic requirements:
- Use some kind of expression language to write the rules,
- It should be possible to store rules in a database,
- Rules need a priority, so that only the best can be fired,
- It should also be possible to fire all matching rules,
- Rules should evaluate against one input which can be an object like a tree, containing all the information which rules need to evaluate against
- Predefined actions which are programmed in the system should be executed when certains rules fire.
So to help clarify those requirements, imagine the following examples:
1) In some forum system, the administrator needs to be able to configure when emails are sent.
Here, I would write some rules like "when the config flag called sendUserEmail is set to true, send an email to the user" and "when the config flag called sendAdministratorEmail is true and the user has posted less than 5 postings, send an email to the administrator".
2) A tarif system needs to be configurable, so that the best tarif can be offered to customers.
For that, I could write rules like these: "when the person is younger than 26 the Youth tarif is applicable", "when the person is older than 59, the Senior tarif is applicable", and "when the person is neither a youth, nor a senior, then they should be given the Default tarif, unless they have had an account for more than 24 months, in which case they should be offered the Loyalty tarif".
3) A train ticket can be considered to be a product. Depending upon the travel request, different products are suitable.
A rule here, could be something like: "if the travel distance is more than 100km and first class is desired, then product A is to be sold."
Finally, a more complex example, involving some iteration of the input, rather than just property evaluation:
4) Timetable software needs to deterimine when students can leave school.
A rule for that might read: "If a class contains any student under age 10, the entire class gets to leave early. Otherwise, they leave at the normal time."
So, with those requirements in mind, I went to look for an expression language. I started with the unified expression language specified in JSP 2.1. Using the jasper jar used in Tomcat and Apache Commons EL jar, I got something up and running very quickly. Then I discovered the MVEL library from Codehaus.org, which incidentally is used in Drools (the leading Java rule engine?) and it worked even better. It offers far more functionality as far as I can tell.
So, I designed my rule engine to work like this:
1) An engine is configured with some rules.
2) A rule has these attributes:
- namespace: an engine may contain many rules, but only some may be relevant to a particular call and this namespace can be used for filtering
- name: a unique name within a namespace
- expression: an MVEL expression for the rule
- outcome: a string which the engine might use if this rules expression evaluates to true
- priority: an integer. The bigger the value, the higher the priority.
- description: a useful description to aid the management of rules.
3) The engine is given an input object and evaluates all rules (optionally within a namespace) and either:
a) returns all rules which evaluate to true,
b) returns the outcome (string) from the rule with the highest priority, out of all rules evaluating to true,
c) execute an action (defined within the application) which is associated with the outcome of the rule with the highest priority, out of all rules evaluating to true.
4) "Action"s are instances of classes which the application programmer can supply. An action is given a name. When the engine is asked to execute an action based on the rules, the name of the action matching the "winning" rule's outcome is executed.
5) A rule can be built up of "sub-rules". A subrule is only ever used as a building block on which to base more complex rules. When evaluating rules, the engine will never select a subrule to be the best (highest priority) "winning" rule, i.e. one evaluating to true. Subrules make it easier to build complex rules, as I shall show shortly.
So, time for some code!
First, let's look at the code for the tarif system:
Rule r1 = new Rule("YouthTarif", "input.person.age < 26", "YT2011", 3, "ch.maxant.someapp.tarifs", null); Rule r2 = new Rule("SeniorTarif", "input.person.age > 59", "ST2011", 3, "ch.maxant.someapp.tarifs", null); Rule r3 = new Rule("DefaultTarif", "!#YouthTarif && !#SeniorTarif", "DT2011", 3, "ch.maxant.someapp.tarifs", null); Rule r4 = new Rule("LoyaltyTarif", "#DefaultTarif && input.account.ageInMonths > 24", "LT2011", 4, "ch.maxant.someapp.tarifs", null); List<Rule> rules = Arrays.asList(r1, r2, r3, r4); Engine engine = new Engine(rules, true); TarifRequest request = new TarifRequest(); request.setPerson(new Person("p")); request.setAccount(new Account()); request.getPerson().setAge(24); request.getAccount().setAgeInMonths(5);String tarif = engine.getBestOutcome(request);
So, in the above code, I have added 4 rules to the engine, and told the engine to throw an exception if any rule cannot be pre-compiled. Then, I created a TarifRequest, which is the input object. That object is passed into the engine, when I ask the engine to give me the best outcome. In this case, the best outcome is the string "YT2011", the name of the most suitable tarif for the customer I added to the tarif request.
How does it all work? When the engine is given the rules, it does some validation on them, and pre-compiles the rules (to improve overall performance). Notice how the first two rules refer to an object called "input"? That is the object passed into the "getBestOutcome" method on the engine. The engine passes the input object to the MVEL class together with each rules expression. Anytime an expression evaluates to "true", the rule is put to the side as a candidate to be the winner. At the end, the candidates are sorted in order of priority, and the outcome field of the rule with the highest priority is returned by the engine.
Notice how the third and fourth rules contain the '#' character. That is not standard MVEL expression language. The engine examines all rules when they are passed to it, and it replaces any token starting with a hash symbol, with the expression found in the rule named the same as the token. It wraps the expression in brackets. The logger outputs the full rule after reference rules have been resolved and replaced, just in case you want to check the rule.
In the above business case, we were only interested in the best tarif for the customer. Equally, we might have been interested in a list of possible tarifs, so that we could offer the customer a choice. In that case, we could have called the "getMatchingRules" method on the engine, which would have returned all rules, sorted by priority. The tarif names are (in this case) the "outcome" field of the rules.
In the above example, I wanted to receive any of the four outcomes, from the four rules. Sometimes however, you might want to build complex rules based on building blocks, but you might never want those building blocks to be a winning outcome. The train trip example from above can be used to show what I mean here:
Rule rule1 = new SubRule("longdistance", "input.distance > 100", "ch.maxant.produkte", null); Rule rule2 = new SubRule("firstclass", "input.map[\"travelClass\"] == 1", "ch.maxant.produkte", null); Rule rule3 = new Rule("productA", "#longdistance && #firstclass", "productA", 3, "ch.maxant.produkte", null); List<Rule> rules = Arrays.asList(rule1, rule2, rule3); Engine e = new Engine(rules, true); TravelRequest request = new TravelRequest(150); request.put("travelClass", 1); List rs = e.getMatchingRules(request);
In the above code, I build rule3 from two subrules. But I never want the outcomes of those building blocks to be output from the engine. So I create them as SubRules. SubRules don't have an outcome field or priority. They are simply used to build up more complex rules. After the engine has used the sub-rules to replace all tokens beginning in a hash during initialisation, it discards the SubRules - they are not evaluated.
The TravelRequest above takes a distance in the constructor, and contains a map of additional parameters. MVEL let's you easily access map values using the syntax shown in rule 2.
Next, consider the business case of wanting to configure an forum system. The code below introduces actions. Actions are created by the application programmer and supplied to the engine. The engine takes the outcomes (as described in the first example), and searches for actions with the same names as those outcomes, and calls the "execute" method on those actions (they all implement the IAction interface). This functionality is useful when a system must be capable of predefined things, but the choice of what to do needs to be highly configurable and independent of deployment.
Rule r1 = new Rule("SendEmailToUser", "input.config.sendUserEmail == true", "SendEmailToUser", 1, "ch.maxant.someapp.config", null); Rule r2 = new Rule("SendEmailToModerator", "input.config.sendAdministratorEmail == true and input.user.numberOfPostings < 5", "SendEmailToModerator", 2, "ch.maxant.someapp.config", null); List<Rule> rules = Arrays.asList(r1, r2); final List<String> log = new ArrayList<String>(); Action<ForumSetup, Void> a1 = new Action<ForumSetup, Void>("SendEmailToUser") { @Override public Void execute(ForumSetup input) { log.add("Sending email to user!"); return null; } }; Action<ForumSetup, Void> a2 = new Action<ForumSetup, Void>("SendEmailToModerator") { @Override public Void execute(ForumSetup input) { log.add("Sending email to moderator!"); return null; } }; Engine engine = new Engine(rules, true); ForumSetup setup = new ForumSetup(); setup.getConfig().setSendUserEmail(true); setup.getConfig().setSendAdministratorEmail(true); setup.getUser().setNumberOfPostings(2); engine.executeAllActions(setup, Arrays.asList(a1, a2));
In the code above, the actions are passed to the engine when we call the "executeAllActions" method. In this case, both actions are executed, because the setup object causes both rules to evaluate to true. Note that the actions are executed in the order of highest priority rule first. Each action is only ever executed once - it's name is noted after execution and it will not be executed again, until the engines "execute*Action*" method is called again. Also, if you only want the action associated with the best outcome to be executed, call the "executeBestAction" method instead of "executeAllActions".
Finally, let's consider the classroom example.
String expression = "for(student : input.students){" + "if(student.age < 10) return true;" + "}" + "return false;"; Rule r1 = new Rule("containsStudentUnder10", expression , "leaveEarly", 1, "ch.maxant.rules", "If a class contains a student under 10 years of age, then the class may go home early"); Rule r2 = new Rule("default", "true" , "leaveOnTime", 0, "ch.maxant.rules", "this is the default"); Classroom classroom = new Classroom(); classroom.getStudents().add(new Person(12)); classroom.getStudents().add(new Person(10)); classroom.getStudents().add(new Person(8)); Engine e = new Engine(Arrays.asList(r1, r2), true); String outcome = e.getBestOutcome(classroom);
The outcome above is "leaveEarly", because the classroom contains one student whose age is less than 10. MVEL let's you write some pretty comprehensive expressions, and is really a programming language in it's own right. The engine simply requires a rule to return true, if the rule is to be considered a candidate for firing.
There are more examples in the JUnit tests contained in the source code.
So, the requirements are fulfiled, except for "It should be possible to store rules in a database". While this library doesn't support reading and writing rules to / from a database, rules are String based. So it wouldn't be hard to create some JDBC or JPA code which reads rules out of a database and populates Rule objects and passes them to the Engine. I haven't added this to the library, because normally these things as well as the management of rules is something quite project specific. And because my library will never be as cool or popular as Drools, I'm not sure it would be worth my while to add such functionality.
I've put the rule engine into an OSGi library with the LGPL licence and it can be downloaded from my tools site. This library depends on MVEL, which can be downloaded here (I used version 2.0.19). If you like it, let me know!
From http://blog.maxant.co.uk/pebble/2011/11/12/1321129560000.html
Opinions expressed by DZone contributors are their own.
Comments